{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "card-swap",
  "title": "Card Swap",
  "description": "A 3D stack of cards that cycles through a showcase loop, with pointer parallax and spring re-flow.",
  "dependencies": ["framer-motion"],
  "registryDependencies": ["@godui/godui-theme"],
  "files": [
    {
      "path": "packages/components/src/card-swap/card-swap.tsx",
      "content": "\"use client\";\n\nimport { motion, useReducedMotion } from \"framer-motion\";\nimport * as React from \"react\";\n\nexport type CardSwapProps = Omit<\n  React.HTMLAttributes<HTMLDivElement>,\n  \"children\"\n> & {\n  /** Cards to cycle through, front to back. Provide 2–5. */\n  children?: React.ReactNode;\n  /** Auto-advance interval in ms. Set `0` to disable. */\n  interval?: number;\n  /** Pause the auto-advance while the pointer is over the stack. */\n  pauseOnHover?: boolean;\n  /** Vertical offset (px) between stacked cards. */\n  offsetY?: number;\n  /** Horizontal offset (px) between stacked cards. */\n  offsetX?: number;\n  /** Scale step removed per card going back. */\n  scaleStep?: number;\n};\n\nconst CardSwap = React.forwardRef<HTMLDivElement, CardSwapProps>(\n  (\n    {\n      children,\n      interval = 3500,\n      pauseOnHover = true,\n      offsetY = 28,\n      offsetX = 22,\n      scaleStep = 0.06,\n      className,\n      onPointerMove,\n      onPointerLeave,\n      ...props\n    },\n    forwardedRef,\n  ) => {\n    const ref = React.useRef<HTMLDivElement>(null);\n    React.useImperativeHandle(\n      forwardedRef,\n      () => ref.current as HTMLDivElement,\n    );\n    const reduce = useReducedMotion();\n    const items = React.Children.toArray(children);\n    const n = items.length;\n\n    // `order[r]` is the item index sitting at rank `r` (0 = front).\n    const [order, setOrder] = React.useState(() =>\n      Array.from({ length: n }, (_, i) => i),\n    );\n    React.useEffect(() => {\n      setOrder(Array.from({ length: n }, (_, i) => i));\n    }, [n]);\n\n    const [tilt, setTilt] = React.useState({ x: 0, y: 0 });\n    const [paused, setPaused] = React.useState(false);\n\n    const advance = React.useCallback(() => {\n      setOrder((o) => (o.length ? [...o.slice(1), o[0] as number] : o));\n    }, []);\n    const retreat = React.useCallback(() => {\n      setOrder((o) =>\n        o.length ? [o[o.length - 1] as number, ...o.slice(0, -1)] : o,\n      );\n    }, []);\n\n    React.useEffect(() => {\n      if (!interval || paused || n < 2) return;\n      const t = setInterval(advance, interval);\n      return () => clearInterval(t);\n    }, [interval, paused, n, advance]);\n\n    const handleMove = (e: React.PointerEvent<HTMLDivElement>) => {\n      onPointerMove?.(e);\n      if (reduce) return;\n      const el = ref.current;\n      if (!el) return;\n      const rect = el.getBoundingClientRect();\n      const px = (e.clientX - rect.left) / rect.width - 0.5;\n      const py = (e.clientY - rect.top) / rect.height - 0.5;\n      setTilt({ x: -py * 12, y: px * 14 });\n    };\n    const handleLeave = (e: React.PointerEvent<HTMLDivElement>) => {\n      onPointerLeave?.(e);\n      setTilt({ x: 0, y: 0 });\n      if (pauseOnHover) setPaused(false);\n    };\n\n    // rank of each item index, so we can drive its slot from the item's position\n    const rankOf = React.useMemo(() => {\n      const m = new Array<number>(n);\n      order.forEach((itemIndex, rank) => {\n        m[itemIndex] = rank;\n      });\n      return m;\n    }, [order, n]);\n\n    return (\n      <div\n        ref={ref}\n        data-slot=\"card-swap\"\n        onPointerMove={handleMove}\n        onPointerEnter={() => pauseOnHover && setPaused(true)}\n        onPointerLeave={handleLeave}\n        className={`relative grid place-items-center [perspective:1200px] ${className ?? \"\"}`}\n        {...props}\n      >\n        <motion.div\n          className=\"relative h-full w-full [transform-style:preserve-3d]\"\n          animate={{ rotateX: tilt.x, rotateY: tilt.y }}\n          transition={{\n            type: \"spring\",\n            stiffness: 170,\n            damping: 12,\n            mass: 0.1,\n          }}\n        >\n          {items.map((child, i) => {\n            const r = rankOf[i] ?? 0;\n            return (\n              <motion.div\n                // biome-ignore lint/suspicious/noArrayIndexKey: index identifies a stable card\n                key={i}\n                className=\"absolute inset-0 will-change-transform\"\n                style={{ zIndex: n - r, transformStyle: \"preserve-3d\" }}\n                animate={{\n                  x: r * offsetX,\n                  y: -r * offsetY,\n                  scale: 1 - r * scaleStep,\n                  rotateZ: r * -2.5,\n                  opacity: r > 3 ? 0 : 1,\n                }}\n                transition={\n                  reduce\n                    ? { duration: 0 }\n                    : { type: \"spring\", stiffness: 320, damping: 32, mass: 0.9 }\n                }\n              >\n                {child}\n              </motion.div>\n            );\n          })}\n        </motion.div>\n\n        {/* Controls — keyboard accessible, do not steal focus from card content. */}\n        {n > 1 ? (\n          <div className=\"absolute -bottom-12 flex items-center gap-2\">\n            <button\n              type=\"button\"\n              onClick={retreat}\n              aria-label=\"Previous card\"\n              className=\"rounded-full border border-border bg-card px-3 py-1.5 text-sm text-foreground shadow-sm [transition:background_150ms] hover:bg-accent\"\n            >\n              ‹\n            </button>\n            <button\n              type=\"button\"\n              onClick={advance}\n              aria-label=\"Next card\"\n              className=\"rounded-full border border-border bg-card px-3 py-1.5 text-sm text-foreground shadow-sm [transition:background_150ms] hover:bg-accent\"\n            >\n              ›\n            </button>\n          </div>\n        ) : null}\n      </div>\n    );\n  },\n);\nCardSwap.displayName = \"CardSwap\";\n\nexport { CardSwap };\n",
      "type": "registry:ui",
      "target": "components/godui/card-swap.tsx"
    }
  ],
  "type": "registry:ui"
}
