{"$schema":"https://ui.shadcn.com/schema/registry.json","author":"Kaiyu Hsu <uicapsule@kyh.io>","categories":["cards-grids","minimal"],"dependencies":["@gsap/react","gsap"],"description":"An infinite stack of cards placed with perspective one on top of another.","devDependencies":[],"files":[{"content":"import { useRef } from \"react\";\nimport { useGSAP } from \"@gsap/react\";\nimport { gsap } from \"gsap\";\n\ninterface CardStackProps {\n  cards: { src: string; href: string; alt: string }[];\n}\n\n/** Wheel delta (px) required to advance the stack by one timeline second. */\nconst WHEEL_PIXELS_PER_SECOND = 1000;\n/** Max tilt (deg) applied across the full height/width of the viewport. */\nconst TILT_X_RANGE = 5;\nconst TILT_Y_RANGE = 10;\n\nconst setContentTransformOnRepeat = (transform: string) =>\n  function onRepeat(this: gsap.core.Tween) {\n    const [target] = this.targets();\n    if (target instanceof HTMLElement) {\n      target.style.transform = transform;\n    }\n  };\n\nexport const CardStack = ({ cards }: CardStackProps) => {\n  const rootRef = useRef<HTMLDivElement>(null);\n\n  useGSAP(() => {\n    let incr = 0;\n\n    const root = rootRef.current;\n    if (!root) {\n      return;\n    }\n\n    const slides = root.querySelectorAll(\".slide\");\n    const slideContent = root.querySelectorAll(\".content\");\n\n    if (slides.length === 0) {\n      return;\n    }\n\n    const deltaObject = { delta: 0 };\n\n    const baseDuration = slides.length / 2;\n    const staggerEach = 0.5;\n    const repeatDelay = baseDuration - staggerEach;\n\n    const tl = gsap.timeline({ paused: true, smoothChildTiming: true });\n\n    const deltaTo = gsap.quickTo(deltaObject, \"delta\", {\n      duration: 0.8,\n      ease: \"power1\",\n      onUpdate: () => {\n        tl.totalTime(deltaObject.delta);\n      },\n    });\n    const rotY = gsap.quickTo(root, \"rotationY\", {\n      duration: 0.3,\n      ease: \"power3\",\n    });\n    const rotX = gsap.quickTo(root, \"rotationX\", {\n      duration: 0.3,\n      ease: \"power3\",\n    });\n\n    tl.fromTo(\n      slides,\n      {\n        force3D: true,\n        y: \"-15vw\",\n        z: \"-60vw\",\n      },\n      {\n        duration: baseDuration,\n        ease: \"none\",\n        force3D: true,\n        immediateRender: false,\n        stagger: {\n          each: staggerEach,\n          repeat: -1,\n          repeatDelay: 0,\n        },\n        y: \"0vw\",\n        z: \"0vw\",\n      },\n    );\n\n    tl.fromTo(\n      slideContent,\n      {\n        y: \"10vh\",\n      },\n      {\n        duration: staggerEach,\n        ease: \"back.out(1.05)\",\n        stagger: {\n          each: staggerEach,\n          onRepeat: setContentTransformOnRepeat(\"translateY(100vh)\"),\n          repeat: -1,\n          repeatDelay,\n        },\n        y: 0,\n      },\n      \"<\",\n    );\n\n    tl.fromTo(\n      slideContent,\n      {\n        y: 0,\n      },\n      {\n        delay: repeatDelay,\n        duration: staggerEach,\n        ease: \"power3.in\",\n        stagger: {\n          each: staggerEach,\n          onRepeat: setContentTransformOnRepeat(\"translateY(0vh)\"),\n          repeat: -1,\n          repeatDelay,\n        },\n        y: \"200vh\",\n      },\n      \"<\",\n    );\n\n    const beginDistance = slides.length * 100;\n\n    tl.totalTime(beginDistance);\n\n    deltaTo(beginDistance + 0.01, beginDistance);\n\n    const snap = gsap.utils.snap(baseDuration / slides.length);\n\n    const handleWheel = (e: WheelEvent) => {\n      e.preventDefault();\n      incr -= e.deltaY / WHEEL_PIXELS_PER_SECOND;\n      deltaTo(snap(incr + beginDistance));\n    };\n\n    const handleMouseMove = (e: MouseEvent) => {\n      const valX = (e.clientY / window.innerHeight - 0.5) * TILT_X_RANGE;\n      const valY = (e.clientX / window.innerWidth - 0.5) * TILT_Y_RANGE;\n      rotX(-valX);\n      rotY(valY);\n    };\n\n    root.addEventListener(\"wheel\", handleWheel, { passive: false });\n    root.addEventListener(\"mousemove\", handleMouseMove);\n\n    return () => {\n      root.removeEventListener(\"wheel\", handleWheel);\n      root.removeEventListener(\"mousemove\", handleMouseMove);\n    };\n  });\n\n  return (\n    <section className=\"effect h-screen overflow-hidden bg-[#121212] text-[#f1f1f1] [perspective:150vw]\">\n      <div className=\"root h-full [transform-style:preserve-3d]\" ref={rootRef}>\n        {cards.map((card) => (\n          <a\n            key={card.src}\n            className=\"slide absolute top-[calc(50%-5vw)] left-[calc(50%-20vw)] block [aspect-ratio:1.75] w-[40vw]\"\n            href={card.href}\n            target=\"_blank\"\n          >\n            <img\n              alt={card.alt}\n              className=\"content h-full w-full rounded-[12px] object-cover\"\n              src={card.src}\n            />\n          </a>\n        ))}\n      </div>\n    </section>\n  );\n};\n","path":"/card-stack.tsx","target":"uicapsule/card-stack/card-stack.tsx","type":"registry:file"},{"content":"\"use client\";\n\nimport { CardStack } from \"./card-stack\";\n\nconst rootUrl = \"https://d24l2zb4cwkekfpl.public.blob.vercel-storage.com/card-stack-1\";\n\nconst cards = [\n  { alt: \"UI Capsule\", href: \"https://www.uicapsule.com\", src: `${rootUrl}/uic.webp` },\n  { alt: \"KYH\", href: \"https://kyh.io\", src: `${rootUrl}/kyh.webp` },\n  { alt: \"Vibed Games\", href: \"https://vibedgames.com\", src: `${rootUrl}/vg.webp` },\n  { alt: \"Founding\", href: \"https://founding.so\", src: `${rootUrl}/found.webp` },\n  { alt: \"Init\", href: \"https://init.kyh.io\", src: `${rootUrl}/init.webp` },\n  { alt: \"TC\", href: \"https://tc.kyh.io\", src: `${rootUrl}/tc.webp` },\n  { alt: \"Data Embed\", href: \"https://dataembed.com\", src: `${rootUrl}/data.webp` },\n  { alt: \"Yours Sincerely\", href: \"https://yourssincerely.org\", src: `${rootUrl}/ys.webp` },\n];\n\nconst Preview = () => <CardStack cards={cards} />;\n\nexport default Preview;\n","path":"/preview.tsx","target":"uicapsule/card-stack/preview.tsx","type":"registry:file"}],"homepage":"https://www.uicapsule.com/ui/card-stack","name":"card-stack","registryDependencies":[],"title":"Scrollable Card Stack","type":"registry:block"}