{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "eased-thinking-core",
  "title": "Thinking Core primitive",
  "description": "The clocks, models and contracts behind Eased's thinking states: useElapsed (seconds on the start's own grid), useTokenRate (a smoothed stream rate as motion values), useStatusCarousel, the step model, and the frozen StepStatus and ToolState types with an AI SDK tool-part adapter.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://eased.dev/r/eased-tokens.json"
  ],
  "files": [
    {
      "path": "src/registry/eased/_primitives/thinking-core/thinking-core.ts",
      "content": "/**\n * P11 thinking-core: the clocks, models and contracts shared by every Eased thinking state\n * (Shimmer Text, Thinking Bar, Agent Orb, Step Rail, Reasoning, Tool Call Row, Agent Run).\n * Import from here; the files behind it may move.\n */\nexport {\n  SETTLED_STEP,\n  SETTLED_TOOL,\n  toolStateFromPart,\n  type AISDKToolPartState,\n  type Step,\n  type StepStatus,\n  type ToolState,\n} from \"./types\";\nexport { advanceSteps, summarizeSteps, updateStep, type StepSummary } from \"./steps\";\nexport { formatElapsed, useElapsed, type UseElapsedOptions } from \"./use-elapsed\";\nexport { useTokenRate, type TokenRate, type UseTokenRateOptions } from \"./use-token-rate\";\nexport { useStatusCarousel, type UseStatusCarouselOptions } from \"./use-status-carousel\";\n",
      "type": "registry:ui",
      "target": "@components/eased/_primitives/thinking-core/thinking-core.ts"
    },
    {
      "path": "src/registry/eased/_primitives/thinking-core/types.ts",
      "content": "/**\n * Frozen cross-lane contracts (P11). Lane 2 (Chat) and later Thinking items import these;\n * add members only with a coordinated change, because consumers switch over them exhaustively.\n */\n\n/**\n * Where one step of a run is.\n *\n *   pending   queued, not started\n *   active    running now (one per level, usually)\n *   complete  finished successfully\n *   error     failed; the run may continue or stop\n *   skipped   not needed after all (cancelled plan steps, short-circuited checks)\n */\nexport type StepStatus = \"pending\" | \"active\" | \"complete\" | \"error\" | \"skipped\";\n\n/**\n * Where one tool call is. A superset of the AI SDK's tool-part states, collapsed to what the\n * UI distinguishes; map AI SDK parts with `toolStateFromPart`.\n *\n *   pending            input still streaming in, or queued\n *   awaiting-approval  needs the user to approve or deny before it runs\n *   running            executing\n *   done               returned output\n *   error              threw or returned an error\n *   denied             the user declined it\n */\nexport type ToolState = \"pending\" | \"awaiting-approval\" | \"running\" | \"done\" | \"error\" | \"denied\";\n\n/** States after which a step or tool will not change again. */\nexport const SETTLED_STEP: ReadonlySet<StepStatus> = new Set([\"complete\", \"error\", \"skipped\"]);\nexport const SETTLED_TOOL: ReadonlySet<ToolState> = new Set([\"done\", \"error\", \"denied\"]);\n\n/** One step of a run: a chain-of-thought line, a plan item, an agent action. */\nexport type Step = {\n  /** Stable id: steps are keyed by it, so re-ordering moves rows instead of re-mounting them. */\n  id: string;\n  label: string;\n  status: StepStatus;\n  /** A secondary line: a URL read, a count found, an error message. */\n  detail?: string;\n  /** Epoch ms when the step became active. */\n  startedAt?: number;\n  /** Epoch ms when it settled. */\n  endedAt?: number;\n  /** Nested sub-steps. */\n  steps?: Step[];\n};\n\n/** The AI SDK (v5+) tool-part `state` values `toolStateFromPart` understands. */\nexport type AISDKToolPartState =\n  | \"input-streaming\"\n  | \"input-available\"\n  | \"approval-requested\"\n  | \"approval-responded\"\n  | \"output-available\"\n  | \"output-error\"\n  | \"output-denied\";\n\n/**\n * Maps an AI SDK tool part to a `ToolState`, so `useChat` messages drive Eased tool UI directly:\n * `toolStateFromPart(part)` where `part.type` starts with `tool-`. Unknown states read as `pending`.\n */\nexport function toolStateFromPart(part: { state?: string; approval?: { approved?: boolean } }): ToolState {\n  switch (part.state as AISDKToolPartState | undefined) {\n    case \"input-available\":\n      return \"running\";\n    case \"approval-requested\":\n      return \"awaiting-approval\";\n    case \"approval-responded\":\n      return part.approval?.approved === false ? \"denied\" : \"running\";\n    case \"output-available\":\n      return \"done\";\n    case \"output-error\":\n      return \"error\";\n    case \"output-denied\":\n      return \"denied\";\n    default:\n      return \"pending\";\n  }\n}\n",
      "type": "registry:ui",
      "target": "@components/eased/_primitives/thinking-core/types.ts"
    },
    {
      "path": "src/registry/eased/_primitives/thinking-core/steps.ts",
      "content": "import { SETTLED_STEP, type Step, type StepStatus } from \"./types\";\n\n/** A run's summary, derived from its steps. Pure: call it in render. */\nexport type StepSummary = {\n  /** Leaf and parent steps at the top level. */\n  total: number;\n  /** Top-level steps that have settled (complete, error or skipped). */\n  settled: number;\n  completed: number;\n  errored: number;\n  /** settled / total, 0–1: what a progress rail draws to. 0 for an empty run. */\n  progress: number;\n  /** The first active top-level step, or the first pending one if none is active yet. */\n  current: Step | undefined;\n  /** The run as one status: error if any step failed and nothing is still going, active while anything is pending or active, complete when all settled. */\n  status: StepStatus;\n  /** Epoch ms span from the first start to the last end, when the steps carry times. */\n  durationMs: number | undefined;\n};\n\n/** Summarises a list of steps for a rail, a header (\"4 of 7\"), or a collapsed run (\"7 steps · 45s\"). */\nexport function summarizeSteps(steps: readonly Step[]): StepSummary {\n  let settled = 0;\n  let completed = 0;\n  let errored = 0;\n  let current: Step | undefined;\n  let firstPending: Step | undefined;\n  let start = Infinity;\n  let end = -Infinity;\n  for (const s of steps) {\n    if (SETTLED_STEP.has(s.status)) settled++;\n    if (s.status === \"complete\") completed++;\n    if (s.status === \"error\") errored++;\n    if (s.status === \"active\" && !current) current = s;\n    if (s.status === \"pending\" && !firstPending) firstPending = s;\n    if (s.startedAt !== undefined) start = Math.min(start, s.startedAt);\n    if (s.endedAt !== undefined) end = Math.max(end, s.endedAt);\n  }\n  const total = steps.length;\n  const running = settled < total;\n  return {\n    total,\n    settled,\n    completed,\n    errored,\n    progress: total ? settled / total : 0,\n    current: current ?? firstPending,\n    status: total === 0 ? \"pending\" : running ? (current || settled ? \"active\" : \"pending\") : errored ? \"error\" : \"complete\",\n    durationMs: Number.isFinite(start) && Number.isFinite(end) && end >= start ? end - start : undefined,\n  };\n}\n\n/**\n * Immutable update of one step by id, anywhere in the tree. Stamps `startedAt` when a step\n * becomes active and `endedAt` when it settles, unless the patch sets them.\n *\n *   setSteps((s) => updateStep(s, \"search\", { status: \"complete\", detail: \"14 sources\" }))\n */\nexport function updateStep(steps: readonly Step[], id: string, patch: Partial<Omit<Step, \"id\">>, now = Date.now()): Step[] {\n  return steps.map((s) => {\n    if (s.id === id) {\n      const next = { ...s, ...patch };\n      if (patch.status === \"active\" && s.status !== \"active\" && patch.startedAt === undefined) next.startedAt = now;\n      if (patch.status && SETTLED_STEP.has(patch.status) && !SETTLED_STEP.has(s.status) && patch.endedAt === undefined) next.endedAt = now;\n      return next;\n    }\n    return s.steps ? { ...s, steps: updateStep(s.steps, id, patch, now) } : s;\n  });\n}\n\n/**\n * Settles the active step and activates the next pending one: the usual \"next\" in a linear run.\n * Returns the steps unchanged when nothing is left to advance.\n */\nexport function advanceSteps(steps: readonly Step[], outcome: \"complete\" | \"error\" | \"skipped\" = \"complete\", now = Date.now()): Step[] {\n  const active = steps.find((s) => s.status === \"active\");\n  let next: Step[] = [...steps];\n  if (active) next = updateStep(next, active.id, { status: outcome }, now);\n  const pending = next.find((s) => s.status === \"pending\");\n  if (pending) next = updateStep(next, pending.id, { status: \"active\" }, now);\n  return next;\n}\n",
      "type": "registry:ui",
      "target": "@components/eased/_primitives/thinking-core/steps.ts"
    },
    {
      "path": "src/registry/eased/_primitives/thinking-core/use-elapsed.ts",
      "content": "\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\n\nexport type UseElapsedOptions = {\n  /** Counting while true. When it turns false the value freezes (at `endedAt` if given). Default true. */\n  running?: boolean;\n  /** When the clock started (epoch ms or Date). Omit to start when `running` first turns true. */\n  startedAt?: number | Date;\n  /** When it stopped. Use it to show a finished run's exact duration, e.g. from the server. */\n  endedAt?: number | Date;\n};\n\nconst ms = (t: number | Date | undefined) => (t === undefined ? undefined : typeof t === \"number\" ? t : t.getTime());\n\n/**\n * Whole seconds since a start, ticking exactly on each second boundary of the start time (not\n * of the wall clock), so \"1s\" appears one second after the start, not up to two. It reads\n * `Date.now()` on every tick rather than counting ticks, so a throttled background tab is\n * still right when it returns. Re-renders once a second while running, and never on the\n * server (it returns 0 until mounted).\n *\n *   const seconds = useElapsed({ running: status === \"thinking\", startedAt });\n */\nexport function useElapsed({ running = true, startedAt, endedAt }: UseElapsedOptions = {}): number {\n  const start = useRef<number | undefined>(ms(startedAt));\n  const frozen = useRef<number | undefined>(undefined);\n  const [seconds, setSeconds] = useState(0);\n\n  const startMs = ms(startedAt);\n  const endMs = ms(endedAt);\n\n  useEffect(() => {\n    if (startMs !== undefined) start.current = startMs;\n    const from = () => start.current ?? Date.now();\n    if (!running) {\n      // Freeze: at the given end, else at the moment it stopped.\n      if (start.current === undefined && endMs === undefined) return;\n      const stop = endMs ?? frozen.current ?? Date.now();\n      frozen.current = stop;\n      setSeconds(Math.max(0, Math.floor((stop - from()) / 1000)));\n      return;\n    }\n    // Without a `startedAt`, every new run (running false → true) starts from now.\n    if (startMs === undefined && (start.current === undefined || frozen.current !== undefined)) start.current = Date.now();\n    frozen.current = undefined;\n    let timer = 0;\n    const tick = () => {\n      const elapsed = Date.now() - from();\n      setSeconds(Math.max(0, Math.floor(elapsed / 1000)));\n      // Next boundary of the start's own second grid, plus a hair so floor() lands on it.\n      timer = window.setTimeout(tick, 1000 - (((elapsed % 1000) + 1000) % 1000) + 4);\n    };\n    tick();\n    return () => clearTimeout(timer);\n  }, [running, startMs, endMs]);\n\n  return seconds;\n}\n\n/**\n * Seconds as a compact duration: `12s`, `1m 4s`, `1h 2m`. Every digit is a digit, so it rolls\n * cleanly when passed to Number Roll as a `formatter`.\n */\nexport function formatElapsed(seconds: number): string {\n  const s = Math.max(0, Math.floor(seconds));\n  if (s < 60) return `${s}s`;\n  const m = Math.floor(s / 60);\n  if (m < 60) return `${m}m ${s % 60}s`;\n  return `${Math.floor(m / 60)}h ${m % 60}m`;\n}\n",
      "type": "registry:ui",
      "target": "@components/eased/_primitives/thinking-core/use-elapsed.ts"
    },
    {
      "path": "src/registry/eased/_primitives/thinking-core/use-token-rate.ts",
      "content": "\"use client\";\n\nimport { useMotionValue, type MotionValue } from \"motion/react\";\nimport { useEffect, useRef } from \"react\";\n\nexport type UseTokenRateOptions = {\n  /** Tokens per second that reads as full speed (`rate` = 1). Default 60, a fast model's stream. */\n  max?: number;\n  /**\n   * Smoothing time constant in seconds. Streams arrive in bursts; the rate follows the average\n   * over roughly this window, so a 40-token chunk reads as a surge, not a spike. Default 0.6.\n   */\n  smoothing?: number;\n  /** Characters per token when `source` is a string. Default 4 (English prose). */\n  charsPerToken?: number;\n};\n\nexport type TokenRate = {\n  /** Smoothed tokens per second. */\n  tps: MotionValue<number>;\n  /** `tps / max`, clamped to 0–1: feed it to anything that should move with the stream. */\n  rate: MotionValue<number>;\n};\n\n/**\n * How fast a stream is flowing, as motion values, so an orb or a meter can follow it without\n * re-rendering. Pass the streaming text (or a running token count); every change counts the\n * new tokens, and a frame loop turns them into a smoothed tokens-per-second that rises with\n * bursts and decays to 0 when the stream pauses. The loop runs only while the rate is\n * non-zero. If the text shrinks (a new message), it starts over.\n *\n *   const { rate } = useTokenRate(message.text);\n *   <AgentOrb state=\"speaking\" rate={rate} />\n */\nexport function useTokenRate(source: string | number, { max = 60, smoothing = 0.6, charsPerToken = 4 }: UseTokenRateOptions = {}): TokenRate {\n  const tps = useMotionValue(0);\n  const rate = useMotionValue(0);\n  const last = useRef<number | null>(null);\n  const pending = useRef(0);\n  const frame = useRef(0);\n  const opts = useRef({ max, smoothing });\n\n  useEffect(() => {\n    opts.current = { max, smoothing };\n  });\n\n  useEffect(() => {\n    const count = typeof source === \"number\" ? source : source.length / charsPerToken;\n    const prev = last.current;\n    last.current = count;\n    if (prev === null || count < prev) return; // first sight, or a reset\n    pending.current += count - prev;\n    if (frame.current) return;\n\n    let then = performance.now();\n    const step = (now: number) => {\n      const dt = Math.min(0.1, Math.max(0.001, (now - then) / 1000));\n      then = now;\n      const observed = pending.current / dt;\n      pending.current = 0;\n      const { max, smoothing } = opts.current;\n      const k = 1 - Math.exp(-dt / Math.max(0.05, smoothing));\n      const next = tps.get() + (observed - tps.get()) * k;\n      if (next < 0.05) {\n        tps.set(0);\n        rate.set(0);\n        frame.current = 0;\n        return;\n      }\n      tps.set(next);\n      rate.set(Math.min(1, next / max));\n      frame.current = requestAnimationFrame(step);\n    };\n    frame.current = requestAnimationFrame(step);\n  }, [source, charsPerToken, tps, rate]);\n\n  useEffect(\n    () => () => {\n      cancelAnimationFrame(frame.current);\n      frame.current = 0;\n    },\n    [],\n  );\n\n  return { tps, rate };\n}\n",
      "type": "registry:ui",
      "target": "@components/eased/_primitives/thinking-core/use-token-rate.ts"
    },
    {
      "path": "src/registry/eased/_primitives/thinking-core/use-status-carousel.ts",
      "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\nexport type UseStatusCarouselOptions = {\n  /**\n   * A real status. While set it is shown instead of the rotation, at once; when it clears the\n   * rotation resumes where it left off. Use it for \"Reading stripe.com…\" from a tool event.\n   */\n  status?: string | null;\n  /**\n   * Milliseconds each line stays. Default: by length, 2.2s plus 45ms a character, capped at\n   * 4s, so a long line can be read and a short one does not linger.\n   */\n  interval?: number | ((line: string) => number);\n  /** Stop rotating (keeps the current line). Default false. */\n  paused?: boolean;\n  /** Start over at the first line when the lines change. Default true. */\n  restartOnChange?: boolean;\n  /** Cycle back to the first line after the last. Default false: hold on the last line. */\n  loop?: boolean;\n};\n\nconst byLength = (line: string) => Math.min(4000, 2200 + line.length * 45);\n\n/**\n * The line to show from a rotating set of status lines (\"Pondering…\", \"Reading the diff…\"),\n * interrupted by a real `status` whenever there is one. By default it holds on the last line\n * instead of looping back, since a status that repeats reads as stuck; set `loop` for\n * ambient lines that may cycle. It does not advance while the tab is hidden. Render the result\n * through Text Swap or Shimmer Text so each change is a swap, not a pop.\n *\n *   const line = useStatusCarousel([\"Searching…\", \"Reading 4 pages…\", \"Comparing…\"], { status });\n *   <ShimmerText>{line}</ShimmerText>\n */\nexport function useStatusCarousel(lines: readonly string[], { status, interval, paused = false, restartOnChange = true, loop = false }: UseStatusCarouselOptions = {}): string {\n  const [index, setIndex] = useState(0);\n  // Compared by content, so an inline array literal does not restart the rotation every render.\n  const linesKey = lines.join(\"\\u0000\");\n  const [source, setSource] = useState(linesKey);\n  // Adjust state while rendering when the lines change (no effect, no flash of the old line).\n  if (source !== linesKey) {\n    setSource(linesKey);\n    if (restartOnChange) setIndex(0);\n  }\n  const i = Math.min(index, Math.max(0, lines.length - 1));\n  const line = lines[i] ?? \"\";\n  const hold = status ? null : typeof interval === \"function\" ? interval(line) : (interval ?? byLength(line));\n  const atEnd = lines.length < 2 || (!loop && i >= lines.length - 1);\n\n  useEffect(() => {\n    if (hold === null || paused || atEnd) return;\n    let timer = 0;\n    let remaining = hold;\n    let since = performance.now();\n    const arm = () => {\n      since = performance.now();\n      timer = window.setTimeout(() => setIndex((n) => (n + 1) % lines.length), remaining);\n    };\n    const onVisibility = () => {\n      if (document.hidden) {\n        clearTimeout(timer);\n        remaining = Math.max(0, remaining - (performance.now() - since));\n      } else arm();\n    };\n    if (!document.hidden) arm();\n    document.addEventListener(\"visibilitychange\", onVisibility);\n    return () => {\n      clearTimeout(timer);\n      document.removeEventListener(\"visibilitychange\", onVisibility);\n    };\n  }, [hold, paused, atEnd, i, lines.length]);\n\n  return status || line;\n}\n",
      "type": "registry:ui",
      "target": "@components/eased/_primitives/thinking-core/use-status-carousel.ts"
    }
  ],
  "type": "registry:ui"
}