Blocks

Layout 349

standard
Text Align LeftImage/Video Right2 ColumnsButtonsImageMultiple Images
Layout 349

Code

relume-layout349.tsx
"use client";

import { useEffect, useRef, useMemo, useState } from "react";
import clsx from "clsx";
import { Button, type ButtonProps } from "@/components/ui/button";
import { ChevronRight } from "relume-icons";

type ImageProps = {
  src: string;
  alt?: string;
};

type ContentProps = {
  tagline: string;
  heading: string;
  description: string;
  buttons: ButtonProps[];
  image: ImageProps;
};

type Props = {
  contents: ContentProps[];
  images: ImageProps[];
};

export type Layout349Props = React.ComponentPropsWithoutRef<"section"> & Partial<Props>;

export const Layout349 = (props: Layout349Props) => {
  const { contents, images } = {
    ...Layout349Defaults,
    ...props,
  };

  const [activeSection, setActiveSection] = useState(0);
  const sectionRefs = useRef<(HTMLDivElement | null)[]>([]);
  const overlayIndices = useMemo(() => new Set([0, 2]), []);

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        const visible = entries
          .filter((e) => e.isIntersecting)
          .sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0];
        if (visible) {
          const idx = Number((visible.target as HTMLElement).dataset.index);
          if (!Number.isNaN(idx)) setActiveSection((prev) => (prev === idx ? prev : idx));
        }
      },
      { threshold: [0.5, 0.75, 1] },
    );
    sectionRefs.current.forEach((el: HTMLDivElement | null) => el && observer.observe(el));
    return () => observer.disconnect();
  }, []);

  return (
    <section className="relative isolate">
      <div className="pointer-events-none absolute inset-0 -z-10">
        <div
          className={clsx(
            "h-full bg-scheme-foreground transition-opacity duration-300 md:sticky md:top-0 md:h-screen",
            overlayIndices.has(activeSection) ? "opacity-100" : "opacity-0",
          )}
        />
      </div>
      <div className="px-[5%]">
        <div className="container">
          <div className="relative grid gap-x-12 py-16 sm:gap-y-12 md:grid-cols-2 md:py-0 lg:gap-x-20">
            <div className="grid grid-cols-1 gap-12 md:block">
              {contents.map((content, index) => (
                <div
                  key={index}
                  ref={(el) => {
                    sectionRefs.current[index] = el;
                  }}
                  data-index={index}
                  className="flex flex-col items-start justify-center md:h-screen"
                >
                  <p className="mb-3 font-semibold md:mb-4">{content.tagline}</p>
                  <h2 className="mb-5 text-h2 font-bold md:mb-6">{content.heading}</h2>
                  <p className="text-medium">{content.description}</p>
                  <div className="mt-6 flex flex-wrap items-center gap-4 md:mt-8">
                    {content.buttons.map((button, index) => (
                      <Button key={index} {...button}>
                        {button.title}
                      </Button>
                    ))}
                  </div>
                  <div className="mt-10 block w-full md:hidden">
                    <img
                      src={content.image.src}
                      className="w-full rounded-image"
                      alt={content.image.alt}
                    />
                  </div>
                </div>
              ))}
            </div>
            <div className="sticky top-0 hidden h-screen md:flex md:flex-col md:items-center md:justify-center">
              {images.map((image, index) => (
                <img
                  key={index}
                  src={image.src}
                  className={clsx("absolute w-full rounded-image", {
                    "opacity-100": activeSection === index,
                    "opacity-0": activeSection !== index,
                  })}
                  alt={image.alt}
                />
              ))}
            </div>
          </div>
        </div>
      </div>
    </section>
  );
};

export const Layout349Defaults: Props = {
  contents: [
    {
      tagline: "Tagline",
      heading: "Medium length section heading goes here",
      description:
        "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat.",
      buttons: [
        { title: "Button", variant: "secondary" },
        {
          title: "Button",
          variant: "link",
          size: "link",
          iconRight: <ChevronRight className="text-scheme-text" />,
        },
      ],
      image: {
        src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image-1.svg",
        alt: "Relume placeholder image 1",
      },
    },
    {
      tagline: "Tagline",
      heading: "Medium length section heading goes here",
      description:
        "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat.",
      buttons: [
        { title: "Button", variant: "secondary" },
        {
          title: "Button",
          variant: "link",
          size: "link",
          iconRight: <ChevronRight className="text-scheme-text" />,
        },
      ],
      image: {
        src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image-2.svg",
        alt: "Relume placeholder image 2",
      },
    },
    {
      tagline: "Tagline",
      heading: "Medium length section heading goes here",
      description:
        "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat.",
      buttons: [
        { title: "Button", variant: "secondary" },
        {
          title: "Button",
          variant: "link",
          size: "link",
          iconRight: <ChevronRight className="text-scheme-text" />,
        },
      ],
      image: {
        src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image-3.svg",
        alt: "Relume placeholder image 3",
      },
    },
    {
      tagline: "Tagline",
      heading: "Medium length section heading goes here",
      description:
        "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat.",
      buttons: [
        { title: "Button", variant: "secondary" },
        {
          title: "Button",
          variant: "link",
          size: "link",
          iconRight: <ChevronRight className="text-scheme-text" />,
        },
      ],
      image: {
        src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image-4.svg",
        alt: "Relume placeholder image 4",
      },
    },
  ],
  images: [
    {
      src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image-1.svg",
      alt: "Relume placeholder image 1",
    },
    {
      src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image-2.svg",
      alt: "Relume placeholder image 2",
    },
    {
      src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image-3.svg",
      alt: "Relume placeholder image 3",
    },
    {
      src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image-4.svg",
      alt: "Relume placeholder image 4",
    },
  ],
};

npm i @radix-ui/react-dialog @radix-ui/react-slot @radix-ui/react-tabs class-variance-authority@0.7.1 clsx@^2.1.1 motion@^12.15.0 relume-icons@1.3.0 tailwind-merge@^2.2.2