{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "use-mouse-position",
  "title": "useMousePosition",
  "description": "A hook that tracks pointer position, optionally relative to an element, with clamped 0..1 normalized coordinates.",
  "files": [
    {
      "path": "src/registry/hooks/use-mouse-position.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nexport interface UseMousePositionOptions {\n  /** 传了就额外给出相对该元素的坐标与 0..1 归一化值;不传则相对视口。 */\n  ref?: React.RefObject<Element | null>\n  /** 关掉监听(指针驱动的效果不在屏幕上时没必要跟踪)。默认 true。 */\n  enabled?: boolean\n  /** 用 requestAnimationFrame 把每帧的多次 pointermove 合并成一次提交。默认 true。 */\n  throttleWithRaf?: boolean\n}\n\nexport interface MousePosition {\n  /** 视口坐标(clientX / clientY)。 */\n  x: number\n  y: number\n  /** 相对 `ref` 元素左上角的坐标;没传 ref 时等于视口坐标。 */\n  elementX: number\n  elementY: number\n  /** `elementX/Y` 除以元素宽高并 clamp 到 0..1,直接可喂给渐变 / 视差 / 倾斜。 */\n  normalizedX: number\n  normalizedY: number\n  /** 指针当前是否落在 `ref` 元素矩形内(没传 ref 时表示指针是否还在文档内)。 */\n  isInside: boolean\n}\n\nconst INITIAL_POSITION: MousePosition = {\n  x: 0,\n  y: 0,\n  elementX: 0,\n  elementY: 0,\n  normalizedX: 0,\n  normalizedY: 0,\n  isInside: false,\n}\n\nfunction clamp01(value: number): number {\n  if (Number.isNaN(value)) return 0\n  return Math.min(1, Math.max(0, value))\n}\n\n/**\n * 跟踪指针位置,并可选地把它换算成相对某个元素的坐标 + 0..1 归一化值 ——\n * spotlight、磁吸按钮、视差、倾斜卡片这类效果要的就是后面那对归一化值。\n *\n * - **用 `pointermove` 而不是 `mousemove`**:鼠标、触控笔、触摸屏走同一套事件,\n *   不必再补一份 touch 分支。\n * - **每帧一次提交**:`pointermove` 一秒能打上百次,默认把最新坐标存进闭包变量、\n *   用 rAF 在下一帧统一 `setState`;`throttleWithRaf: false` 则逐事件同步提交。\n * - **归一化值恒在 0..1**:指针移出元素时 `isInside` 变 false,但归一化值被 clamp\n *   住不会越界,消费者不必自己兜边界;元素宽或高为 0 时按 0 处理,不会出 NaN。\n * - **SSR 安全**:监听全在 effect 内挂,首帧返回全 0 且 `isInside: false`。\n * - **清理**:pending 的 rAF、`pointermove` 与 `pointerleave` 监听在卸载 / 依赖\n *   变化时全部取消。\n */\nexport function useMousePosition(options: UseMousePositionOptions = {}): MousePosition {\n  const { ref, enabled = true, throttleWithRaf = true } = options\n  const [position, setPosition] = React.useState<MousePosition>(INITIAL_POSITION)\n\n  React.useEffect(() => {\n    if (!enabled) return\n\n    let frame: number | null = null\n    let latest: { x: number; y: number } | null = null\n\n    const commit = () => {\n      frame = null\n      const point = latest\n      latest = null\n      if (!point) return\n\n      const element = ref?.current ?? null\n\n      if (!element) {\n        // 没传 ref:element* 就是视口坐标,归一化按视口尺寸算。\n        const width = window.innerWidth || 1\n        const height = window.innerHeight || 1\n        setPosition({\n          x: point.x,\n          y: point.y,\n          elementX: point.x,\n          elementY: point.y,\n          normalizedX: clamp01(point.x / width),\n          normalizedY: clamp01(point.y / height),\n          isInside: true,\n        })\n        return\n      }\n\n      // 每次提交都重新量一次:元素可能被滚动、被布局变化推走。因为提交已经被 rAF\n      // 压到每帧一次,这里的 getBoundingClientRect 也就是每帧一次。\n      const rect = element.getBoundingClientRect()\n      const elementX = point.x - rect.left\n      const elementY = point.y - rect.top\n\n      setPosition({\n        x: point.x,\n        y: point.y,\n        elementX,\n        elementY,\n        normalizedX: rect.width > 0 ? clamp01(elementX / rect.width) : 0,\n        normalizedY: rect.height > 0 ? clamp01(elementY / rect.height) : 0,\n        isInside:\n          elementX >= 0 && elementX <= rect.width && elementY >= 0 && elementY <= rect.height,\n      })\n    }\n\n    const handlePointerMove = (event: PointerEvent) => {\n      latest = { x: event.clientX, y: event.clientY }\n      if (!throttleWithRaf) {\n        commit()\n        return\n      }\n      if (frame !== null) return\n      frame = requestAnimationFrame(commit)\n    }\n\n    // 指针整个移出文档(离开窗口 / 切走)时,坐标保留最后一次值,只把 isInside 归位。\n    const handlePointerLeave = () => {\n      setPosition(previous => (previous.isInside ? { ...previous, isInside: false } : previous))\n    }\n\n    window.addEventListener(\"pointermove\", handlePointerMove)\n    document.addEventListener(\"pointerleave\", handlePointerLeave)\n\n    return () => {\n      window.removeEventListener(\"pointermove\", handlePointerMove)\n      document.removeEventListener(\"pointerleave\", handlePointerLeave)\n      if (frame !== null) cancelAnimationFrame(frame)\n    }\n  }, [enabled, ref, throttleWithRaf])\n\n  return position\n}\n\nexport default useMousePosition\n",
      "type": "registry:hook"
    }
  ],
  "type": "registry:hook"
}