{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "card-pile",
  "type": "registry:component",
  "title": "Card Pile",
  "description": "Notifications stacked like a small deck of cards: flick the top one away, or press Delete, and the next one steps forward.",
  "categories": [
    "component",
    "Status"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://coss.com/ui/r/button.json"
  ],
  "files": [
    {
      "path": "registry/devl/card-pile.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport {\n  AlertTriangleIcon,\n  BellIcon,\n  CheckCheckIcon,\n  CheckCircle2Icon,\n  InfoIcon,\n  RotateCcwIcon,\n  XIcon,\n  type LucideIcon,\n} from \"lucide-react\";\nimport { AnimatePresence, motion, type PanInfo, useReducedMotion } from \"motion/react\";\nimport { type KeyboardEvent, useRef, useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\nexport type NoticeTone = \"info\" | \"success\" | \"warning\" | \"destructive\";\n\nexport interface Notice {\n  id: string;\n  title: string;\n  body?: string;\n  time?: string;\n  tone?: NoticeTone;\n  action?: string;\n}\n\nexport interface CardPileProps {\n  notices: Notice[];\n  visible?: number;\n  onDismiss?: (notice: Notice) => void;\n  onAction?: (notice: Notice) => void;\n  className?: string;\n}\n\nconst toneStyle: Record<NoticeTone, { icon: LucideIcon; chip: string }> = {\n  info: { icon: InfoIcon, chip: \"bg-info/12 text-info-foreground\" },\n  success: { icon: CheckCircle2Icon, chip: \"bg-success/12 text-success-foreground\" },\n  warning: { icon: AlertTriangleIcon, chip: \"bg-warning/14 text-warning-foreground\" },\n  destructive: { icon: AlertTriangleIcon, chip: \"bg-destructive/10 text-destructive-foreground\" },\n};\n\nconst SWIPE = 90;\nconst press = \"transition-[scale,background-color,box-shadow] duration-150 ease-out active:scale-[0.97]\";\n\nexport function CardPile({ notices, visible = 3, onDismiss, onAction, className }: CardPileProps) {\n  const reduce = useReducedMotion();\n  const [queue, setQueue] = useState(notices);\n  const [exitDir, setExitDir] = useState<1 | -1>(1);\n  // Exiting cards keep stale closures; always act on the live queue.\n  const queueRef = useRef(queue);\n  queueRef.current = queue;\n  const stackRef = useRef<HTMLDivElement>(null);\n\n  const dismiss = (dir: 1 | -1 = 1) => {\n    const [front, next] = queueRef.current;\n    if (!front) return;\n    const hadFocus = stackRef.current?.contains(document.activeElement) ?? false;\n    queueRef.current = queueRef.current.slice(1);\n    setExitDir(dir);\n    setQueue((q) => q.filter((n) => n.id !== front.id));\n    onDismiss?.(front);\n    if (hadFocus && next) {\n      requestAnimationFrame(() => stackRef.current?.querySelector<HTMLElement>(`[data-notice=\"${next.id}\"]`)?.focus());\n    }\n  };\n\n  const onDragEnd = (_: unknown, info: PanInfo) => {\n    if (Math.abs(info.offset.x) > SWIPE || Math.abs(info.velocity.x) > 600) dismiss(info.offset.x > 0 ? 1 : -1);\n  };\n\n  const onKeyDown = (e: KeyboardEvent) => {\n    if (e.key === \"Delete\" || e.key === \"Backspace\" || e.key === \"ArrowRight\") {\n      e.preventDefault();\n      dismiss(1);\n    } else if (e.key === \"ArrowLeft\") {\n      e.preventDefault();\n      dismiss(-1);\n    }\n  };\n\n  const stack = queue.slice(0, visible + 1);\n\n  return (\n    <section aria-label=\"Notifications\" className={cn(\"w-full max-w-[380px]\", className)}>\n      <div className=\"mb-3 flex items-center justify-between px-1\">\n        <h2 className=\"flex items-center gap-2 font-medium text-sm\">\n          <BellIcon className=\"size-4 text-muted-foreground\" />\n          Notifications\n        </h2>\n        <span aria-live=\"polite\" className=\"text-muted-foreground text-xs tabular-nums\">\n          {queue.length ? `${queue.length} unread` : \"All read\"}\n        </span>\n      </div>\n\n      <div className=\"relative h-[156px]\" ref={stackRef} style={{ perspective: 800 }}>\n        <AnimatePresence custom={exitDir} initial={false}>\n          {stack\n            .map((n, i) => {\n              const front = i === 0;\n              const hidden = i >= visible;\n              const tone = toneStyle[n.tone ?? \"info\"];\n              const Icon = tone.icon;\n              return (\n                <motion.article\n                  animate={{\n                    y: hidden ? -36 : -i * 12,\n                    scale: 1 - i * 0.05,\n                    opacity: hidden ? 0 : 1,\n                    x: 0,\n                    rotate: 0,\n                  }}\n                  aria-label={front ? `${n.title}. Press Delete or swipe to dismiss.` : undefined}\n                  aria-hidden={!front}\n                  className={cn(\n                    \"absolute inset-x-0 bottom-0 flex h-[132px] origin-top select-none flex-col rounded-2xl border bg-card p-4 text-card-foreground shadow-xs/5 outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n                    front ? \"cursor-grab active:cursor-grabbing\" : \"pointer-events-none\",\n                  )}\n                  custom={exitDir}\n                  data-notice={n.id}\n                  drag={front ? \"x\" : false}\n                  dragElastic={0.6}\n                  dragSnapToOrigin\n                  exit=\"exit\"\n                  variants={{\n                    exit: (dir: 1 | -1) =>\n                      reduce\n                        ? { opacity: 0, zIndex: 60, transition: { duration: 0.15 } }\n                        : {\n                            zIndex: 60,\n                            x: dir * 360,\n                            rotate: dir * 14,\n                            opacity: 0,\n                            pointerEvents: \"none\",\n                            transition: { type: \"spring\", stiffness: 420, damping: 40 },\n                          },\n                  }}\n                  initial={{ y: -48, scale: 1 - (visible + 1) * 0.05, opacity: 0 }}\n                  key={n.id}\n                  onDragEnd={front ? onDragEnd : undefined}\n                  onKeyDown={front ? onKeyDown : undefined}\n                  role=\"group\"\n                  style={{ zIndex: 50 - i }}\n                  tabIndex={front ? 0 : -1}\n                  transition={\n                    reduce ? { duration: 0 } : { type: \"spring\", stiffness: 420, damping: 34 }\n                  }\n                  whileDrag={{ rotate: 0, scale: 1.02 }}\n                >\n                  <div className={cn(\"flex gap-3 transition-opacity duration-200\", i > 1 && \"opacity-0\")}>\n                    <span className={cn(\"flex size-8 shrink-0 items-center justify-center rounded-full\", tone.chip)}>\n                      <Icon className=\"size-4\" />\n                    </span>\n                    <div className=\"min-w-0 flex-1\">\n                      <div className=\"flex items-baseline gap-2\">\n                        <h3 className=\"truncate font-medium text-sm\">{n.title}</h3>\n                        {n.time && <span className=\"ms-auto shrink-0 text-muted-foreground text-xs\">{n.time}</span>}\n                      </div>\n                      {n.body && <p className=\"mt-0.5 line-clamp-2 text-muted-foreground text-sm\">{n.body}</p>}\n                    </div>\n                  </div>\n                  <div\n                    className={cn(\n                      \"mt-auto flex items-center gap-2 pt-2 transition-opacity duration-200\",\n                      i > 1 && \"opacity-0\",\n                    )}\n                  >\n                    <span className=\"text-muted-foreground text-xs\">Swipe to dismiss</span>\n                    <div className=\"ms-auto flex gap-1.5\">\n                      {n.action && (\n                        <Button\n                          onClick={() => {\n                            onAction?.(n);\n                            dismiss(1);\n                          }}\n                          onPointerDown={(e) => e.stopPropagation()}\n                          className={press}\n                          size=\"xs\"\n                          tabIndex={front ? 0 : -1}\n                          variant=\"outline\"\n                        >\n                          {n.action}\n                        </Button>\n                      )}\n                      <Button\n                        aria-label={`Dismiss ${n.title}`}\n                        className={cn(press, \"after:absolute after:-inset-1\")}\n                        onClick={() => dismiss(1)}\n                        onPointerDown={(e) => e.stopPropagation()}\n                        size=\"icon-xs\"\n                        tabIndex={front ? 0 : -1}\n                        variant=\"ghost\"\n                      >\n                        <XIcon />\n                      </Button>\n                    </div>\n                  </div>\n                </motion.article>\n              );\n            })\n            .reverse()}\n        </AnimatePresence>\n\n        <AnimatePresence>\n          {queue.length === 0 && (\n            <motion.div\n              animate={{ opacity: 1, scale: 1, y: 0 }}\n              className=\"absolute inset-x-0 bottom-0 flex h-[132px] flex-col items-center justify-center gap-1.5 rounded-2xl border border-dashed bg-muted/40 text-center\"\n              exit={{ opacity: 0, scale: 0.96 }}\n              initial={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.96, y: 8 }}\n              transition={{ type: \"spring\", stiffness: 380, damping: 32, delay: reduce ? 0 : 0.12 }}\n            >\n              <span className=\"flex size-7 items-center justify-center rounded-full bg-success/12 text-success-foreground\">\n                <CheckCheckIcon className=\"size-3.5\" />\n              </span>\n              <p className=\"font-medium text-sm\">You're all caught up</p>\n              <Button className={press} onClick={() => setQueue(notices)} size=\"xs\" variant=\"ghost\">\n                <RotateCcwIcon />\n                Bring them back\n              </Button>\n            </motion.div>\n          )}\n        </AnimatePresence>\n      </div>\n    </section>\n  );\n}\n"
    }
  ]
}