{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "use-idle",
  "title": "useIdle",
  "description": "Flips a boolean idle state after a period with no pointer, keyboard, wheel, or touch activity, with a throttled reset and SSR-safe cleanup.",
  "files": [
    {
      "path": "src/registry/hooks/use-idle.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nexport interface UseIdleOptions {\n  /** Activity events to listen for, attached to `window` as passive. Passing this replaces the default list wholesale. */\n  events?: string[]\n  /** Initial idle value at mount. Default `false` — treated as \"activity just happened\". */\n  initialState?: boolean\n}\n\nconst DEFAULT_EVENTS = [\"pointermove\", \"pointerdown\", \"keydown\", \"wheel\", \"touchstart\"]\n\n// high-frequency events (pointermove/wheel) reach handleActivity on every tick; a\n// clearTimeout + setTimeout per tick costs real time with many listeners or on a weak\n// device. Throttle by timestamp comparison instead: activity inside the window is\n// dropped and only the first activity past it resets the timer. The cost is that idle\n// can fire up to one window early, which is fine for idle detection.\n// the window has to narrow with timeout: a fixed 1s would call continuous activity idle\n// whenever timeout < 1s (the 500ms timer expires first, while the next activity let\n// through has to wait for 1s).\nconst throttleFor = (timeout: number) => Math.min(1000, Math.max(100, Math.floor(timeout / 3)))\n\n/**\n * User idle detection. Mounting starts a `timeout` ms timer; any of `events` (pointer\n * move/down, keyboard, wheel, touch by default) counts as activity — it sets `idle`\n * to `false` and restarts the timer, and the timer expiring sets it to `true`.\n *\n * `visibilitychange` is handled separately and cannot be switched off through\n * `events`: coming back to the tab (`document.visibilityState === \"visible\"`) counts\n * as activity and resets the timer, but leaving it (hidden) does **not** force idle —\n * the timer keeps counting real elapsed time and flips on its own, with no \"away means\n * idle\" special case (a background tab's timer may be throttled and fire late; that\n * drift is accepted rather than chased second by second).\n *\n * Every `setState` happens in an event or timer callback, never during render or\n * synchronously in an effect body, so it is SSR-safe; unmount clears the timer and\n * every listener.\n */\nexport function useIdle(timeout = 60000, options: UseIdleOptions = {}): boolean {\n  const { events = DEFAULT_EVENTS, initialState = false } = options\n  const [idle, setIdle] = React.useState(initialState)\n\n  // latest events in a ref; the effect depends on eventsKey below (contents, not the\n  // array identity), so an inline array literal from a consumer does not tear the\n  // listeners down and back up every render. The ref is synced in a dependency-array-less\n  // effect (after render), never written during render.\n  const eventsRef = React.useRef(events)\n  React.useEffect(() => {\n    eventsRef.current = events\n  })\n  const eventsKey = events.join(\",\")\n\n  // holds the current idle value across effect rebuilds (timeout / events changing), so\n  // the internal state cannot drift from the rendered `idle`.\n  const idleRef = React.useRef(initialState)\n\n  React.useEffect(() => {\n    if (typeof window === \"undefined\") return\n\n    const eventNames = eventsRef.current\n    let idleTimer: ReturnType<typeof setTimeout> | null = null\n    let lastActivity = 0\n\n    const setIdleState = (next: boolean) => {\n      idleRef.current = next\n      setIdle(next)\n    }\n\n    const throttleMs = throttleFor(timeout)\n\n    const scheduleIdle = () => {\n      if (idleTimer !== null) clearTimeout(idleTimer)\n      idleTimer = setTimeout(() => setIdleState(true), timeout)\n    }\n\n    const handleActivity = () => {\n      const now = Date.now()\n      if (now - lastActivity < throttleMs) return\n      lastActivity = now\n      if (idleRef.current) setIdleState(false)\n      scheduleIdle()\n    }\n\n    const handleVisibility = () => {\n      if (document.visibilityState === \"visible\") handleActivity()\n      // hidden: deliberately untouched — let the timer already scheduled above expire on its own.\n    }\n\n    scheduleIdle()\n    eventNames.forEach(name => window.addEventListener(name, handleActivity, { passive: true }))\n    document.addEventListener(\"visibilitychange\", handleVisibility)\n\n    return () => {\n      if (idleTimer !== null) clearTimeout(idleTimer)\n      eventNames.forEach(name => window.removeEventListener(name, handleActivity))\n      document.removeEventListener(\"visibilitychange\", handleVisibility)\n    }\n  }, [timeout, eventsKey])\n\n  return idle\n}\n\nexport default useIdle\n",
      "type": "registry:hook"
    }
  ],
  "type": "registry:hook"
}