{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "card-stack",
  "type": "registry:component",
  "title": "Card Stack",
  "description": "Payment cards tucked in a stitched leather wallet – hover and they rise out in a fan, click the front one to lift it clear and flip it, or a peeking one to pull it forward.",
  "categories": [
    "illustration",
    "Objects"
  ],
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/devl/card-stack.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { motion, useInView, useReducedMotion } from \"motion/react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type CardStackHue = \"sky\" | \"emerald\" | \"amber\" | \"violet\" | \"rose\" | \"orange\" | \"indigo\" | \"teal\";\n\nconst hues: Record<CardStackHue, string> = {\n  sky: \"from-sky-400 via-sky-500 to-blue-700\",\n  emerald: \"from-emerald-400 via-emerald-500 to-teal-700\",\n  amber: \"from-amber-300 via-amber-500 to-orange-600\",\n  violet: \"from-violet-400 via-violet-500 to-indigo-700\",\n  rose: \"from-rose-400 via-rose-500 to-pink-700\",\n  orange: \"from-orange-400 via-orange-500 to-red-600\",\n  indigo: \"from-indigo-400 via-indigo-600 to-violet-900\",\n  teal: \"from-teal-300 via-teal-500 to-cyan-700\",\n};\n\nexport interface PaymentCard {\n  issuer: string;\n  type?: string;\n  last4: string;\n  holder: string;\n  expires: string;\n  hue?: CardStackHue;\n  cvc?: string;\n}\n\nexport interface CardStackProps {\n  cards: PaymentCard[];\n  autoPlay?: boolean;\n  className?: string;\n}\n\nconst spring = { type: \"spring\", stiffness: 260, damping: 26 } as const;\nconst flipSpring = { type: \"spring\", stiffness: 180, damping: 22 } as const;\n\nfunction Contactless({ className }: { className?: string }) {\n  return (\n    <svg aria-hidden className={className} fill=\"none\" stroke=\"currentColor\" strokeLinecap=\"round\" strokeWidth={2} viewBox=\"0 0 24 24\">\n      <path d=\"M8.5 8.5a5 5 0 0 1 0 7\" />\n      <path d=\"M12 6a8.5 8.5 0 0 1 0 12\" />\n      <path d=\"M15.5 3.5a12 12 0 0 1 0 17\" />\n    </svg>\n  );\n}\n\nfunction NetworkMark({ className }: { className?: string }) {\n  return (\n    <svg aria-hidden className={className} viewBox=\"0 0 40 24\">\n      <rect fill=\"white\" fillOpacity={0.9} height={14} rx={3.5} transform=\"rotate(45 13 12)\" width={14} x={6} y={5} />\n      <rect fill=\"white\" fillOpacity={0.55} height={14} rx={3.5} transform=\"rotate(45 27 12)\" width={14} x={20} y={5} />\n    </svg>\n  );\n}\n\nfunction Chip() {\n  return (\n    <div className=\"relative h-[15cqw] w-[19cqw] overflow-hidden rounded-[2.6cqw] bg-linear-to-br from-amber-100 via-amber-300 to-amber-500 shadow-[inset_0_0_0_1px_rgb(0_0_0/0.12)]\">\n      <div className=\"absolute inset-y-0 left-1/3 w-px bg-black/20\" />\n      <div className=\"absolute inset-y-0 right-1/3 w-px bg-black/20\" />\n      <div className=\"absolute inset-x-0 top-1/2 h-px bg-black/20\" />\n      <div className=\"absolute top-1/2 left-1/2 h-[45%] w-[30%] -translate-1/2 rounded-[1cqw] border border-black/20\" />\n    </div>\n  );\n}\n\nfunction Front({ card }: { card: PaymentCard }) {\n  return (\n    <div className=\"flex size-full flex-col justify-between p-[6.5cqw] text-white\">\n      <div className=\"flex items-start justify-between\">\n        <div className=\"flex items-baseline gap-[2cqw]\">\n          <span className=\"font-semibold text-[6.4cqw] tracking-tight\">{card.issuer}</span>\n          {card.type && <span className=\"text-[4.2cqw] text-white/70\">{card.type}</span>}\n        </div>\n        <Contactless className=\"size-[8cqw] text-white/80\" />\n      </div>\n      <Chip />\n      <div className=\"flex flex-col gap-[2.5cqw]\">\n        <div className=\"font-mono text-[5.6cqw] tracking-[0.08em] text-white/95 tabular-nums\">\n          <span className=\"text-white/60\">•••• •••• ••••</span> {card.last4}\n        </div>\n        <div className=\"flex items-end justify-between\">\n          <div className=\"flex gap-[6cqw] text-[3.8cqw]\">\n            <span className=\"flex flex-col\">\n              <span className=\"text-[2.8cqw] text-white/60 uppercase tracking-wider\">Card holder</span>\n              <span className=\"font-medium\">{card.holder}</span>\n            </span>\n            <span className=\"flex flex-col\">\n              <span className=\"text-[2.8cqw] text-white/60 uppercase tracking-wider\">Expires</span>\n              <span className=\"font-medium tabular-nums\">{card.expires}</span>\n            </span>\n          </div>\n          <NetworkMark className=\"h-[7.5cqw] w-[12.5cqw]\" />\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction Back({ card }: { card: PaymentCard }) {\n  return (\n    <div className=\"flex size-full flex-col text-white\">\n      <div className=\"mt-[8cqw] h-[13cqw] bg-black/75\" />\n      <div className=\"flex items-center gap-[3cqw] px-[6.5cqw] pt-[5cqw]\">\n        <div className=\"h-[10cqw] flex-1 rounded-[1.5cqw] bg-[repeating-linear-gradient(135deg,rgb(255_255_255/0.95)_0_3px,rgb(255_255_255/0.8)_3px_6px)]\" />\n        <div className=\"flex h-[10cqw] items-center rounded-[1.5cqw] bg-white px-[2.5cqw] font-mono text-[4.4cqw] text-black/80 italic tabular-nums\">\n          {card.cvc ?? \"000\"}\n        </div>\n      </div>\n      <div className=\"mt-auto flex items-end justify-between gap-[4cqw] px-[6.5cqw] pb-[6cqw]\">\n        <p className=\"text-[3.4cqw] text-white/65\">Issued by {card.issuer}</p>\n        <div className=\"size-[11cqw] rounded-[2cqw] bg-linear-to-br from-sky-200 via-violet-200 to-amber-100 opacity-80 shadow-[inset_0_0_0_1px_rgb(255_255_255/0.4)]\" />\n      </div>\n    </div>\n  );\n}\n\nfunction Surface({ hue, children }: { hue: CardStackHue; children: React.ReactNode }) {\n  return (\n    <div\n      className={cn(\n        \"absolute inset-0 overflow-hidden rounded-[5.5cqw] bg-linear-to-br shadow-[inset_0_1px_0_rgb(255_255_255/0.25),inset_0_0_0_1px_rgb(255_255_255/0.08)] [backface-visibility:hidden]\",\n        hues[hue],\n      )}\n    >\n      <div aria-hidden className=\"absolute -top-1/2 -right-1/4 size-[90cqw] rounded-full bg-white/10\" />\n      <div aria-hidden className=\"absolute -bottom-3/4 -left-1/3 size-[90cqw] rounded-full bg-black/10\" />\n      <div className=\"relative size-full\">{children}</div>\n    </div>\n  );\n}\n\nfunction placement(rank: number, count: number, fanned: boolean, lifted: boolean) {\n  if (rank === 0 && lifted) return { x: \"0%\", y: \"-74%\", rotate: 0, scale: 1 };\n  if (!fanned) return { x: \"0%\", y: `${-rank * 23}%`, rotate: 0, scale: 1 - rank * 0.03 };\n  if (rank === 0) return { x: \"0%\", y: \"-46%\", rotate: 0, scale: 1 };\n  const side = count === 2 ? 1 : rank === 1 ? -1 : 1;\n  return { x: `${side * 30}%`, y: \"-38%\", rotate: side * 10, scale: 0.94 };\n}\n\nfunction Wallet() {\n  return (\n    <div\n      aria-hidden\n      className=\"pointer-events-none absolute inset-x-[17%] bottom-[5%] z-10 h-[41%] rounded-[18px] bg-linear-to-b from-stone-600 to-stone-800 shadow-[inset_0_1px_0_rgb(255_255_255/0.18),0_18px_30px_-14px_rgb(0_0_0/0.5)] ring-1 ring-black/10 [mask-image:radial-gradient(circle_at_50%_0,transparent_22px,black_22.5px)] dark:ring-white/10\"\n    >\n      <div className=\"absolute inset-2.5 rounded-[11px] border border-white/25 border-dashed [mask-image:radial-gradient(circle_at_50%_-10px,transparent_29px,black_29.5px)]\" />\n      <div className=\"absolute inset-x-0 top-0 h-1/2 rounded-t-[18px] bg-linear-to-b from-white/10 to-transparent\" />\n    </div>\n  );\n}\n\nfunction useCalmMotion() {\n  const prefers = useReducedMotion();\n  const [mounted, setMounted] = useState(false);\n  useEffect(() => setMounted(true), []);\n  return mounted && !!prefers;\n}\n\nexport function CardStack({ cards, autoPlay = true, className }: CardStackProps) {\n  const reduce = useCalmMotion();\n  const root = useRef<HTMLDivElement>(null);\n  const inView = useInView(root);\n  const list = cards.slice(0, 3);\n  const [order, setOrder] = useState(() => list.map((_, i) => i));\n  const [hovered, setHovered] = useState(false);\n  const [focused, setFocused] = useState(false);\n  const [pinned, setPinned] = useState(false);\n  const [flipped, setFlipped] = useState(false);\n  const [auto, setAuto] = useState({ fan: false, flip: false });\n  const [touched, setTouched] = useState(false);\n\n  const user = hovered || focused || pinned || touched;\n  const fanned = user ? hovered || focused || pinned : auto.fan;\n  const showBack = user ? flipped : auto.flip;\n\n  useEffect(() => {\n    if (!autoPlay || user || reduce || !inView) {\n      setAuto({ fan: false, flip: false });\n      return;\n    }\n    const steps: [number, { fan: boolean; flip: boolean }][] = [\n      [1600, { fan: true, flip: false }],\n      [1100, { fan: true, flip: true }],\n      [2600, { fan: true, flip: false }],\n      [1000, { fan: false, flip: false }],\n    ];\n    let i = 0;\n    let t: ReturnType<typeof setTimeout>;\n    const next = () => {\n      const [ms, state] = steps[i % steps.length];\n      t = setTimeout(() => {\n        setAuto(state);\n        i++;\n        next();\n      }, i % steps.length === 0 && i > 0 ? ms + 2400 : ms);\n    };\n    next();\n    return () => clearTimeout(t);\n  }, [autoPlay, user, reduce, inView]);\n\n  return (\n    <div\n      ref={root}\n      className={cn(\n        \"relative w-full max-w-[460px] overflow-hidden rounded-2xl border bg-card p-4 shadow-xs/5 sm:p-6\",\n        className,\n      )}\n    >\n      <div\n        aria-hidden\n        className=\"dot-grid pointer-events-none absolute inset-0 opacity-50 [mask-image:radial-gradient(closest-side,black,transparent)]\"\n      />\n      <div\n        aria-label=\"Payment cards\"\n        className=\"relative mx-auto aspect-[4/3] w-full max-w-[400px]\"\n        onBlur={(e) => !e.currentTarget.contains(e.relatedTarget) && setFocused(false)}\n        onFocus={(e) => e.target.matches(\":focus-visible\") && setFocused(true)}\n        onPointerEnter={(e) => e.pointerType === \"mouse\" && setHovered(true)}\n        onPointerLeave={(e) => {\n          if (e.pointerType !== \"mouse\") return;\n          setHovered(false);\n          setFlipped(false);\n        }}\n        role=\"group\"\n      >\n        <motion.div\n          animate={{ scaleX: fanned ? 1.5 : 1, opacity: fanned ? 0.7 : 1 }}\n          aria-hidden\n          className=\"absolute bottom-[3%] left-1/2 h-6 w-1/2 -translate-x-1/2 rounded-[50%] bg-foreground/10 blur-xl\"\n          initial={false}\n          transition={spring}\n        />\n        {list.map((card, i) => {\n          const rank = order.indexOf(i);\n          const isFront = rank === 0;\n          const place = placement(rank, list.length, fanned, isFront && showBack);\n          const hue = card.hue ?? ([\"violet\", \"emerald\", \"orange\"] as const)[i];\n          return (\n            <motion.div\n              animate={place}\n              className=\"absolute top-[38%] left-1/2 -ml-[27%] aspect-[1.586] w-[54%] [perspective:1000px]\"\n              initial={false}\n              key={card.last4}\n              style={{ zIndex: list.length - rank, originY: 1 }}\n              transition={reduce ? { duration: 0 } : spring}\n            >\n              <motion.button\n                animate={{ rotateY: isFront && showBack ? 180 : 0 }}\n                aria-label={\n                  isFront\n                    ? `${card.issuer} card ending ${card.last4}: show ${showBack ? \"front\" : \"back\"}`\n                    : `Bring ${card.issuer} card ending ${card.last4} to the front`\n                }\n                className=\"@container relative size-full cursor-pointer rounded-[5.5cqw] text-left shadow-[0_14px_30px_-12px_rgb(0_0_0/0.45),0_2px_6px_-2px_rgb(0_0_0/0.2)] outline-none [transform-style:preserve-3d] focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-card\"\n                initial={false}\n                onClick={() => {\n                  setTouched(true);\n                  if (!fanned && !hovered) {\n                    setPinned(true);\n                    return;\n                  }\n                  if (isFront) setFlipped((f) => !f);\n                  else {\n                    setFlipped(false);\n                    setOrder((o) => [i, ...o.filter((x) => x !== i)]);\n                  }\n                }}\n                transition={reduce ? { duration: 0 } : flipSpring}\n                type=\"button\"\n                whileTap={{ scale: 0.98 }}\n              >\n                <Surface hue={hue}>\n                  <Front card={card} />\n                </Surface>\n                <div className=\"absolute inset-0 [backface-visibility:hidden] [transform:rotateY(180deg)]\">\n                  <Surface hue={hue}>\n                    <Back card={card} />\n                  </Surface>\n                </div>\n              </motion.button>\n            </motion.div>\n          );\n        })}\n        <Wallet />\n      </div>\n    </div>\n  );\n}\n"
    }
  ]
}