{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "use-flick",
  "title": "useFlick",
  "description": "Drag, throw and spring-to-nearest for lists that land on an item.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://spotcheckui.com/r/use-reduced-motion.json"
  ],
  "files": [
    {
      "path": "registry/spotcheck/hooks/use-flick.ts",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { animate, useMotionValue } from \"motion/react\"\n\nimport { useReducedMotion } from \"@/registry/spotcheck/hooks/use-reduced-motion\"\n\nconst SPRING = { stiffness: 240, damping: 26 }\nconst MAX_VELOCITY = 12\nconst PROJECTION = 0.1\nconst RUBBER_BAND = 0.3\nconst TAP_DISTANCE = 5\n\n/** Past either end the drag gives way, so the edge feels soft rather than a hard stop. */\nexport function rubberBand(position: number, count: number) {\n  const last = count - 1\n  if (position < 0) return position * RUBBER_BAND\n  if (position > last) return last + (position - last) * RUBBER_BAND\n  return position\n}\n\n/** Caps the throw, carries it forward, and rounds to the nearest item that exists. */\nexport function flickTarget(position: number, velocity: number, count: number) {\n  const capped = Math.max(-MAX_VELOCITY, Math.min(MAX_VELOCITY, velocity))\n  return Math.max(0, Math.min(count - 1, Math.round(position + capped * PROJECTION)))\n}\n\ntype FlickOptions = {\n  /** How many items there are. */\n  count: number\n  /** The selected index. The position follows it when it changes from outside. */\n  index: number\n  /** Runs when a move lands on a different item. */\n  onIndexChange: (index: number) => void\n  /** Which way drags count. */\n  axis: \"x\" | \"y\"\n  /** Pixels of drag that move one item. A function can measure the element when it's called. */\n  step: number | (() => number)\n  /** Runs for a press that didn't move, with the event and the position when it began. */\n  onTap?: (event: React.PointerEvent<HTMLElement>, position: number) => void\n}\n\ntype Gesture = { start: number; from: number; last: number; time: number; velocity: number; moved: number }\n\n/** Drag, throw and spring-to-nearest for lists that land on an item. Position is in items. */\nexport function useFlick({ count, index, onIndexChange, axis, step, onTap }: FlickOptions) {\n  const reduced = useReducedMotion()\n  const position = useMotionValue(index)\n  const gesture = React.useRef<Gesture | null>(null)\n  const motion = React.useRef<ReturnType<typeof animate> | null>(null)\n  const goal = React.useRef(index)\n\n  const moveTo = React.useCallback(\n    (target: number, velocity = 0) => {\n      goal.current = target\n      motion.current?.stop()\n      if (reduced) position.jump(target)\n      else motion.current = animate(position, target, { type: \"spring\", ...SPRING, velocity })\n    },\n    [position, reduced],\n  )\n\n  const settle = React.useCallback(\n    (target: number, velocity = 0) => {\n      const next = Math.max(0, Math.min(count - 1, target))\n      moveTo(next, velocity)\n      if (next !== index) onIndexChange(next)\n    },\n    [count, index, moveTo, onIndexChange],\n  )\n\n  // Follows a selection that changes from outside the gesture.\n  React.useEffect(() => {\n    if (!gesture.current && goal.current !== index) moveTo(index)\n  }, [index, moveTo])\n\n  const stepSize = () => (typeof step === \"function\" ? step() : step)\n  const along = (event: React.PointerEvent<HTMLElement>) => (axis === \"x\" ? event.clientX : event.clientY)\n\n  const bind = {\n    onPointerDown: (event: React.PointerEvent<HTMLElement>) => {\n      if (event.button !== 0) return\n      event.currentTarget.setPointerCapture(event.pointerId)\n      motion.current?.stop()\n      const at = along(event)\n      gesture.current = { start: at, from: position.get(), last: at, time: performance.now(), velocity: 0, moved: 0 }\n    },\n    onPointerMove: (event: React.PointerEvent<HTMLElement>) => {\n      const state = gesture.current\n      if (!state) return\n      const at = along(event)\n      const now = performance.now()\n      const size = stepSize()\n      state.moved = Math.max(state.moved, Math.abs(at - state.start))\n      state.velocity = (((state.last - at) / size) / (Math.max(1, now - state.time) / 1000)) * 0.6 + state.velocity * 0.4\n      state.last = at\n      state.time = now\n      position.jump(rubberBand(state.from + (state.start - at) / size, count))\n    },\n    onPointerUp: (event: React.PointerEvent<HTMLElement>) => {\n      const state = gesture.current\n      if (!state) return\n      gesture.current = null\n      if (state.moved < TAP_DISTANCE) {\n        moveTo(index)\n        onTap?.(event, state.from)\n        return\n      }\n      settle(flickTarget(position.get(), state.velocity, count), state.velocity)\n    },\n    onPointerCancel: () => {\n      if (!gesture.current) return\n      gesture.current = null\n      moveTo(index)\n    },\n  }\n\n  return { position, settle, bind }\n}\n",
      "type": "registry:hook"
    }
  ],
  "type": "registry:hook"
}