card-stack

PreviousNext
Docs API Reference
uicapsuleblock

Preview

Loading preview…
/card-stack.tsx
import React from "react";
import { useGSAP } from "@gsap/react";
import gsap from "gsap";

type CardStackProps = {
  cards: { src: string; href: string }[];
  className?: string;
};

export const CardStack = ({ cards }: CardStackProps) => {
  const rootRef = React.useRef<HTMLDivElement>(null);

  useGSAP(() => {
    let incr = 0;

    const root = rootRef.current;
    const slides = root?.querySelectorAll(".slide");
    const slideContent = root?.querySelectorAll(".content");

    if (!root || !slides || !slideContent) {
      return;
    }

    const deltaObject = { delta: 0 };

    const baseDuration = slides.length / 2;
    const staggerEach = 0.5;
    const repeatDelay = baseDuration - staggerEach;

    const deltaTo = gsap.quickTo(deltaObject, "delta", {
      duration: 0.8,
      ease: "power1",
      onUpdate: () => {
        tl.totalTime(deltaObject.delta);
      },
    });
    const rotY = gsap.quickTo(root, "rotationY", {
      duration: 0.3,
      ease: "power3",
    });
    const rotX = gsap.quickTo(root, "rotationX", {
      duration: 0.3,
      ease: "power3",
    });

    const tl = gsap.timeline({ paused: true, smoothChildTiming: true });
    tl.fromTo(
      slides,
      {
        y: "-15vw",
        z: "-60vw",
        force3D: true,
      },
      {
        y: "0vw",
        z: "0vw",
        ease: "none",
        force3D: true,
        immediateRender: false,
        duration: baseDuration,
        stagger: {
          each: staggerEach,
          repeat: -1,
          repeatDelay: 0,
        },
      },
    );

    tl.fromTo(
      slideContent,
      {
        y: "10vh",
      },
      {
        y: 0,
        ease: "back.out(1.05)",
        duration: staggerEach,
        stagger: {
          each: staggerEach,
          repeat: -1,
          repeatDelay: repeatDelay,
          onRepeat() {
            this.targets()[0].style.transform = "translateY(100vh)";
          },
        },
      },
      "<",
    );

    tl.fromTo(
      slideContent,
      {
        y: 0,
      },
      {
        y: "200vh",
        ease: "power3.in",
        duration: staggerEach,
        delay: repeatDelay,
        stagger: {
          each: staggerEach,
          repeat: -1,
          repeatDelay: repeatDelay,
          onRepeat() {
            this.targets()[0].style.transform = "translateY(0vh)";
          },
        },
      },
      "<",
    );

    const beginDistance = slides.length * 100;

    tl.totalTime(beginDistance);

    deltaTo(beginDistance + 0.01, beginDistance);

    const snap = gsap.utils.snap(baseDuration / slides.length);

    const handleWheel = (e: WheelEvent) => {
      incr -= e.deltaY / 1000;
      deltaTo(snap(incr + beginDistance));
    };

    const handleMouseMove = (e: MouseEvent) => {
      const valX = (e.clientY / window.innerHeight - 0.5) * 5;
      const valY = (e.clientX / window.innerWidth - 0.5) * 10;
      rotX(-valX);
      rotY(valY);
    };

    window.addEventListener("wheel", handleWheel, { passive: true });
    root?.addEventListener("mousemove", handleMouseMove);

    return () => {
      window.removeEventListener("wheel", handleWheel);
      root?.removeEventListener("mousemove", handleMouseMove);
    };
  });

  return (
    <section className="effect h-screen overflow-hidden bg-[#121212] text-[#f1f1f1] [perspective:150vw]">
      <div className="root h-full [transform-style:preserve-3d]" ref={rootRef}>
        {cards.map((card) => (
          <a
            key={card.src}
            className="slide absolute top-[calc(50%-5vw)] left-[calc(50%-20vw)] block [aspect-ratio:1.75] w-[40vw]"
            href={card.href}
            target="_blank"
          >
            <img
              className="content h-full w-full rounded-[12px] object-cover"
              src={card.src}
            />
          </a>
        ))}
      </div>
    </section>
  );
};

Installation

npx shadcn@latest add @uicapsule/card-stack

Usage

import { CardStack } from "@/components/card-stack"
<CardStack />