{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "hooks",
  "type": "registry:component",
  "title": "Hooks",
  "description": "Scroll and motion hooks: useReducedMotion, useScrollspy, useReadProgress, useInView, plus smoothScrollTo.",
  "dependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/hooks/hooks.ts",
      "type": "registry:hook",
      "target": "components/moco/hooks.ts",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nexport function useReducedMotion(): boolean {\n  const [reduced, setReduced] = React.useState(false);\n  React.useEffect(() => {\n    const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    const on = () => setReduced(mq.matches);\n    on();\n    mq.addEventListener(\"change\", on);\n    return () => mq.removeEventListener(\"change\", on);\n  }, []);\n  return reduced;\n}\n\n/** Index of the section currently crossing the middle band of the viewport. */\nexport function useScrollspy(ids: string[]): number {\n  const [active, setActive] = React.useState(0);\n\n  React.useEffect(() => {\n    if (!ids.length) return;\n    const seen = new Map<string, number>();\n    const io = new IntersectionObserver(\n      (entries) => {\n        for (const e of entries) seen.set(e.target.id, e.intersectionRatio);\n        // The last heading whose top has passed the band wins.\n        let next = 0;\n        ids.forEach((id, i) => {\n          const el = document.getElementById(id);\n          if (el && el.getBoundingClientRect().top <= window.innerHeight * 0.35) next = i;\n        });\n        setActive(next);\n      },\n      { rootMargin: \"-30% 0px -60% 0px\", threshold: [0, 0.5, 1] },\n    );\n    const els = ids.map((id) => document.getElementById(id)).filter(Boolean) as Element[];\n    els.forEach((el) => io.observe(el));\n\n    const onScroll = () => {\n      let next = 0;\n      ids.forEach((id, i) => {\n        const el = document.getElementById(id);\n        if (el && el.getBoundingClientRect().top <= window.innerHeight * 0.35) next = i;\n      });\n      setActive(next);\n    };\n    onScroll();\n    window.addEventListener(\"scroll\", onScroll, { passive: true });\n    return () => {\n      io.disconnect();\n      window.removeEventListener(\"scroll\", onScroll);\n    };\n  }, [ids]);\n\n  return active;\n}\n\n/** 0→1 read progress through the first element matching the selector. */\nexport function useReadProgress(selector = \"#article-root\"): number {\n  const [p, setP] = React.useState(0);\n  React.useEffect(() => {\n    const onScroll = () => {\n      const el = document.querySelector<HTMLElement>(selector);\n      if (!el) return;\n      const start = el.offsetTop;\n      const end = start + el.offsetHeight - window.innerHeight;\n      const y = window.scrollY;\n      const raw = end > start ? (y - start) / (end - start) : 0;\n      setP(Math.min(1, Math.max(0, raw)));\n    };\n    onScroll();\n    window.addEventListener(\"scroll\", onScroll, { passive: true });\n    window.addEventListener(\"resize\", onScroll);\n    return () => {\n      window.removeEventListener(\"scroll\", onScroll);\n      window.removeEventListener(\"resize\", onScroll);\n    };\n  }, [selector]);\n  return p;\n}\n\n/** Fires once when the element first enters the viewport. */\nexport function useInView<T extends Element>(\n  ref: React.RefObject<T | null>,\n  { once = true, rootMargin = \"0px 0px -15% 0px\" } = {},\n): boolean {\n  const [seen, setSeen] = React.useState(false);\n  React.useEffect(() => {\n    const el = ref.current;\n    if (!el) return;\n    const io = new IntersectionObserver(\n      ([e]) => {\n        if (e.isIntersecting) {\n          setSeen(true);\n          if (once) io.disconnect();\n        } else if (!once) {\n          setSeen(false);\n        }\n      },\n      { rootMargin },\n    );\n    io.observe(el);\n    return () => io.disconnect();\n  }, [ref, once, rootMargin]);\n  return seen;\n}\n\nexport function smoothScrollTo(id: string) {\n  const el = document.getElementById(id);\n  if (!el) return;\n  const reduce = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n  el.scrollIntoView({ behavior: reduce ? \"auto\" : \"smooth\", block: \"start\" });\n  el.setAttribute(\"tabindex\", \"-1\");\n  (el as HTMLElement).focus({ preventScroll: true });\n}\n"
    }
  ],
  "docs": "\"use client\";\n\nimport * as React from \"react\";\nimport { useInView, useReadProgress, useScrollspy, smoothScrollTo } from \"./hooks\";\n\nexport default function HooksDemo() {\n  const sections = [\"one\", \"two\", \"three\"];\n  const active = useScrollspy(sections);\n  const progress = useReadProgress(\"#demo-article\");\n  const ref = React.useRef<HTMLParagraphElement>(null);\n  const seen = useInView(ref);\n\n  return (\n    <div>\n      <div style={{ position: \"sticky\", top: 0, display: \"flex\", gap: \"1rem\" }}>\n        <span>read: {Math.round(progress * 100)}%</span>\n        {sections.map((id, i) => (\n          <button key={id} onClick={() => smoothScrollTo(id)} style={{ fontWeight: active === i ? 700 : 400 }}>\n            {id}\n          </button>\n        ))}\n      </div>\n      <article id=\"demo-article\">\n        {sections.map((id) => (\n          <section key={id} id={id} style={{ minHeight: \"80vh\" }}>\n            <h2>{id}</h2>\n          </section>\n        ))}\n        <p ref={ref}>{seen ? \"You scrolled me into view.\" : \"Scroll down…\"}</p>\n      </article>\n    </div>\n  );\n}\n",
  "meta": {
    "tags": [
      "hooks",
      "scroll",
      "motion",
      "utility"
    ]
  }
}