{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "use-agent-run",
  "title": "Use agent run",
  "description": "The use-agent-run hook used by ndekoui components.",
  "dependencies": [],
  "registryDependencies": [
    "https://ui.ndekocode.dev/r/agent-run.json"
  ],
  "files": [
    {
      "path": "registry/ndeko/hooks/use-agent-run.ts",
      "content": "'use client';\n\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useEffect,\n  useRef,\n  useState,\n  useSyncExternalStore,\n} from 'react';\n\nimport {\n  createRunStore,\n  type RunEvent,\n  type RunSnapshot,\n  type RunStore,\n} from '@/registry/ndeko/lib/agent-run';\n\nexport function useRunSnapshot(store: RunStore) {\n  return useSyncExternalStore(\n    store.subscribe,\n    store.getSnapshot,\n    store.getSnapshot\n  );\n}\n\ntype Clock = {\n  now: number;\n  listeners: Set<() => void>;\n  timer?: ReturnType<typeof setInterval>;\n};\n\nconst CLOCKS = new Map<number, Clock>();\n\nfunction clockOf(intervalMs: number) {\n  const existing = CLOCKS.get(intervalMs);\n  if (existing) {\n    return existing;\n  }\n  const clock: Clock = { now: Date.now(), listeners: new Set() };\n  CLOCKS.set(intervalMs, clock);\n  return clock;\n}\n\nfunction tick(clock: Clock) {\n  clock.now = Date.now();\n  for (const listener of clock.listeners) {\n    listener();\n  }\n}\n\nfunction subscribeToClock(intervalMs: number, listener: () => void) {\n  const clock = clockOf(intervalMs);\n  clock.listeners.add(listener);\n  const isFirstListener = clock.timer === undefined;\n  if (isFirstListener) {\n    clock.now = Date.now();\n    clock.timer = setInterval(() => tick(clock), intervalMs);\n  }\n  return () => {\n    clock.listeners.delete(listener);\n    const hasListeners = clock.listeners.size > 0;\n    if (hasListeners) {\n      return;\n    }\n    clearInterval(clock.timer);\n    clock.timer = undefined;\n  };\n}\n\nfunction serverNow() {\n  return 0;\n}\n\nfunction idleSubscription() {\n  return () => {};\n}\n\n/** Off screen, every clock under a closed gate holds still, so nothing re-renders where no one looks. */\nexport const ClockGateContext = createContext(true);\n\n/** The current time, refreshed every `intervalMs` while `isActive` and the nearest gate is open; it holds still otherwise. */\nexport function useNow(intervalMs = 1000, isActive = true) {\n  const isGateOpen = useContext(ClockGateContext);\n  const isTicking = isActive && isGateOpen;\n  const subscribe = useCallback(\n    (listener: () => void) => {\n      if (!isTicking) {\n        return idleSubscription();\n      }\n      return subscribeToClock(intervalMs, listener);\n    },\n    [intervalMs, isTicking]\n  );\n  const getSnapshot = useCallback(() => clockOf(intervalMs).now, [intervalMs]);\n  return useSyncExternalStore(subscribe, getSnapshot, serverNow);\n}\n\nfunction subscribeToVisibility(listener: () => void) {\n  document.addEventListener('visibilitychange', listener);\n  return () => document.removeEventListener('visibilitychange', listener);\n}\n\nfunction isDocumentVisible() {\n  return document.visibilityState === 'visible';\n}\n\nfunction visibleOnServer() {\n  return true;\n}\n\nexport function useDocumentVisible() {\n  return useSyncExternalStore(\n    subscribeToVisibility,\n    isDocumentVisible,\n    visibleOnServer\n  );\n}\n\nexport type RunScriptStep = {\n  /** Milliseconds after the previous step. */\n  after: number;\n  event: RunEvent;\n};\n\nexport type RunScriptOptions = {\n  initial: RunSnapshot;\n  /** False while the view is off screen: the script holds its place and picks up from there. */\n  isActive?: boolean;\n  /** Starts again from `initial` this long after the last step. Without it the run stays at its end. */\n  loopAfter?: number;\n};\n\ntype Pending = {\n  remaining: number;\n  scheduledAt: number;\n};\n\n/**\n * Plays a scripted run into a store, the way a backend would stream it, for previews and demos.\n * It pauses while inactive or while the tab is hidden, and loops when asked to.\n */\nexport function useRunScript(\n  script: RunScriptStep[],\n  { initial, isActive = true, loopAfter }: RunScriptOptions\n) {\n  const [store] = useState(() => createRunStore(initial));\n  const [round, setRound] = useState(0);\n  const [isFinished, setIsFinished] = useState(false);\n  const isVisible = useDocumentVisible();\n  const nextIndex = useRef(0);\n  const pending = useRef<Pending | null>(null);\n  const shouldPlay = isActive && isVisible;\n\n  useEffect(() => {\n    if (!shouldPlay) {\n      return;\n    }\n    let timer: ReturnType<typeof setTimeout> | undefined;\n\n    function finish() {\n      const canLoop = loopAfter !== undefined;\n      if (!canLoop) {\n        setIsFinished(true);\n        return;\n      }\n      schedule(loopAfter, () => {\n        store.dispatch({ type: 'reset', snapshot: initial });\n        nextIndex.current = 0;\n        setRound((current) => current + 1);\n      });\n    }\n\n    function schedule(delay: number, run: () => void) {\n      const saved = pending.current;\n      const remaining = saved?.remaining ?? delay;\n      pending.current = { remaining, scheduledAt: Date.now() };\n      timer = setTimeout(() => {\n        pending.current = null;\n        run();\n      }, remaining);\n    }\n\n    function playNext() {\n      const step = script[nextIndex.current];\n      if (!step) {\n        finish();\n        return;\n      }\n      schedule(step.after, () => {\n        store.dispatch(step.event);\n        nextIndex.current += 1;\n        playNext();\n      });\n    }\n\n    playNext();\n\n    return () => {\n      clearTimeout(timer);\n      const saved = pending.current;\n      if (!saved) {\n        return;\n      }\n      const spent = Date.now() - saved.scheduledAt;\n      const remaining = Math.max(0, saved.remaining - spent);\n      pending.current = { remaining, scheduledAt: saved.scheduledAt };\n    };\n  }, [shouldPlay, script, store, initial, loopAfter, round]);\n\n  const restart = useCallback(() => {\n    pending.current = null;\n    nextIndex.current = 0;\n    store.dispatch({ type: 'reset', snapshot: initial });\n    setIsFinished(false);\n    setRound((current) => current + 1);\n  }, [store, initial]);\n\n  const snapshot = useRunSnapshot(store);\n\n  return { store, snapshot, restart, isFinished };\n}\n",
      "type": "registry:hook"
    }
  ],
  "categories": [
    "hooks"
  ],
  "type": "registry:hook"
}