{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "cover-flow",
  "title": "Cover Flow",
  "description": "A physics 3D cover flow — the centered slide faces front while neighbours rotate away in perspective, with momentum drag and snap.",
  "dependencies": ["framer-motion"],
  "registryDependencies": ["@godui/godui-theme"],
  "files": [
    {
      "path": "packages/components/src/cover-flow/cover-flow.tsx",
      "content": "\"use client\";\n\nimport { motion, type PanInfo, useReducedMotion } from \"framer-motion\";\nimport * as React from \"react\";\n\nexport type CoverFlowProps = Omit<\n  React.HTMLAttributes<HTMLDivElement>,\n  \"children\" | \"onChange\"\n> & {\n  /** Slides, in order. The centered one faces front. */\n  children?: React.ReactNode;\n  /** Index shown at center on mount. */\n  defaultIndex?: number;\n  /** Fired when the centered slide settles on a new index. */\n  onChange?: (index: number) => void;\n  /** Slide width in px. */\n  itemWidth?: number;\n  /** Slide height in px. */\n  itemHeight?: number;\n  /** Extra px added between slide centers, on top of the overlap. */\n  gap?: number;\n  /** CSS perspective depth for the 3D stage. */\n  perspective?: number;\n  /** Render a fading floor reflection under each slide. */\n  reflection?: boolean;\n};\n\n// SPRING.smooth — surfaces / shared-layout morph.\nconst MORPH_SPRING = {\n  type: \"spring\",\n  stiffness: 320,\n  damping: 32,\n  mass: 0.9,\n} as const;\n\nconst clampIndex = (v: number, count: number) =>\n  Math.max(0, Math.min(count - 1, v));\n\ntype CoverItemProps = {\n  offset: number;\n  spacing: number;\n  width: number;\n  height: number;\n  reflection: boolean;\n  reduce: boolean;\n  onSelect: () => void;\n  children: React.ReactNode;\n};\n\nconst CoverItem: React.FC<CoverItemProps> = ({\n  offset,\n  spacing,\n  width,\n  height,\n  reflection,\n  reduce,\n  onSelect,\n  children,\n}) => {\n  const sign = Math.sign(offset);\n  const abs = Math.abs(offset);\n  const near = Math.min(abs, 1);\n  const far = Math.max(abs - 1, 0);\n  // Neighbours sit a full pitch out; farther cards compress so the row\n  // telescopes toward the edges instead of scrolling off-screen.\n  const x = sign * (near * spacing + far * spacing * 0.55);\n  const rotateY = reduce ? 0 : -Math.max(-1, Math.min(1, offset)) * 52;\n  const z = reduce ? 0 : -Math.min(abs, 3) * 130;\n  const scale = 1 - Math.min(abs, 3) * 0.08;\n  const opacity =\n    abs > 3.4 ? 0 : Math.max(0.15, 1 - Math.max(abs - 1, 0) * 0.28);\n\n  return (\n    <motion.div\n      className=\"absolute top-1/2 left-1/2 cursor-pointer [transform-style:preserve-3d]\"\n      style={{\n        width,\n        height,\n        marginLeft: -width / 2,\n        marginTop: -height / 2,\n        zIndex: Math.round(100 - abs * 10),\n        transformPerspective: 1000,\n      }}\n      initial={false}\n      animate={{ x, rotateY, z, scale, opacity }}\n      transition={MORPH_SPRING}\n      onTap={onSelect}\n    >\n      <div className=\"size-full overflow-hidden rounded-2xl border border-border bg-card shadow-2xl\">\n        {children}\n      </div>\n      {reflection && !reduce ? (\n        <div\n          aria-hidden\n          className=\"pointer-events-none absolute inset-x-0 top-full mt-1 h-1/2 overflow-hidden rounded-2xl opacity-30 [mask-image:linear-gradient(to_bottom,rgba(0,0,0,0.6),transparent)] [transform:scaleY(-1)]\"\n        >\n          <div className=\"size-full overflow-hidden rounded-2xl border border-border bg-card\">\n            {children}\n          </div>\n        </div>\n      ) : null}\n    </motion.div>\n  );\n};\n\nconst CoverFlow = React.forwardRef<HTMLDivElement, CoverFlowProps>(\n  (\n    {\n      children,\n      defaultIndex = 0,\n      onChange,\n      itemWidth = 240,\n      itemHeight = 300,\n      gap = 0,\n      perspective = 1200,\n      reflection = true,\n      className,\n      ...props\n    },\n    forwardedRef,\n  ) => {\n    const reduce = useReducedMotion() ?? false;\n    const items = React.Children.toArray(children);\n    const count = items.length;\n    const spacing = itemWidth * 0.72 + gap;\n\n    const [active, setActive] = React.useState(() =>\n      clampIndex(defaultIndex, count),\n    );\n    const dragFrom = React.useRef(0);\n\n    const onChangeRef = React.useRef(onChange);\n    onChangeRef.current = onChange;\n\n    const goTo = React.useCallback(\n      (index: number) => {\n        const next = clampIndex(index, count);\n        setActive((prev) => {\n          if (prev !== next) onChangeRef.current?.(next);\n          return next;\n        });\n      },\n      [count],\n    );\n\n    const handlePanStart = () => {\n      dragFrom.current = active;\n    };\n    const handlePan = (_e: unknown, info: PanInfo) => {\n      goTo(Math.round(dragFrom.current - info.offset.x / spacing));\n    };\n    const handlePanEnd = (_e: unknown, info: PanInfo) => {\n      // A fast flick nudges one extra slide in the throw direction.\n      if (Math.abs(info.velocity.x) > 600) {\n        goTo(active - Math.sign(info.velocity.x));\n      }\n    };\n\n    const handleKey = (e: React.KeyboardEvent) => {\n      if (e.key === \"ArrowLeft\") {\n        e.preventDefault();\n        goTo(active - 1);\n      } else if (e.key === \"ArrowRight\") {\n        e.preventDefault();\n        goTo(active + 1);\n      }\n    };\n\n    return (\n      <div\n        ref={forwardedRef}\n        data-slot=\"cover-flow\"\n        className={`flex flex-col items-center gap-5 ${className ?? \"\"}`}\n        {...props}\n      >\n        <motion.div\n          role=\"group\"\n          aria-roledescription=\"carousel\"\n          aria-label=\"Cover flow\"\n          className=\"relative cursor-grab touch-none overflow-hidden active:cursor-grabbing\"\n          style={{\n            width: itemWidth * 3,\n            maxWidth: \"90vw\",\n            height: itemHeight * 1.7,\n            perspective,\n          }}\n          onPanStart={handlePanStart}\n          onPan={handlePan}\n          onPanEnd={handlePanEnd}\n        >\n          <div className=\"absolute inset-0 [transform-style:preserve-3d]\">\n            {items.map((child, i) => (\n              <CoverItem\n                // biome-ignore lint/suspicious/noArrayIndexKey: slides are positional\n                key={i}\n                offset={i - active}\n                spacing={spacing}\n                width={itemWidth}\n                height={itemHeight}\n                reflection={reflection}\n                reduce={reduce}\n                onSelect={() => goTo(i)}\n              >\n                {child}\n              </CoverItem>\n            ))}\n          </div>\n        </motion.div>\n\n        <div className=\"flex items-center gap-4\">\n          <button\n            type=\"button\"\n            onClick={() => goTo(active - 1)}\n            onKeyDown={handleKey}\n            disabled={active === 0}\n            aria-label=\"Previous\"\n            className=\"grid size-10 place-items-center rounded-full border border-border bg-card text-foreground shadow-sm [transition:transform_150ms,background_150ms] hover:bg-accent active:scale-95 disabled:opacity-40\"\n          >\n            ‹\n          </button>\n          <span className=\"text-sm tabular-nums text-muted-foreground\">\n            {active + 1} / {count}\n          </span>\n          <button\n            type=\"button\"\n            onClick={() => goTo(active + 1)}\n            onKeyDown={handleKey}\n            disabled={active === count - 1}\n            aria-label=\"Next\"\n            className=\"grid size-10 place-items-center rounded-full border border-border bg-card text-foreground shadow-sm [transition:transform_150ms,background_150ms] hover:bg-accent active:scale-95 disabled:opacity-40\"\n          >\n            ›\n          </button>\n        </div>\n      </div>\n    );\n  },\n);\nCoverFlow.displayName = \"CoverFlow\";\n\nexport { CoverFlow };\n",
      "type": "registry:ui",
      "target": "components/godui/cover-flow.tsx"
    }
  ],
  "type": "registry:ui"
}
