{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "use-reduced-motion",
  "type": "registry:lib",
  "title": "useReducedMotion hook",
  "description": "@interlace/ui — the `useReducedMotion` hook every interactive primitive uses to gate animations on the user's OS preference.",
  "author": "ofri-peretz <https://github.com/ofri-peretz>",
  "categories": [
    "foundation",
    "util"
  ],
  "dependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/interlace-ui/lib/use-reduced-motion.ts",
      "target": "hooks/use-reduced-motion.ts",
      "type": "registry:lib",
      "content": "'use client';\n\nimport { useCallback, useSyncExternalStore } from 'react';\n\n// @interlace/use-reduced-motion v1.1.0 — Interlace design system.\n// Docs, props and live preview: https://ds.interlace.tools/c/use-reduced-motion\n// What changed since: https://ds.interlace.tools/c/use-reduced-motion#history\n// Generated banner — keep it, the upgrade diff reads this version.\n\nconst QUERY = '(prefers-reduced-motion: reduce)';\n\n/**\n * Returns `true` when the user has `prefers-reduced-motion: reduce` set.\n *\n * Use to gate motion-heavy components: animation should be disabled or\n * dramatically reduced when this returns `true`.\n *\n * ## Why `useSyncExternalStore` and not `useState` + `useEffect`\n *\n * The canonical hook — the one MUI, Vercel, Linear and Stripe all ship — is\n * `useState(false)` plus an effect that calls `matchMedia` on mount. It is\n * SSR-safe, and it is **one frame late**: the first render always returns\n * `false`, so a component that gates on it paints its animated first frame and\n * only then snaps to the still state.\n *\n * For most gates that is invisible. For the ones this package ships it is not —\n * the frame that gets painted is `AnimatedList`'s `scale: 0`, `FlipWords`' 8px\n * blur, `Spotlight`'s `opacity: 0`. A user who set the preference precisely\n * because motion makes them ill gets one frame of exactly the motion they\n * turned off, on every mount. WCAG 2.3.3 is not satisfied by \"briefly\".\n *\n * `useSyncExternalStore` reads the store DURING the first render, so on a\n * client-rendered mount — which is where every decorative component in this\n * package actually lives — the first painted frame is already correct.\n *\n * **The honest statement is that this closes the gap on CSR and cannot close it\n * on hydration.** The server genuinely cannot know the preference, so\n * `getServerSnapshot` must return `false` and the hydration frame is\n * unavoidable for any JavaScript hook. The only thing that closes THAT is CSS,\n * which is why `preflight.css` clamps `animation-duration` and\n * `transition-duration` under `reduce` for `*`: that reset is live before the\n * first paint and needs no JavaScript at all.\n *\n * So this hook is for the motion CSS cannot reach — `motion/react`,\n * `requestAnimationFrame`, timer-driven steps. The registry publishes that\n * split per component as `a11y.motion.driver`.\n *\n * @example\n * ```tsx\n * const reduceMotion = useReducedMotion();\n * <div className={reduceMotion ? 'static' : 'animate-bounce'} />\n * ```\n */\nexport function useReducedMotion(): boolean {\n  const subscribe = useCallback((onChange: () => void) => {\n    // `matchMedia` is absent in jsdom without a shim and in any non-browser\n    // runtime. A no-op unsubscribe leaves the store at the server snapshot\n    // rather than throwing — the same failure mode as the effect-based hook,\n    // minus the crash.\n    if (typeof window === 'undefined' || !window.matchMedia) return () => {};\n    const mql = window.matchMedia(QUERY);\n    mql.addEventListener('change', onChange);\n    return () => mql.removeEventListener('change', onChange);\n  }, []);\n\n  const getSnapshot = useCallback(() => {\n    if (typeof window === 'undefined' || !window.matchMedia) return false;\n    return window.matchMedia(QUERY).matches;\n  }, []);\n\n  // The server cannot know the preference. `false` is the only honest answer,\n  // and it is why the hydration frame above is unavoidable.\n  const getServerSnapshot = useCallback(() => false, []);\n\n  return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n}\n"
    }
  ],
  "meta": {
    "tier": "util",
    "client": true,
    "minViewport": null,
    "loading": false,
    "version": "1.1.0",
    "since": "1.0.0"
  },
  "docs": "## @interlace/use-reduced-motion\n\nInstalled to `hooks/use-reduced-motion.ts`.\n\n```tsx\nimport { /* … */ } from '@/hooks/use-reduced-motion';\n```\n\nProps, a11y contract, live preview and source: https://ds.interlace.tools/c/use-reduced-motion"
}
