{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "use-autoplay",
  "title": "Use autoplay",
  "description": "The use-autoplay hook used by ndekoui components.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://ui.ndekocode.dev/r/use-agent-run.json",
    "https://ui.ndekocode.dev/r/use-prefers-reduced-motion.json"
  ],
  "files": [
    {
      "path": "registry/ndeko/hooks/use-autoplay.ts",
      "content": "'use client';\n\nimport { useInView } from 'motion/react';\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useEffect,\n  useRef,\n  useState,\n} from 'react';\n\nimport { useDocumentVisible } from '@/registry/ndeko/hooks/use-agent-run';\nimport { usePrefersReducedMotion } from '@/registry/ndeko/hooks/use-prefers-reduced-motion';\n\nexport type AutoplayStep = {\n  /** Milliseconds after the previous step. */\n  after: number;\n  run: () => void;\n};\n\nexport type AutoplayOptions = {\n  /** Starts again this long after the last step. Without it the demonstration plays once. */\n  loopAfter?: number;\n  /** Puts the piece back in its first state before a new round. */\n  onReset?: () => void;\n  isEnabled?: boolean;\n};\n\n/**\n * A page that shows several pieces at once can let them play in turn: under a closed gate a piece\n * holds its place, as it does off screen, and takes up again where it stopped once the gate opens.\n */\nexport const AutoplayGateContext = createContext(true);\n\ntype Pending = {\n  remaining: number;\n  scheduledAt: number;\n};\n\ntype Latest = {\n  steps: readonly AutoplayStep[];\n  onReset?: () => void;\n};\n\n/**\n * Plays a piece's own demonstration while it is on screen: each step calls a handler a person\n * would trigger. The first click or key inside the piece ends it for good, and so does the keyboard\n * bringing focus into it, so the script never fights the person or moves what they just reached.\n * It holds its place off screen and in a hidden tab, and it does not play under reduced motion.\n * Steps must not move focus or open a layer outside the piece.\n */\nexport function useAutoplay<Element extends HTMLElement = HTMLDivElement>(\n  steps: readonly AutoplayStep[],\n  { loopAfter, onReset, isEnabled = true }: AutoplayOptions = {}\n) {\n  const ref = useRef<Element>(null);\n  const isInView = useInView(ref, { amount: 0.3 });\n  const isVisible = useDocumentVisible();\n  const shouldReduceMotion = usePrefersReducedMotion();\n  const isGateOpen = useContext(AutoplayGateContext);\n  const [isTakenOver, setIsTakenOver] = useState(false);\n  const [round, setRound] = useState(0);\n  const latest = useRef<Latest>({ steps, onReset });\n  const nextIndex = useRef(0);\n  const pending = useRef<Pending | null>(null);\n  const isAutoplaying = isEnabled && !isTakenOver && !shouldReduceMotion;\n  const isOnScreen = isInView && isVisible;\n  const isItsTurn = isOnScreen && isGateOpen;\n  const shouldPlay = isAutoplaying && isItsTurn;\n\n  useEffect(() => {\n    latest.current = { steps, onReset };\n  });\n\n  useEffect(() => {\n    if (!shouldPlay) {\n      return;\n    }\n    let timer: ReturnType<typeof setTimeout> | undefined;\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 finish() {\n      if (loopAfter === undefined) {\n        return;\n      }\n      schedule(loopAfter, () => {\n        nextIndex.current = 0;\n        latest.current.onReset?.();\n        setRound((current) => current + 1);\n      });\n    }\n\n    function playNext() {\n      const index = nextIndex.current;\n      const step = latest.current.steps[index];\n      if (!step) {\n        finish();\n        return;\n      }\n      schedule(step.after, () => {\n        latest.current.steps[index]?.run();\n        nextIndex.current = index + 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, loopAfter, round]);\n\n  useEffect(() => {\n    const element = ref.current;\n    if (!element || isTakenOver) {\n      return;\n    }\n    function takeOverFrom(event: Event) {\n      if (event.isTrusted) {\n        setIsTakenOver(true);\n      }\n    }\n    function takeOverOnKeyboardFocus(event: FocusEvent) {\n      const target = event.target;\n      if (!(target instanceof HTMLElement)) {\n        return;\n      }\n      const isFromPerson = event.isTrusted;\n      const isKeyboardFocus = target.matches(':focus-visible');\n      if (isFromPerson && isKeyboardFocus) {\n        setIsTakenOver(true);\n      }\n    }\n    element.addEventListener('pointerdown', takeOverFrom, true);\n    element.addEventListener('keydown', takeOverFrom, true);\n    element.addEventListener('focusin', takeOverOnKeyboardFocus);\n    return () => {\n      element.removeEventListener('pointerdown', takeOverFrom, true);\n      element.removeEventListener('keydown', takeOverFrom, true);\n      element.removeEventListener('focusin', takeOverOnKeyboardFocus);\n    };\n  }, [isTakenOver]);\n\n  const takeOver = useCallback(() => {\n    setIsTakenOver(true);\n  }, []);\n\n  const restart = useCallback(() => {\n    pending.current = null;\n    nextIndex.current = 0;\n    latest.current.onReset?.();\n    setIsTakenOver(false);\n    setRound((current) => current + 1);\n  }, []);\n\n  return { ref, isInView, isAutoplaying, takeOver, restart };\n}\n\ntype TypingOptions = {\n  /** Milliseconds before the first character. */\n  startAfter?: number;\n  /** Milliseconds between two characters. */\n  characterMs?: number;\n};\n\nfunction delayOfCharacter(\n  index: number,\n  startAfter: number,\n  characterMs: number\n) {\n  if (index === 0) {\n    return startAfter;\n  }\n  return characterMs;\n}\n\n/** One step per character, so a field fills the way a person types into it. */\nexport function typingSteps(\n  text: string,\n  write: (typed: string) => void,\n  { startAfter = 400, characterMs = 42 }: TypingOptions = {}\n): AutoplayStep[] {\n  const characters = Array.from(text);\n  return characters.map((_, index) => {\n    const end = index + 1;\n    const typed = characters.slice(0, end).join('');\n    const after = delayOfCharacter(index, startAfter, characterMs);\n    return { after, run: () => write(typed) };\n  });\n}\n",
      "type": "registry:hook"
    }
  ],
  "categories": [
    "hooks"
  ],
  "type": "registry:hook"
}