{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"swipe-stack","type":"registry:ui","title":"Swipe Stack","description":"A deck of cards you drag away to reveal the next one. Keyboard accessible.","dependencies":[],"registryDependencies":[],"files":[{"path":"registry/ui/swipe-stack.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useRef, useState, type KeyboardEvent, type PointerEvent, type ReactNode } from \"react\";\n\nexport interface SwipeStackProps {\n  cards: ReactNode[];\n  /** Px of drag needed to send the top card to the back. */\n  sensitivity?: number;\n  /** Px between stacked cards. */\n  offset?: number;\n  className?: string;\n}\n\nexport function SwipeStack({ cards, sensitivity = 120, offset = 10, className }: SwipeStackProps) {\n  const [order, setOrder] = useState(() => cards.map((_, i) => i));\n  const drag = useRef<{ x: number; y: number } | null>(null);\n\n  const next = () => setOrder((o) => [...o.slice(1), o[0]]);\n\n  const onDown = (e: PointerEvent<HTMLDivElement>) => {\n    e.currentTarget.setPointerCapture(e.pointerId);\n    drag.current = { x: e.clientX, y: e.clientY };\n    e.currentTarget.style.transition = \"none\";\n  };\n\n  const onMove = (e: PointerEvent<HTMLDivElement>) => {\n    if (!drag.current) return;\n    const dx = e.clientX - drag.current.x;\n    const dy = e.clientY - drag.current.y;\n    e.currentTarget.style.transform = `translate(${dx}px, ${dy}px) rotate(${dx / 12}deg)`;\n  };\n\n  const onUp = (e: PointerEvent<HTMLDivElement>) => {\n    if (!drag.current) return;\n    const dx = e.clientX - drag.current.x;\n    const dy = e.clientY - drag.current.y;\n    drag.current = null;\n    const el = e.currentTarget;\n    el.style.transition = \"\";\n    el.style.transform = \"\";\n    if (Math.hypot(dx, dy) > sensitivity) next();\n  };\n\n  const onKey = (e: KeyboardEvent<HTMLDivElement>) => {\n    if (e.key === \"Enter\" || e.key === \" \" || e.key === \"ArrowRight\") {\n      e.preventDefault();\n      next();\n    }\n  };\n\n  // Cards added or removed by the parent: fall back to natural order.\n  const safeOrder = order.length === cards.length ? order : cards.map((_, i) => i);\n\n  return (\n    <div className={`relative ${className ?? \"\"}`} style={{ display: \"grid\" }}>\n      {safeOrder.map((cardIndex, depth) => {\n        const isTop = depth === 0;\n        return (\n          <div\n            key={cardIndex}\n            role={isTop ? \"button\" : undefined}\n            tabIndex={isTop ? 0 : -1}\n            aria-label={isTop ? \"Swipe or press Enter for next card\" : undefined}\n            aria-hidden={!isTop || undefined}\n            onPointerDown={isTop ? onDown : undefined}\n            onPointerMove={isTop ? onMove : undefined}\n            onPointerUp={isTop ? onUp : undefined}\n            onPointerCancel={isTop ? onUp : undefined}\n            onKeyDown={isTop ? onKey : undefined}\n            style={{\n              gridArea: \"1 / 1\",\n              zIndex: cards.length - depth,\n              transform: `translateY(${depth * offset}px) scale(${1 - depth * 0.05})`,\n              transformOrigin: \"50% 100%\",\n              opacity: depth > 3 ? 0 : 1,\n              transition: \"transform .35s cubic-bezier(.2,.7,.2,1), opacity .35s\",\n              cursor: isTop ? \"grab\" : \"default\",\n              touchAction: \"none\",\n              userSelect: \"none\",\n            }}\n          >\n            {cards[cardIndex]}\n          </div>\n        );\n      })}\n    </div>\n  );\n}\n"}]}