{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "draggable-card",
  "title": "Draggable Card",
  "description": "Scattered photo cards you can fling around with inertia, tilt and bounds.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/velora/draggable-card.tsx",
      "content": "\"use client\";\n\nimport {\n  Children,\n  createContext,\n  useContext,\n  useId,\n  useRef,\n  useState,\n  useSyncExternalStore,\n} from \"react\";\nimport {\n  motion,\n  useMotionValue,\n  useReducedMotion,\n  useSpring,\n  useTransform,\n  useVelocity,\n} from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst noopSubscribe = () => () => {};\n\ninterface Board {\n  ref: React.RefObject<HTMLDivElement | null>;\n  hint: string;\n  count: number;\n  top: () => number;\n}\n\nconst BoardContext = createContext<Board | null>(null);\nconst IndexContext = createContext(0);\n\n// Integer hash → [0, 1). Same result on server and client (no Math.random).\nconst seeded = (i: number, salt: number) =>\n  ((Math.imul(i + 1, 2654435761) ^ Math.imul(salt, 40503)) >>> 0) % 1000 / 1000;\n\ninterface DraggableCardContainerProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** `DraggableCard` elements */\n  children: React.ReactNode;\n}\n\n/**\n * The board that scatters its `DraggableCard`s and keeps them inside its\n * bounds. Give it a height (default h-96).\n */\nexport function DraggableCardContainer({\n  children,\n  className,\n  ...props\n}: DraggableCardContainerProps) {\n  const ref = useRef<HTMLDivElement>(null);\n  const hint = useId();\n  const z = useRef(Children.count(children));\n  const board: Board = { ref, hint, count: Children.count(children), top: () => ++z.current };\n\n  return (\n    <div\n      {...props}\n      ref={ref}\n      data-slot=\"draggable-card-container\"\n      className={cn(\"relative h-96 w-full\", className)}\n    >\n      <p id={hint} hidden>\n        Arrow keys move the card; Enter or Space brings it to the front.\n      </p>\n      <BoardContext value={board}>\n        {Children.map(children, (child, i) => (\n          <IndexContext value={i}>{child}</IndexContext>\n        ))}\n      </BoardContext>\n    </div>\n  );\n}\n\ninterface DraggableCardProps\n  extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onDrag\" | \"onDragStart\" | \"onDragEnd\" | \"onAnimationStart\"> {\n  /** Accessible name for the card, e.g. its caption */\n  label?: string;\n  /** Resting tilt in degrees (defaults to a seeded ±7° per card) */\n  rotate?: number;\n}\n\n/**\n * A photo-style card for `DraggableCardContainer`: drag or fling it (it\n * tilts with the motion and springs back inside the board), nudge it with\n * arrow keys, and press Enter or Space to bring it to the front.\n */\nexport function DraggableCard({\n  label,\n  rotate,\n  className,\n  style,\n  children,\n  ...props\n}: DraggableCardProps) {\n  const ref = useRef<HTMLDivElement>(null);\n  const board = useContext(BoardContext);\n  const index = useContext(IndexContext);\n  const count = board?.count ?? 1;\n  const hydrated = useSyncExternalStore(noopSubscribe, () => true, () => false);\n  const reduced = !!useReducedMotion() && hydrated;\n  const [z, setZ] = useState(index + 1);\n  const x = useMotionValue(0);\n  const y = useMotionValue(0);\n  const base = rotate ?? Math.round((seeded(index, 1) - 0.5) * 14);\n  // Lean toward the direction of travel, springing upright when released\n  const lean = useSpring(useTransform(useVelocity(x), [-2000, 2000], [-12, 12], { clamp: true }), {\n    stiffness: 300,\n    damping: 30,\n  });\n  const tilt = useTransform(lean, (v) => base + v);\n  // Resting spot as a % of the board, in a loose zigzag. The card is also\n  // translated by -p% of its own size, so it always starts fully inside.\n  const place = (p: number) => Math.round(Math.min(Math.max(p, 0), 100) * 100) / 100;\n  const px = place(((index + 0.5) / count) * 100 + (seeded(index, 2) - 0.5) * 8);\n  const py = place(50 + (index % 2 ? 30 : -30) + (seeded(index, 3) - 0.5) * 16);\n\n  const front = () => board && setZ(board.top());\n\n  // Keyboard nudge, clamped to the board. Offsets come from layout (offset*)\n  // plus the motion values, so rapid key repeats never read a stale frame.\n  const nudge = (dx: number, dy: number) => {\n    const card = ref.current;\n    const box = board?.ref.current;\n    if (!card || !box) return;\n    const move = (mv: typeof x, d: number, start: number, size: number, room: number, p: number) => {\n      const from = start - (size * p) / 100 + mv.get();\n      mv.set(mv.get() + Math.min(Math.max(d, Math.min(0, -from)), Math.max(0, room - size - from)));\n    };\n    move(x, dx, card.offsetLeft, card.offsetWidth, box.clientWidth, px);\n    move(y, dy, card.offsetTop, card.offsetHeight, box.clientHeight, py);\n  };\n\n  return (\n    <motion.div\n      {...props}\n      ref={ref}\n      tabIndex={0}\n      role=\"group\"\n      aria-roledescription=\"draggable card\"\n      aria-label={label}\n      aria-describedby={board?.hint}\n      data-slot=\"draggable-card\"\n      drag\n      dragConstraints={board?.ref}\n      dragElastic={reduced ? 0 : 0.25}\n      dragMomentum={!reduced}\n      dragTransition={{ bounceStiffness: 260, bounceDamping: 22 }}\n      whileDrag={reduced ? undefined : { scale: 1.04 }}\n      onPointerDown={(e) => {\n        front();\n        props.onPointerDown?.(e);\n      }}\n      onKeyDown={(e) => {\n        props.onKeyDown?.(e);\n        const step = e.shiftKey ? 48 : 12;\n        const moves: Record<string, [number, number]> = {\n          ArrowLeft: [-step, 0],\n          ArrowRight: [step, 0],\n          ArrowUp: [0, -step],\n          ArrowDown: [0, step],\n        };\n        if (moves[e.key]) nudge(...moves[e.key]);\n        else if (e.key === \"Enter\" || e.key === \" \") front();\n        else return;\n        e.preventDefault();\n      }}\n      style={{\n        x,\n        y,\n        rotate: reduced ? base : tilt,\n        zIndex: z,\n        left: `${px}%`,\n        top: `${py}%`,\n        translate: `-${px}% -${py}%`,\n        ...style,\n      }}\n      className={cn(\n        \"absolute w-48 cursor-grab touch-none rounded-lg border bg-card p-2.5 pb-4 text-card-foreground shadow-xl select-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background active:cursor-grabbing sm:w-56\",\n        className\n      )}\n    >\n      {children}\n    </motion.div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/velora/draggable-card.tsx"
    }
  ],
  "type": "registry:ui"
}