{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"flip-stack","type":"registry:ui","title":"Flip Stack","description":"A pile of cards where clicking tosses the top card up, flips it end over end and drops it behind the pile as the rest step forward.","dependencies":["motion"],"registryDependencies":[],"files":[{"path":"registry/ui/flip-stack.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useState, type KeyboardEvent, type ReactNode } from \"react\";\nimport { motion, useReducedMotion } from \"motion/react\";\n\nexport interface FlipStackProps {\n  /** Card faces, top of the pile first. */\n  cards: ReactNode[];\n  /** Shared back face shown while a card is upside down. */\n  back?: ReactNode;\n  /** Card width in px. */\n  width?: number;\n  /** Card height in px. */\n  height?: number;\n  /** Px each card behind peeks out above the one in front. */\n  offset?: number;\n  /** Scale lost per card of depth. */\n  scaleStep?: number;\n  /** How many cards stay visible in the pile. */\n  visible?: number;\n  /** How high the card is tossed, as a fraction of its height. */\n  lift?: number;\n  /** Seconds for one full flip. */\n  duration?: number;\n  /** Accessible name for the stack. */\n  label?: string;\n  onChange?: (topIndex: number) => void;\n  className?: string;\n}\n\nconst DefaultBack = () => (\n  <div\n    className=\"grid size-full place-items-center bg-muted\"\n    style={{\n      backgroundImage:\n        \"repeating-linear-gradient(45deg, color-mix(in srgb, currentColor 7%, transparent) 0 2px, transparent 2px 12px), radial-gradient(circle at 50% 50%, color-mix(in srgb, currentColor 10%, transparent), transparent 60%)\",\n    }}\n  >\n    <span className=\"grid size-14 place-items-center rounded-full border border-border bg-card text-lg font-semibold text-muted-foreground\">✦</span>\n  </div>\n);\n\n/**\n * A pile of cards. Clicking (or Enter/Space/→) tosses the top card up, flips it over\n * end-to-end and drops it behind the pile while the rest step forward.\n */\nexport function FlipStack({\n  cards,\n  back,\n  width = 240,\n  height = 300,\n  offset = 16,\n  scaleStep = 0.06,\n  visible = 3,\n  lift = 0.4,\n  duration = 0.9,\n  label = \"Card stack\",\n  onChange,\n  className,\n}: FlipStackProps) {\n  const n = cards.length;\n  const [order, setOrder] = useState(() => cards.map((_, i) => i));\n  // Whole turns each card has made, so rotation keeps increasing instead of unwinding.\n  const [turns, setTurns] = useState<number[]>(() => cards.map(() => 0));\n  const [tossing, setTossing] = useState<number | null>(null);\n  const reduce = useReducedMotion();\n\n  // Keep state valid if the number of cards changes.\n  const pile = order.length === n ? order : cards.map((_, i) => i);\n\n  const land = (card: number) => {\n    const next = [...pile.slice(1), card];\n    setOrder(next);\n    setTurns((t) => cards.map((_, i) => (t[i] ?? 0) + (i === card ? 1 : 0)));\n    setTossing(null);\n    onChange?.(next[0]);\n  };\n\n  const flip = () => {\n    if (n < 2 || tossing !== null) return;\n    if (reduce) land(pile[0]);\n    else setTossing(pile[0]);\n  };\n\n  const onKey = (e: KeyboardEvent) => {\n    if (e.key === \"Enter\" || e.key === \" \" || e.key === \"ArrowRight\") {\n      e.preventDefault();\n      flip();\n    }\n  };\n\n  const face = \"absolute inset-0 overflow-hidden rounded-[inherit] [backface-visibility:hidden] [-webkit-backface-visibility:hidden]\";\n\n  return (\n    <div\n      role=\"button\"\n      tabIndex={0}\n      aria-label={`${label}: card ${pile[0] + 1} of ${n}. Press to flip to the next card.`}\n      onClick={flip}\n      onKeyDown={onKey}\n      className={`relative cursor-pointer rounded-2xl outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-8 focus-visible:ring-offset-background ${className ?? \"\"}`}\n      style={{ width, height, perspective: 1400 }}\n    >\n      {cards.map((card, i) => {\n        const pos = pile.indexOf(i);\n        const up = tossing === i;\n        const depth = Math.min(pos, visible);\n        const tilt = pos === 0 ? 0 : (i % 2 ? 1.6 : -1.6) * Math.min(pos, 2);\n        const rest = (turns[i] ?? 0) * 360;\n        return (\n          <motion.div\n            key={i}\n            aria-hidden={pos !== 0}\n            initial={false}\n            animate={\n              up\n                ? { y: -height * lift, rotateX: rest + 180, rotate: -4, scale: 1.04, opacity: 1 }\n                : { y: -depth * offset, rotateX: rest, rotate: tilt, scale: 1 - depth * scaleStep, opacity: pos < visible ? 1 : 0 }\n            }\n            transition={\n              up\n                ? { duration: duration * 0.45, ease: [0.3, 0.6, 0.35, 1] }\n                : { type: \"spring\", duration: duration * 0.7, bounce: 0.25, opacity: { duration: 0.2 } }\n            }\n            onAnimationComplete={() => {\n              if (up) land(i);\n            }}\n            className=\"absolute inset-0 rounded-2xl shadow-[0_20px_40px_-18px_rgba(0,0,0,.75)]\"\n            style={{ zIndex: up ? n + 1 : n - pos, transformStyle: \"preserve-3d\", transformOrigin: \"50% 50%\" }}\n          >\n            <div className={face}>{card}</div>\n            <div className={face} style={{ transform: \"rotateX(180deg)\" }}>\n              {back ?? <DefaultBack />}\n            </div>\n          </motion.div>\n        );\n      })}\n    </div>\n  );\n}\n"}]}