{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "use-intersection-observer",
  "title": "useIntersectionObserver",
  "description": "A callback-ref hook reporting whether an element is intersecting its viewport, with an optional one-shot reveal-once mode.",
  "files": [
    {
      "path": "src/registry/hooks/use-intersection-observer.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nexport interface UseIntersectionObserverOptions {\n  /** Passed through to `IntersectionObserver` as `threshold`. Default 0. */\n  threshold?: number | number[]\n  /** Passed through to `IntersectionObserver` as `rootMargin`. Default \"0px\". */\n  rootMargin?: string\n  /** Disconnect the observer the first time the element intersects. Default false. */\n  once?: boolean\n}\n\nexport interface UseIntersectionObserverResult<T extends Element> {\n  /** Callback ref — attach it to the element you want to observe. */\n  ref: (node: T | null) => void\n  isIntersecting: boolean\n  entry: IntersectionObserverEntry | null\n}\n\n/**\n * 用**callback ref**(不是 `useRef` + `useEffect`)订阅一个元素的可见性。这是\n * 比 `ref` 对象更稳的姿势:`ref` 对象只在挂载时读一次 `.current`,元素被条件\n * 渲染替换(换 key、换分支)时不会自动重新 observe;callback ref 由 React 在\n * 节点挂载/替换/卸载时逐次调用,天然拿到「现在到底在观察哪个节点」的真相,\n * 于是重新 observe 变成免费的。\n *\n * `ref` 回调本身用 `React.useCallback` 包裹,依赖数组是 `[observerInit, once]`,\n * 其中 `observerInit` 是把 `threshold`/`rootMargin` 序列化成字符串(`thresholdKey`\n * = `JSON.stringify(threshold)`)后再 `useMemo` 出来的 —— `threshold` 常是调用方\n * 每次渲染现写的数组字面量(`[0, 0.5]`),按引用比较会导致每次渲染都判定“变了”\n * 从而无谓地 disconnect/reconnect;以序列化值为 key 之后,内容不变时 `observerInit`\n * 保持同一个对象身份,`ref` 回调也保持同一份函数身份,已挂载的节点不会被重新\n * observe。\n *\n * 只有 `observerInit`/`once` 真的变化(调用方显式改了 threshold/rootMargin/once)\n * 时,React 才会用新的 `ref` 函数身份替换旧的——这一步 React 会自动先用 `null`\n * 调一次旧函数、再用当前节点调一次新函数,于是「断开旧 observer、按新 options\n * 重新 observe」全部发生在 callback ref 里,不需要额外的 `useEffect`。\n *\n * 生命周期:节点挂载→(SSR/无 `IntersectionObserver` 时直接跳过)建 observer→\n * observe;每次交叉状态变化在 observer 的(异步)回调里 `setState`,不是在渲染期\n * 或 effect 体内同步 `setState`;`once` 为 true 时,一旦交叉就 `disconnect()`\n * 并保留 `isIntersecting: true`,后续不再触发;节点被替换或组件卸载时,\n * React 用 `null` 调用 callback ref,此时无条件 `disconnect()` 上一个 observer。\n * 多个组件各自调用本 hook 时,各自持有独立的 observer/state,互不影响。\n */\nexport function useIntersectionObserver<T extends Element = HTMLElement>(\n  options: UseIntersectionObserverOptions = {},\n): UseIntersectionObserverResult<T> {\n  const { threshold = 0, rootMargin = \"0px\", once = false } = options\n  const [isIntersecting, setIsIntersecting] = React.useState(false)\n  const [entry, setEntry] = React.useState<IntersectionObserverEntry | null>(null)\n  const observerRef = React.useRef<IntersectionObserver | null>(null)\n\n  // `threshold` is frequently an inline array literal (`[0, 0.5]`) that gets a\n  // fresh identity every render even when its contents are unchanged. Serialize\n  // it into a stable string key first, then memoize the actual `IntersectionObserver`\n  // init dict off that key (+ rootMargin) so its identity only changes when the\n  // values themselves change — not on every render.\n  const thresholdKey = JSON.stringify(threshold)\n  const observerInit = React.useMemo<IntersectionObserverInit>(\n    () => ({ threshold: JSON.parse(thresholdKey) as number | number[], rootMargin }),\n    [thresholdKey, rootMargin],\n  )\n\n  const ref = React.useCallback(\n    (node: T | null) => {\n      observerRef.current?.disconnect()\n      observerRef.current = null\n\n      if (!node || typeof IntersectionObserver === \"undefined\") return\n\n      const observer = new IntersectionObserver(([observedEntry]) => {\n        setEntry(observedEntry)\n        setIsIntersecting(observedEntry.isIntersecting)\n        if (once && observedEntry.isIntersecting) {\n          observer.disconnect()\n        }\n      }, observerInit)\n\n      observer.observe(node)\n      observerRef.current = observer\n    },\n    [observerInit, once],\n  )\n\n  return { ref, isIntersecting, entry }\n}\n\nexport default useIntersectionObserver\n",
      "type": "registry:hook"
    }
  ],
  "type": "registry:hook"
}