{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "use-stream",
  "title": "useStream",
  "description": "Reads an AI SDK UI message stream or plain SSE, with stop and regenerate.",
  "files": [
    {
      "path": "registry/spotcheck/hooks/use-stream.ts",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nexport type StreamSource = { id: string; title: string; url?: string }\nexport type StreamTool = { id: string; name: string; input?: unknown; output?: unknown; done: boolean }\nexport type StreamStatus = \"idle\" | \"streaming\" | \"done\" | \"stopped\" | \"error\"\nexport type StreamState = { text: string; sources: StreamSource[]; tools: StreamTool[] }\n\nconst EMPTY: StreamState = { text: \"\", sources: [], tools: [] }\nconst DONE = \"[DONE]\"\nconst AI_SDK_HEADER = \"x-vercel-ai-ui-message-stream\"\n\n/** Splits SSE text into complete `data:` payloads, returning whatever is still incomplete. */\nexport function readSseEvents(buffer: string) {\n  const blocks = buffer.split(/\\r?\\n\\r?\\n/)\n  const rest = blocks.pop() ?? \"\"\n  const events = blocks.flatMap((block) => {\n    const data = block\n      .split(/\\r?\\n/)\n      .filter((line) => line.startsWith(\"data:\"))\n      .map((line) => line.slice(5).replace(/^ /, \"\"))\n    return data.length > 0 ? [data.join(\"\\n\")] : []\n  })\n  return { events, rest }\n}\n\n/** Applies one AI SDK UI message stream part. Parts it doesn't use are ignored. */\nexport function applyAiSdkPart(state: StreamState, data: string): StreamState {\n  if (data === DONE) return state\n  const part = JSON.parse(data) as Record<string, unknown> & { type: string }\n  const id = String(part.toolCallId ?? \"\")\n\n  switch (part.type) {\n    case \"text-delta\":\n      return { ...state, text: state.text + String(part.delta ?? \"\") }\n    case \"source-url\":\n      return {\n        ...state,\n        sources: [\n          ...state.sources,\n          { id: String(part.sourceId), title: String(part.title ?? part.url), url: String(part.url) },\n        ],\n      }\n    case \"tool-input-start\":\n      return { ...state, tools: [...state.tools, { id, name: String(part.toolName), done: false }] }\n    case \"tool-input-available\":\n      return { ...state, tools: state.tools.map((tool) => (tool.id === id ? { ...tool, input: part.input } : tool)) }\n    case \"tool-output-available\":\n      return {\n        ...state,\n        tools: state.tools.map((tool) => (tool.id === id ? { ...tool, output: part.output, done: true } : tool)),\n      }\n    case \"error\":\n      throw new Error(String(part.errorText ?? \"The stream failed.\"))\n    default:\n      return state\n  }\n}\n\ntype UseStreamOptions = {\n  /** Sent as the JSON body of the POST request. */\n  body?: unknown\n  headers?: Record<string, string>\n}\n\n/**\n * Streams from an endpoint. Responses from the AI SDK's toUIMessageStreamResponse are detected by\n * their header. Anything else is read as plain SSE, where each `data:` line is more text.\n */\nexport function useStream(url: string, { body, headers }: UseStreamOptions = {}) {\n  const [state, setState] = React.useState(EMPTY)\n  const [status, setStatus] = React.useState<StreamStatus>(\"idle\")\n  const [error, setError] = React.useState<string | null>(null)\n  const controller = React.useRef<AbortController | null>(null)\n\n  const start = React.useCallback(\n    async (override?: unknown) => {\n      controller.current?.abort()\n      const abort = new AbortController()\n      controller.current = abort\n      setState(EMPTY)\n      setError(null)\n      setStatus(\"streaming\")\n\n      try {\n        const response = await fetch(url, {\n          method: \"POST\",\n          headers: { \"content-type\": \"application/json\", ...headers },\n          body: JSON.stringify(override ?? body ?? {}),\n          signal: abort.signal,\n        })\n        if (!response.ok || !response.body) throw new Error(`The request failed with ${response.status}.`)\n\n        const isAiSdk = response.headers.has(AI_SDK_HEADER)\n        const reader = response.body.pipeThrough(new TextDecoderStream()).getReader()\n        let buffer = \"\"\n        for (;;) {\n          const { value, done } = await reader.read()\n          if (done) break\n          const { events, rest } = readSseEvents(buffer + value)\n          buffer = rest\n          setState((current) =>\n            events.reduce(\n              (next, data) =>\n                isAiSdk ? applyAiSdkPart(next, data) : data === DONE ? next : { ...next, text: next.text + data },\n              current,\n            ),\n          )\n        }\n        setStatus(\"done\")\n      } catch (failure) {\n        if (abort.signal.aborted) return\n        setError(failure instanceof Error ? failure.message : \"The stream failed.\")\n        setStatus(\"error\")\n      }\n    },\n    [body, headers, url],\n  )\n\n  const stop = React.useCallback(() => {\n    controller.current?.abort()\n    setStatus(\"stopped\")\n  }, [])\n\n  const reset = React.useCallback(() => {\n    controller.current?.abort()\n    setState(EMPTY)\n    setError(null)\n    setStatus(\"idle\")\n  }, [])\n\n  React.useEffect(() => () => controller.current?.abort(), [])\n\n  return { ...state, status, error, start, stop, regenerate: start, reset }\n}\n",
      "type": "registry:hook"
    }
  ],
  "type": "registry:hook"
}