{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "eased-gesture",
  "title": "Gesture kit",
  "description": "useDrag: one-axis drag with Apple's rubber-band past the ends, velocity projection onto snap points, scroll handoff from an inner scroller, interruptible release springs that carry the finger's velocity, and keyboard snapping. Plus the pure maths (rubberband, project, pickSnap, velocity tracker).",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://eased.dev/r/eased-tokens.json",
    "https://eased.dev/r/eased-motion.json"
  ],
  "files": [
    {
      "path": "src/registry/eased/_primitives/gesture/gesture-math.ts",
      "content": "/**\n * The maths behind Eased gestures. Pure functions, so sliders, sheets, pagers and canvases\n * all resist, fling and settle the same way.\n */\n\n/** UIScrollView's rubber-band constant: how stiff an overscroll feels. Lower is stiffer. */\nexport const RUBBER_BAND = 0.55;\n\n/** UIScrollView's deceleration rates, per millisecond. `normal` for sheets, `fast` for pagers. */\nexport const DECELERATION = { normal: 0.998, fast: 0.99 } as const;\n\n/**\n * Apple's rubber-band curve. Maps how far a pointer has travelled past an edge to how far the\n * element follows it: almost 1:1 at first, then flattening toward `dimension` and never reaching it.\n *\n *   f(x) = (1 − 1 / (x·c / d + 1)) · d\n */\nexport function rubberband(overshoot: number, dimension: number, constant = RUBBER_BAND) {\n  if (overshoot === 0 || dimension <= 0) return 0;\n  const x = Math.abs(overshoot);\n  return Math.sign(overshoot) * (1 - 1 / ((x * constant) / dimension + 1)) * dimension;\n}\n\n/** The inverse of `rubberband`: the pointer travel that produces a displayed overshoot. */\nexport function rubberbandInverse(displayed: number, dimension: number, constant = RUBBER_BAND) {\n  if (displayed === 0 || dimension <= 0) return 0;\n  const f = Math.min(Math.abs(displayed), dimension * 0.999);\n  return Math.sign(displayed) * ((dimension / constant) * f) / (dimension - f);\n}\n\n/** `value` inside [min, max], rubber-banded past either end instead of clamped. */\nexport function rubberbandClamp(value: number, min: number, max: number, dimension: number, constant = RUBBER_BAND) {\n  if (value < min) return min - rubberband(min - value, dimension, constant);\n  if (value > max) return max + rubberband(value - max, dimension, constant);\n  return value;\n}\n\n/** The inverse of `rubberbandClamp`, used to pick a gesture up from an overshot position. */\nexport function rubberbandUnclamp(value: number, min: number, max: number, dimension: number, constant = RUBBER_BAND) {\n  if (value < min) return min - rubberbandInverse(min - value, dimension, constant);\n  if (value > max) return max + rubberbandInverse(value - max, dimension, constant);\n  return value;\n}\n\nexport const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));\n\n/**\n * Velocity projection: how far a flick would carry before friction stops it, as UIScrollView\n * decelerates. `velocity` is px/s; at `DECELERATION.normal` a 1000px/s flick travels ≈ 500px.\n */\nexport function project(velocity: number, decelerationRate: number = DECELERATION.normal) {\n  return ((velocity / 1000) * decelerationRate) / (1 - decelerationRate);\n}\n\n/** Index of the point nearest `value`. Ties go to the earlier point. */\nexport function nearest(value: number, points: readonly number[]) {\n  let best = 0;\n  for (let i = 1; i < points.length; i++) {\n    if (Math.abs(points[i] - value) < Math.abs(points[best] - value)) best = i;\n  }\n  return best;\n}\n\n/**\n * Where a release settles: the snap point nearest to where the flick would carry the element,\n * not where the finger let go. A slow release lands on the nearest point; a flick skips ahead.\n */\nexport function pickSnap(position: number, velocity: number, points: readonly number[], decelerationRate: number = DECELERATION.normal) {\n  return nearest(position + project(velocity, decelerationRate), points);\n}\n\ntype Sample = { t: number; p: number };\n\n/**\n * Pointer velocity in px/s over the last `window` ms, the way Motion's pan gesture measures it.\n * Returns 0 if the pointer has rested longer than `rest` ms, so a held-then-released drag\n * does not fling.\n */\nexport function createVelocityTracker(window = 100, rest = 60) {\n  let samples: Sample[] = [];\n  return {\n    reset() {\n      samples = [];\n    },\n    add(p: number, t = performance.now()) {\n      samples.push({ t, p });\n      while (samples.length > 2 && t - samples[1].t > window) samples.shift();\n    },\n    velocity(now = performance.now()) {\n      if (samples.length < 2) return 0;\n      const last = samples[samples.length - 1];\n      if (now - last.t > rest) return 0;\n      const first = samples[0];\n      const dt = (last.t - first.t) / 1000;\n      return dt > 0 ? (last.p - first.p) / dt : 0;\n    },\n  };\n}\n",
      "type": "registry:ui",
      "target": "@components/eased/_primitives/gesture/gesture-math.ts"
    },
    {
      "path": "src/registry/eased/_primitives/gesture/use-drag.ts",
      "content": "\"use client\";\n\nimport { animate, useMotionValue, type AnimationPlaybackControls, type MotionValue, type Transition } from \"motion/react\";\nimport { useCallback, useEffect, useLayoutEffect, useRef, useState, type KeyboardEvent as ReactKeyboardEvent } from \"react\";\nimport { isEditable } from \"../../lib/dom\";\nimport { instant, smooth, useSafeReducedMotion } from \"../../lib/motion\";\nimport {\n  DECELERATION,\n  RUBBER_BAND,\n  clamp,\n  createVelocityTracker,\n  nearest,\n  project,\n  rubberbandClamp,\n  rubberbandUnclamp,\n} from \"./gesture-math\";\n\nexport type DragAxis = \"x\" | \"y\";\n\nexport type DragInfo = {\n  /** The displayed position, after rubber-banding. */\n  value: number;\n  /** Pointer travel along the axis since the drag started. */\n  offset: number;\n  /** Pointer velocity along the axis, px/s. */\n  velocity: number;\n  /** Where a release right now would carry the element: `value` plus the velocity projection. */\n  projected: number;\n};\n\nexport type DragReleaseInfo = DragInfo & {\n  /** The snap point the release picked, or -1 without snap points. */\n  index: number;\n  /** The position the release will settle at. */\n  target: number;\n};\n\nexport type UseDragOptions = {\n  /** The axis the element moves on. Default \"y\". The other axis is left to the page. */\n  axis?: DragAxis;\n  /** Drive your own MotionValue instead of the one the hook creates. */\n  value?: MotionValue<number>;\n  /** Positions (px) a release settles on, chosen by velocity projection. */\n  snapPoints?: readonly number[];\n  /** Uncontrolled starting snap point. Default 0. */\n  defaultIndex?: number;\n  /** Controlled snap point. Changing it animates there. */\n  index?: number;\n  /** Called when a release, a key or `snapTo` picks a new snap point. */\n  onIndexChange?: (index: number) => void;\n  /** Hard ends. Default: the lowest and highest snap point, or unbounded without snap points. */\n  min?: number;\n  max?: number;\n  /** Resist past `min`/`max` with Apple's rubber-band (true, default), a custom constant, or clamp (false). */\n  rubberBand?: boolean | number;\n  /** The distance the rubber-band flattens toward. Default: the element's size on the axis. */\n  dimension?: number;\n  /** Friction for velocity projection. Default `DECELERATION.normal` (0.998); pagers want `fast`. */\n  decelerationRate?: number;\n  /** Without snap points, keep gliding after release (Motion inertia) instead of stopping. Default false. */\n  momentum?: boolean;\n  /** Pointer travel (px) before a press becomes a drag, so taps and clicks still work. Default 4. */\n  threshold?: number;\n  /**\n   * Hand scrolling over to the drag, as an iOS sheet does: inside a scroller on the same axis,\n   * the content scrolls until it reaches its start, then the same pull drags the element.\n   * Give the scroller `overscroll-behavior: contain`. Default true.\n   */\n  scrollHandoff?: boolean;\n  /** CSS touch-action the hook sets on the element. Default \"pan-x\" for y, \"pan-y\" for x; false leaves it alone. */\n  touchAction?: string | false;\n  disabled?: boolean;\n  /** How a release settles. Default the smooth spring, carrying the release velocity. */\n  transition?: Transition;\n  onDragStart?: (info: DragInfo) => void;\n  onDrag?: (info: DragInfo) => void;\n  /**\n   * Called on release before the element settles. Return a snap index to go there instead,\n   * or `false` to skip settling and animate the value yourself.\n   */\n  onRelease?: (info: DragReleaseInfo) => number | false | void;\n  onDragEnd?: (info: DragReleaseInfo) => void;\n};\n\nexport type UseDrag = {\n  /** Attach to the draggable element. */\n  ref: (el: HTMLElement | null) => () => void;\n  /** The position. Bind it to `style.x` or `style.y`. */\n  value: MotionValue<number>;\n  /** True while a pointer drags the element. */\n  dragging: boolean;\n  /** The current snap point, or -1 without snap points. */\n  index: number;\n  /** Animate to a snap point. `immediate` jumps (layout changes, reduced motion). */\n  snapTo: (index: number, immediate?: boolean) => void;\n  /**\n   * Put on the element's keyboard handle (a button or slider): arrows step between snap points\n   * in screen order, Home and End jump to the ends.\n   */\n  onKeyDown: (e: ReactKeyboardEvent) => void;\n};\n\ntype Session = {\n  id: number;\n  type: string;\n  startX: number;\n  startY: number;\n  phase: \"pending\" | \"dragging\" | \"ignored\";\n  scroller: HTMLElement | null;\n  /** The release animation this press interrupted, if any. */\n  interrupted: boolean;\n  dragStart: number;\n  origin: number;\n  dimension: number;\n  last: DragInfo;\n};\n\nconst scrollPos = (el: HTMLElement, axis: DragAxis) => (axis === \"y\" ? el.scrollTop : Math.abs(el.scrollLeft));\n\n/** The nearest element between `from` and `root` that scrolls on `axis`. */\nfunction findScroller(from: EventTarget | null, root: HTMLElement, axis: DragAxis) {\n  let el = from instanceof Element ? from : null;\n  while (el && el !== root) {\n    if (el instanceof HTMLElement) {\n      const overflow = getComputedStyle(el)[axis === \"y\" ? \"overflowY\" : \"overflowX\"];\n      const scrolls = axis === \"y\" ? el.scrollHeight > el.clientHeight + 1 : el.scrollWidth > el.clientWidth + 1;\n      if (scrolls && /auto|scroll|overlay/.test(overflow)) return el;\n    }\n    el = el.parentElement;\n  }\n  return null;\n}\n\n/**\n * Eased's drag gesture: one axis, Apple's rubber-band past the ends, velocity projection onto\n * snap points, scroll handoff, and a release that carries the finger's velocity into a spring.\n * Built on Motion values and Motion's spring and inertia animations, so a drag can catch an\n * element mid-flight and any Motion style can read it.\n *\n *   const drag = useDrag({ snapPoints: [0, 240, 400] });\n *   <motion.div ref={drag.ref} style={{ y: drag.value }} />\n */\nexport function useDrag(options: UseDragOptions = {}): UseDrag {\n  const {\n    axis = \"y\",\n    snapPoints,\n    defaultIndex = 0,\n    index: controlledIndex,\n    touchAction,\n  } = options;\n  const reduced = useSafeReducedMotion();\n  const own = useMotionValue(snapPoints?.[controlledIndex ?? defaultIndex] ?? 0);\n  const value = options.value ?? own;\n\n  const [dragging, setDragging] = useState(false);\n  const [index, setIndex] = useState(snapPoints ? (controlledIndex ?? defaultIndex) : -1);\n\n  // Listeners are bound once per element; they read the latest options from here.\n  const opts = useRef(options);\n  const motion = useRef({ reduced, value });\n  // Layout effect, so a press straight after a render already sees the new options.\n  useLayoutEffect(() => {\n    opts.current = options;\n    motion.current = { reduced, value };\n  });\n\n  const current = useRef(index);\n  const anim = useRef<AnimationPlaybackControls | null>(null);\n  const session = useRef<Session | null>(null);\n\n  const bounds = useCallback(() => {\n    const o = opts.current;\n    const pts = o.snapPoints ?? [];\n    const min = o.min ?? (pts.length ? Math.min(...pts) : -Infinity);\n    const max = o.max ?? (pts.length ? Math.max(...pts) : Infinity);\n    return { min, max };\n  }, []);\n\n  const settle = useCallback((target: number, velocity = 0, immediate = false) => {\n    const { reduced, value } = motion.current;\n    anim.current?.stop();\n    if (immediate) {\n      value.jump(target);\n      return;\n    }\n    anim.current = animate(value, target, { ...(reduced ? instant : (opts.current.transition ?? smooth)), velocity });\n  }, []);\n\n  // Set below, once snapTo exists: re-syncs to a controlled index the parent did not accept.\n  const resync = useRef<() => void>(() => {});\n\n  const choose = useCallback((next: number) => {\n    if (next === current.current) return;\n    current.current = next;\n    setIndex(next);\n    opts.current.onIndexChange?.(next);\n    // Controlled: if the parent keeps its old index, go back to it.\n    if (opts.current.index !== undefined) requestAnimationFrame(() => resync.current());\n  }, []);\n\n  const snapTo = useCallback(\n    (next: number, immediate = false) => {\n      const pts = opts.current.snapPoints;\n      if (!pts?.length) return;\n      const i = clamp(Math.round(next), 0, pts.length - 1);\n      choose(i);\n      settle(pts[i], 0, immediate || motion.current.reduced);\n    },\n    [choose, settle],\n  );\n\n  // A controlled index that moves on its own animates there (never under a finger).\n  useEffect(() => {\n    resync.current = () => {\n      const want = opts.current.index;\n      if (want === undefined || want === current.current || session.current?.phase === \"dragging\") return;\n      snapTo(want);\n    };\n  }, [snapTo]);\n  useEffect(() => {\n    resync.current();\n  }, [controlledIndex]);\n\n  // Snap points that move (a resize, a measured container) carry the element with them.\n  const pointsKey = snapPoints?.join(\",\") ?? \"\";\n  useEffect(() => {\n    const pts = opts.current.snapPoints;\n    if (!pts?.length || session.current?.phase === \"dragging\") return;\n    const i = clamp(current.current, 0, pts.length - 1);\n    if (current.current < 0) choose(i);\n    if (anim.current?.state !== \"running\") motion.current.value.jump(pts[i]);\n    else settle(pts[i]);\n  }, [pointsKey, choose, settle]);\n\n  const ref = useCallback(\n    (el: HTMLElement | null) => {\n      if (!el) return () => {};\n      const ax = opts.current.axis ?? \"y\";\n      const action = opts.current.touchAction ?? (ax === \"y\" ? \"pan-x\" : \"pan-y\");\n      if (action !== false) el.style.touchAction = action;\n\n      const tracker = createVelocityTracker();\n      let restoreSelect = \"\";\n\n      const infoFor = (s: Session, pointer: number): DragInfo => {\n        const o = opts.current;\n        const { min, max } = bounds();\n        const raw = s.origin + (pointer - s.dragStart);\n        const band = o.rubberBand ?? true;\n        const shown =\n          band === false ? clamp(raw, min, max) : rubberbandClamp(raw, min, max, s.dimension, band === true ? RUBBER_BAND : band);\n        const velocity = tracker.velocity();\n        return { value: shown, offset: pointer - s.dragStart, velocity, projected: shown + project(velocity, o.decelerationRate ?? DECELERATION.normal) };\n      };\n\n      // The scroller keeps the gesture while it has content to scroll back to, or while the\n      // element is already at its far end; otherwise the pull drags the element.\n      const scrollerWins = (scroller: HTMLElement, dir: number) => {\n        if (scrollPos(scroller, ax) > 1) return true;\n        if (dir > 0) return false;\n        return motion.current.value.get() <= bounds().min + 0.5;\n      };\n\n      const begin = (s: Session, pointer: number) => {\n        const o = opts.current;\n        const { min, max } = bounds();\n        const band = o.rubberBand ?? true;\n        s.phase = \"dragging\";\n        s.dragStart = pointer;\n        s.dimension = o.dimension ?? (ax === \"y\" ? el.offsetHeight : el.offsetWidth);\n        s.origin =\n          band === false ? motion.current.value.get() : rubberbandUnclamp(motion.current.value.get(), min, max, s.dimension, band === true ? RUBBER_BAND : band);\n        anim.current?.stop();\n        tracker.reset();\n        tracker.add(pointer);\n        restoreSelect = document.documentElement.style.userSelect;\n        document.documentElement.style.userSelect = \"none\";\n        window.getSelection()?.removeAllRanges();\n        el.style.willChange = \"transform\";\n        setDragging(true);\n        s.last = infoFor(s, pointer);\n        o.onDragStart?.(s.last);\n      };\n\n      const finish = (s: Session, cancelled: boolean) => {\n        const o = opts.current;\n        const pts = o.snapPoints ?? [];\n        const { min, max } = bounds();\n        document.documentElement.style.userSelect = restoreSelect;\n        el.style.willChange = \"\";\n        setDragging(false);\n\n        // A drag is never also a click on whatever it started on.\n        const swallow = (e: MouseEvent) => {\n          e.stopPropagation();\n          e.preventDefault();\n        };\n        window.addEventListener(\"click\", swallow, { capture: true, once: true });\n        window.setTimeout(() => window.removeEventListener(\"click\", swallow, { capture: true }), 0);\n\n        const info = s.last;\n        const velocity = cancelled ? 0 : info.velocity;\n        const projected = info.value + project(velocity, o.decelerationRate ?? DECELERATION.normal);\n        let target = clamp(info.value, min, max);\n        let i = -1;\n        if (pts.length) {\n          i = nearest(projected, pts);\n          target = pts[i];\n        }\n        const release: DragReleaseInfo = { ...info, velocity, projected, index: i, target };\n        const override = o.onRelease?.(release);\n        if (override === false) {\n          o.onDragEnd?.(release);\n          return;\n        }\n        if (typeof override === \"number\" && pts.length) {\n          release.index = clamp(override, 0, pts.length - 1);\n          release.target = pts[release.index];\n        }\n        if (release.index >= 0) choose(release.index);\n\n        // Out of bounds, the spring pulls back without the outward velocity.\n        const inside = info.value >= min && info.value <= max;\n        if (!pts.length && o.momentum && inside && !motion.current.reduced) {\n          anim.current?.stop();\n          const rate = o.decelerationRate ?? DECELERATION.normal;\n          anim.current = animate(motion.current.value, info.value, {\n            type: \"inertia\",\n            velocity,\n            power: rate / (1 - rate) / 1000,\n            timeConstant: 325,\n            min: Number.isFinite(min) ? min : undefined,\n            max: Number.isFinite(max) ? max : undefined,\n            bounceStiffness: 520,\n            bounceDamping: 38,\n          });\n        } else settle(release.target, inside ? velocity : 0);\n        o.onDragEnd?.(release);\n      };\n\n      const onMove = (e: PointerEvent) => {\n        const s = session.current;\n        if (!s || e.pointerId !== s.id || s.phase === \"ignored\") return;\n        const dx = e.clientX - s.startX;\n        const dy = e.clientY - s.startY;\n        const main = ax === \"y\" ? dy : dx;\n        const cross = ax === \"y\" ? dx : dy;\n        const pointer = ax === \"y\" ? e.clientY : e.clientX;\n\n        if (s.phase === \"pending\") {\n          const threshold = opts.current.threshold ?? 4;\n          // A touch inside a scroller is decided on its first move, before the browser commits\n          // to scrolling; everything else waits for the threshold so taps stay taps.\n          const decideNow = s.scroller && s.type === \"touch\" && (main !== 0 || cross !== 0);\n          if (!decideNow && Math.abs(main) < threshold) {\n            if (Math.abs(cross) >= threshold) s.phase = \"ignored\";\n            return;\n          }\n          if (Math.abs(cross) > Math.abs(main)) {\n            s.phase = \"ignored\";\n            return;\n          }\n          if (s.scroller && scrollerWins(s.scroller, Math.sign(main))) {\n            s.phase = \"ignored\";\n            return;\n          }\n          begin(s, pointer);\n          return;\n        }\n\n        tracker.add(pointer);\n        s.last = infoFor(s, pointer);\n        motion.current.value.set(s.last.value);\n        opts.current.onDrag?.(s.last);\n      };\n\n      const end = (e: PointerEvent) => {\n        const s = session.current;\n        if (!s || e.pointerId !== s.id) return;\n        session.current = null;\n        window.removeEventListener(\"pointermove\", onMove);\n        window.removeEventListener(\"pointerup\", end);\n        window.removeEventListener(\"pointercancel\", end);\n        el.removeEventListener(\"touchmove\", onTouchMove);\n        if (s.phase === \"dragging\") {\n          tracker.add(ax === \"y\" ? e.clientY : e.clientX);\n          if (e.type === \"pointerup\") s.last = { ...s.last, velocity: tracker.velocity() };\n          finish(s, e.type === \"pointercancel\");\n        } else if (s.interrupted) {\n          // A tap that caught the element mid-flight lets it finish where it was going.\n          const pts = opts.current.snapPoints;\n          const v = motion.current.value;\n          if (pts?.length && current.current >= 0) settle(pts[current.current], v.getVelocity());\n          else {\n            const { min, max } = bounds();\n            const inside = clamp(v.get(), min, max);\n            if (inside !== v.get()) settle(inside, v.getVelocity());\n          }\n        }\n      };\n\n      const onDown = (e: PointerEvent) => {\n        const o = opts.current;\n        if (o.disabled || session.current) return;\n        if (e.pointerType === \"mouse\" && e.button !== 0) return;\n        if (isEditable(e.target) || (e.target instanceof Element && e.target.closest(\"[data-drag-ignore]\"))) return;\n        const interrupted = anim.current?.state === \"running\";\n        if (interrupted) anim.current?.stop();\n        session.current = {\n          id: e.pointerId,\n          type: e.pointerType,\n          startX: e.clientX,\n          startY: e.clientY,\n          phase: \"pending\",\n          scroller: (o.scrollHandoff ?? true) ? findScroller(e.target, el, ax) : null,\n          interrupted,\n          dragStart: 0,\n          origin: 0,\n          dimension: 1,\n          last: { value: motion.current.value.get(), offset: 0, velocity: 0, projected: motion.current.value.get() },\n        };\n        window.addEventListener(\"pointermove\", onMove);\n        window.addEventListener(\"pointerup\", end);\n        window.addEventListener(\"pointercancel\", end);\n        // Non-passive only while a finger is down (pointerdown fires before touchstart, so it is\n        // in place for this gesture); an idle element keeps compositor-threaded scrolling.\n        if (e.pointerType === \"touch\") el.addEventListener(\"touchmove\", onTouchMove, { passive: false });\n      };\n\n      // Touch scrolling is only cancellable from touchmove: once a pull is claimed as a drag,\n      // the scroller under it must not also scroll or overscroll.\n      const onTouchMove = (e: TouchEvent) => {\n        if (session.current?.phase === \"dragging\" && e.cancelable) e.preventDefault();\n      };\n\n      el.addEventListener(\"pointerdown\", onDown);\n      return () => {\n        el.removeEventListener(\"pointerdown\", onDown);\n        el.removeEventListener(\"touchmove\", onTouchMove);\n        window.removeEventListener(\"pointermove\", onMove);\n        window.removeEventListener(\"pointerup\", end);\n        window.removeEventListener(\"pointercancel\", end);\n        // Torn down mid-drag (unmount, or a new axis): give the page its selection back and\n        // put the element back on a resting point.\n        if (session.current?.phase === \"dragging\") {\n          document.documentElement.style.userSelect = restoreSelect;\n          el.style.willChange = \"\";\n          setDragging(false);\n          const pts = opts.current.snapPoints;\n          const { min, max } = bounds();\n          const v = motion.current.value;\n          settle(pts?.length && current.current >= 0 ? pts[current.current] : clamp(v.get(), min, max), 0, true);\n        }\n        session.current = null;\n      };\n    },\n    // Re-bind only when the axis or touch-action changes; everything else is read live.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n    [axis, touchAction === false ? \"off\" : (touchAction ?? \"\"), bounds, choose, settle],\n  );\n\n  useEffect(() => () => anim.current?.stop(), []);\n\n  const onKeyDown = useCallback(\n    (e: ReactKeyboardEvent) => {\n      const pts = opts.current.snapPoints;\n      if (!pts?.length) return;\n      const ax = opts.current.axis ?? \"y\";\n      const back = ax === \"y\" ? \"ArrowUp\" : \"ArrowLeft\";\n      const forward = ax === \"y\" ? \"ArrowDown\" : \"ArrowRight\";\n      // Screen order, so \"up\" always means toward the top whatever order the points are in.\n      const order = pts.map((_, i) => i).sort((a, b) => pts[a] - pts[b]);\n      const at = Math.max(0, order.indexOf(current.current));\n      const to =\n        e.key === back ? order[Math.max(0, at - 1)]\n        : e.key === forward ? order[Math.min(order.length - 1, at + 1)]\n        : e.key === \"Home\" ? order[0]\n        : e.key === \"End\" ? order[order.length - 1]\n        : undefined;\n      if (to === undefined) return;\n      e.preventDefault();\n      snapTo(to);\n    },\n    [snapTo],\n  );\n\n  return { ref, value, dragging, index, snapTo, onKeyDown };\n}\n",
      "type": "registry:ui",
      "target": "@components/eased/_primitives/gesture/use-drag.ts"
    }
  ],
  "type": "registry:ui"
}