{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "use-session",
  "title": "Use session",
  "description": "The use-session hook used by ndekoui components.",
  "dependencies": [],
  "registryDependencies": [
    "https://ui.ndekocode.dev/r/use-agent-run.json",
    "https://ui.ndekocode.dev/r/use-autoplay.json",
    "https://ui.ndekocode.dev/r/use-prefers-reduced-motion.json"
  ],
  "files": [
    {
      "path": "registry/ndeko/hooks/use-session.ts",
      "content": "'use client';\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { useDocumentVisible } from '@/registry/ndeko/hooks/use-agent-run';\nimport { useAutoplay } from '@/registry/ndeko/hooks/use-autoplay';\nimport { usePrefersReducedMotion } from '@/registry/ndeko/hooks/use-prefers-reduced-motion';\n\nexport type SessionStep<State, Action> = {\n  /** Milliseconds after the previous step. */\n  after: number;\n  action: Action;\n  /**\n   * Who does it. The agent's steps always play. A demo step plays only until a person takes over.\n   * A person step is done by the demonstration after its delay, and waits for the person once they\n   * have taken over.\n   */\n  by?: 'agent' | 'demo' | 'person';\n  /** For a person step: true once the person has done it themselves. */\n  isAnswered?: (state: State) => boolean;\n  /** The control the demonstration presses for this step. */\n  press?: string;\n};\n\nexport type SessionConfig<State, Action> = {\n  reducer: (state: State, action: Action) => State;\n  initial: State;\n  /** What the block shows under reduced motion, where nothing plays. */\n  resting: State;\n  script: readonly SessionStep<State, Action>[];\n  loopAfter: number;\n  /** While it returns true the agent's steps wait: a pause, or a person who took the controls. */\n  holdWhile?: (state: State) => boolean;\n};\n\ntype Engine<State> = {\n  state: State;\n  cursor: number;\n  round: number;\n};\n\ntype Plan = 'play' | 'skip' | 'wait' | 'loop' | 'stop';\n\ntype Pending = {\n  key: string;\n  remaining: number;\n  scheduledAt: number;\n};\n\ntype Timer = ReturnType<typeof setTimeout>;\n\n/** A finger goes down before the click lands. */\nconst PRESS_LEAD_MS = 140;\n\nfunction planOfPersonStep(isAutoplaying: boolean, isAnswered: boolean): Plan {\n  if (isAnswered) {\n    return 'skip';\n  }\n  if (isAutoplaying) {\n    return 'play';\n  }\n  return 'wait';\n}\n\nfunction planOfDemoStep(isAutoplaying: boolean): Plan {\n  if (isAutoplaying) {\n    return 'play';\n  }\n  return 'skip';\n}\n\nfunction planOfEnd(isAutoplaying: boolean): Plan {\n  if (isAutoplaying) {\n    return 'loop';\n  }\n  return 'stop';\n}\n\nfunction planOfAgentStep(isHeld: boolean): Plan {\n  if (isHeld) {\n    return 'wait';\n  }\n  return 'play';\n}\n\nfunction planOf<State, Action>(\n  step: SessionStep<State, Action> | undefined,\n  isAutoplaying: boolean,\n  isAnswered: boolean,\n  isHeld: boolean\n): Plan {\n  if (!step) {\n    return planOfEnd(isAutoplaying);\n  }\n  if (step.by === 'person') {\n    return planOfPersonStep(isAutoplaying, isAnswered);\n  }\n  if (step.by === 'demo') {\n    return planOfDemoStep(isAutoplaying);\n  }\n  return planOfAgentStep(isHeld);\n}\n\nfunction neverHold() {\n  return false;\n}\n\nfunction delayOf(plan: Plan, stepAfter: number, loopAfter: number) {\n  if (plan === 'loop') {\n    return loopAfter;\n  }\n  if (plan === 'skip') {\n    return 0;\n  }\n  return stepAfter;\n}\n\nfunction remainingOf(saved: Pending | null, key: string, delay: number) {\n  if (saved?.key === key) {\n    return saved.remaining;\n  }\n  return delay;\n}\n\nfunction skipped<State>(engine: Engine<State>): Engine<State> {\n  return { ...engine, cursor: engine.cursor + 1 };\n}\n\n/**\n * Plays a scripted session into a reducer while the block is on screen. The agent's part always\n * plays; the person's part is demonstrated until the first click or key, then waits for the person.\n */\nexport function useSession<State, Action>({\n  reducer,\n  initial,\n  resting,\n  script,\n  loopAfter,\n  holdWhile = neverHold,\n}: SessionConfig<State, Action>) {\n  const autoplay = useAutoplay([]);\n  const isVisible = useDocumentVisible();\n  const shouldReduceMotion = usePrefersReducedMotion();\n  const [engine, setEngine] = useState<Engine<State>>({\n    state: initial,\n    cursor: 0,\n    round: 0,\n  });\n  const [pressed, setPressed] = useState<string | null>(null);\n  const pending = useRef<Pending | null>(null);\n  const { isAutoplaying, isInView } = autoplay;\n  const restartAutoplay = autoplay.restart;\n  const step = script[engine.cursor];\n  const isAnswered = step?.isAnswered?.(engine.state) ?? false;\n  const isHeld = holdWhile(engine.state);\n  const plan = planOf(step, isAutoplaying, isAnswered, isHeld);\n  const isOnScreen = isInView && isVisible;\n  const waitKey = `${engine.round}:${engine.cursor}:${plan}`;\n  const restingCursor = script.length;\n\n  useEffect(() => {\n    if (!shouldReduceMotion) {\n      return;\n    }\n    const timer = setTimeout(() => {\n      setEngine({ state: resting, cursor: restingCursor, round: 0 });\n    }, 0);\n    return () => clearTimeout(timer);\n  }, [shouldReduceMotion, resting, restingCursor]);\n\n  useEffect(() => {\n    const isIdle = plan === 'wait' || plan === 'stop';\n    if (!isOnScreen || isIdle) {\n      return;\n    }\n    const stepAfter = step?.after ?? 0;\n    const delay = delayOf(plan, stepAfter, loopAfter);\n    const remaining = remainingOf(pending.current, waitKey, delay);\n    pending.current = { key: waitKey, remaining, scheduledAt: Date.now() };\n    let pressTimer: Timer | undefined;\n\n    function restarted(current: Engine<State>): Engine<State> {\n      return { state: initial, cursor: 0, round: current.round + 1 };\n    }\n\n    function play(action: Action) {\n      setEngine((current) => ({\n        ...current,\n        state: reducer(current.state, action),\n        cursor: current.cursor + 1,\n      }));\n    }\n\n    function fire() {\n      pending.current = null;\n      if (plan === 'loop') {\n        setEngine(restarted);\n        return;\n      }\n      if (plan === 'skip' || !step) {\n        setEngine(skipped);\n        return;\n      }\n      const action = step.action;\n      const pressKey = step.press;\n      const shouldPress = isAutoplaying && pressKey !== undefined;\n      if (!shouldPress) {\n        play(action);\n        return;\n      }\n      setPressed(pressKey);\n      pressTimer = setTimeout(() => {\n        pressTimer = undefined;\n        setPressed(null);\n        play(action);\n      }, PRESS_LEAD_MS);\n    }\n\n    const timer = setTimeout(fire, remaining);\n\n    return () => {\n      clearTimeout(timer);\n      const wasPressing = pressTimer !== undefined;\n      if (wasPressing) {\n        clearTimeout(pressTimer);\n        setPressed(null);\n        pending.current = { key: waitKey, remaining: 0, scheduledAt: 0 };\n        return;\n      }\n      const saved = pending.current;\n      if (!saved) {\n        return;\n      }\n      const spent = Date.now() - saved.scheduledAt;\n      const left = Math.max(0, saved.remaining - spent);\n      pending.current = { ...saved, remaining: left };\n    };\n  }, [\n    isOnScreen,\n    plan,\n    step,\n    waitKey,\n    loopAfter,\n    isAutoplaying,\n    reducer,\n    initial,\n  ]);\n\n  const act = useCallback(\n    (action: Action) => {\n      setEngine((current) => ({\n        ...current,\n        state: reducer(current.state, action),\n      }));\n    },\n    [reducer]\n  );\n\n  const restart = useCallback(() => {\n    pending.current = null;\n    setPressed(null);\n    // Under reduced motion nothing would play from the first state, so a replay keeps the resting one.\n    if (shouldReduceMotion) {\n      setEngine((current) => ({\n        state: resting,\n        cursor: restingCursor,\n        round: current.round + 1,\n      }));\n      return;\n    }\n    setEngine((current) => ({\n      state: initial,\n      cursor: 0,\n      round: current.round + 1,\n    }));\n    restartAutoplay();\n  }, [initial, resting, restingCursor, shouldReduceMotion, restartAutoplay]);\n\n  return {\n    ref: autoplay.ref,\n    isInView,\n    isAutoplaying,\n    state: engine.state,\n    act,\n    pressed,\n    isFinished: step === undefined,\n    restart,\n    /** Ends the demonstration for good, for a takeover the hook cannot see by itself. */\n    takeOver: autoplay.takeOver,\n  };\n}\n",
      "type": "registry:hook"
    }
  ],
  "categories": [
    "hooks"
  ],
  "type": "registry:hook"
}