{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "eased-inertia-core",
  "title": "Inertia core",
  "description": "useInertia: hand a MotionValue the release velocity and it coasts with UIScrollView friction onto the snap point the flick would reach (points, a step or a function), inside bounds, as a spring or iOS-style decay. Plus the pure inertiaTarget and resolveSnap.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://eased.dev/r/eased-tokens.json",
    "https://eased.dev/r/eased-motion.json",
    "https://eased.dev/r/eased-gesture.json"
  ],
  "files": [
    {
      "path": "src/registry/eased/_primitives/inertia-core/use-inertia.ts",
      "content": "\"use client\";\n\nimport { animate, type AnimationPlaybackControls, type MotionValue, type Transition } from \"motion/react\";\nimport { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from \"react\";\nimport { useSafeReducedMotion } from \"../../lib/motion\";\nimport { DECELERATION, clamp, nearest, project } from \"../gesture/gesture-math\";\n\n/**\n * Where a coast is allowed to stop: a list of points, a step (every multiple of it, from\n * `min` or 0), or a function that rounds a resting position.\n */\nexport type InertiaSnap = readonly number[] | number | ((resting: number) => number);\n\nexport type InertiaOptions = {\n  /** Resting points. Unset: the coast stops wherever friction leaves it. */\n  snap?: InertiaSnap;\n  /** Hard ends. The coast never rests past them. */\n  min?: number;\n  max?: number;\n  /** UIScrollView friction per ms. Default `DECELERATION.normal` (0.998); wheels and pagers want `fast`. */\n  decelerationRate?: number;\n  /**\n   * Below this release speed (px/s) a flick does not coast: the value rests where it was let go,\n   * unless it is within `magnet` of a snap point. Default 0 (every release coasts).\n   */\n  minVelocity?: number;\n  /** A slow release this close (px) to a snap point is pulled onto it. Default 0. */\n  magnet?: number;\n  /**\n   * How the value travels to its resting point. `\"spring\"` (default) rides `transition`\n   * carrying the release velocity; `\"decay\"` is iOS scroll friction that eases onto the point.\n   */\n  mode?: \"spring\" | \"decay\";\n  /** The spring for `\"spring\"` mode. Default a smooth spring that carries velocity. */\n  transition?: Transition;\n  /** Called once the value has come to rest, with where it rests. */\n  onRest?: (value: number) => void;\n};\n\nconst coast = { type: \"spring\", visualDuration: 0.45, bounce: 0.08 } as const;\n\n/** Rounds `value` onto `snap`, inside `[min, max]`. */\nexport function resolveSnap(value: number, snap: InertiaSnap | undefined, min = -Infinity, max = Infinity) {\n  const inside = clamp(value, min, max);\n  if (snap === undefined) return inside;\n  if (typeof snap === \"function\") return clamp(snap(inside), min, max);\n  if (typeof snap === \"number\") {\n    if (snap <= 0) return inside;\n    const origin = Number.isFinite(min) ? min : 0;\n    return clamp(origin + Math.round((inside - origin) / snap) * snap, min, max);\n  }\n  if (!snap.length) return inside;\n  return clamp(snap[nearest(inside, snap)], min, max);\n}\n\n/**\n * Where a release at `position` moving at `velocity` (px/s) comes to rest: the snap point\n * nearest to where friction would carry it, not the one nearest the finger. Pure, so a\n * component can show the destination (a label, a tick) before the value gets there.\n */\nexport function inertiaTarget(position: number, velocity: number, options: InertiaOptions = {}) {\n  const { snap, min = -Infinity, max = Infinity, decelerationRate = DECELERATION.normal, minVelocity = 0, magnet = 0 } = options;\n  if (Math.abs(velocity) < minVelocity) {\n    const resting = clamp(position, min, max);\n    if (snap === undefined || magnet <= 0) return resting;\n    const snapped = resolveSnap(resting, snap, min, max);\n    return Math.abs(snapped - resting) <= magnet ? snapped : resting;\n  }\n  return resolveSnap(position + project(velocity, decelerationRate), snap, min, max);\n}\n\nexport type UseInertia = {\n  /**\n   * Release the value with a velocity (px/s): it coasts and rests on a snap point inside the\n   * bounds. Returns the resting position. Per-call options override the hook's.\n   */\n  release: (velocity: number, options?: InertiaOptions) => number;\n  /** Animate to a position, carrying an optional velocity (keyboard steps, clicks, controlled values). */\n  to: (target: number, options?: { velocity?: number; transition?: Transition; immediate?: boolean }) => void;\n  /** Stop wherever the value is, e.g. when a finger catches it mid-coast. */\n  stop: () => void;\n  /** True while a coast or `to` animation runs. */\n  isAnimating: () => boolean;\n};\n\n/**\n * Momentum for a MotionValue that something else drags: hand it the release velocity and it\n * coasts, with UIScrollView friction, to the snap point the flick would reach. Pairs with\n * `useDrag({ onRelease: () => false })` from the gesture kit. Under reduced motion every\n * release and `to` jumps straight to its resting point.\n *\n *   const inertia = useInertia(x, { snap: [0, 160, 320], min: 0, max: 320 });\n *   useDrag({ value: x, onRelease: ({ value, velocity }) => (inertia.release(velocity), false) });\n */\nexport function useInertia(value: MotionValue<number>, options: InertiaOptions = {}): UseInertia {\n  const reduced = useSafeReducedMotion();\n  const opts = useRef(options);\n  const live = useRef({ value, reduced });\n  useLayoutEffect(() => {\n    opts.current = options;\n    live.current = { value, reduced };\n  });\n  const anim = useRef<AnimationPlaybackControls | null>(null);\n\n  const stop = useCallback(() => {\n    anim.current?.stop();\n    anim.current = null;\n  }, []);\n\n  const finish = useCallback((controls: AnimationPlaybackControls, onRest?: (v: number) => void) => {\n    anim.current = controls;\n    controls.finished.then(() => {\n      if (anim.current !== controls) return;\n      anim.current = null;\n      onRest?.(live.current.value.get());\n    });\n  }, []);\n\n  const to = useCallback<UseInertia[\"to\"]>(\n    (target, { velocity = 0, transition, immediate } = {}) => {\n      const { value, reduced } = live.current;\n      stop();\n      if (immediate || reduced) {\n        value.jump(target);\n        opts.current.onRest?.(target);\n        return;\n      }\n      finish(animate(value, target, { ...(transition ?? opts.current.transition ?? coast), velocity }), opts.current.onRest);\n    },\n    [finish, stop],\n  );\n\n  const release = useCallback<UseInertia[\"release\"]>(\n    (velocity, override) => {\n      const o = { ...opts.current, ...override };\n      const { value, reduced } = live.current;\n      const from = value.get();\n      const target = inertiaTarget(from, velocity, o);\n      stop();\n      if (reduced) {\n        value.jump(target);\n        o.onRest?.(target);\n        return target;\n      }\n      // Released past an end: pull back without the outward velocity.\n      const outside = from < (o.min ?? -Infinity) || from > (o.max ?? Infinity);\n      const v = outside && Math.sign(velocity) === Math.sign(from - target) ? 0 : velocity;\n      if (o.mode === \"decay\") {\n        const rate = o.decelerationRate ?? DECELERATION.normal;\n        finish(\n          animate(value, from, {\n            type: \"inertia\",\n            velocity: v,\n            power: rate / (1 - rate) / 1000,\n            timeConstant: 240,\n            restDelta: 0.5,\n            modifyTarget: () => target,\n            min: o.min,\n            max: o.max,\n            bounceStiffness: 520,\n            bounceDamping: 38,\n          }),\n          o.onRest,\n        );\n      } else {\n        finish(animate(value, target, { ...(o.transition ?? coast), velocity: v }), o.onRest);\n      }\n      return target;\n    },\n    [finish, stop],\n  );\n\n  const isAnimating = useCallback(() => anim.current?.state === \"running\", []);\n\n  useEffect(() => stop, [stop]);\n\n  return useMemo(() => ({ release, to, stop, isAnimating }), [release, to, stop, isAnimating]);\n}\n\nexport { DECELERATION } from \"../gesture/gesture-math\";\n",
      "type": "registry:ui",
      "target": "@components/eased/_primitives/inertia-core/use-inertia.ts"
    }
  ],
  "type": "registry:ui"
}