{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "streaming-message",
  "title": "Streaming message",
  "description": "Streaming message is a production-ready ai component from BJORK UI, packaged for shadcn registries.",
  "dependencies": [
    "clsx",
    "framer-motion",
    "lucide-react",
    "next-themes",
    "tailwind-merge"
  ],
  "files": [
    {
      "path": "components/bjork-ui/ai/streaming-message.tsx",
      "content": "\"use client\";\n\nimport {\n  memo,\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n  type ReactNode,\n} from \"react\";\nimport { Check, Copy } from \"lucide-react\";\nimport { LiveRegion } from \"@/components/bjork-ui/_core/a11y\";\nimport {\n  AI_WELL,\n  FOCUS_RING,\n  PRESS,\n  useAiTone,\n  useReduceMotion,\n  type BjorkTone,\n} from \"@/components/bjork-ui/ai/_shared\";\nimport { cn } from \"@/lib/utils\";\n\n// ---------------------------------------------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------------------------------------------\n\nexport type StreamingMessageRole = \"assistant\" | \"user\";\n\nexport interface StreamingCodeProps {\n  code: string;\n  /** Language from the fence info string, lower-cased. Empty when the fence has none. */\n  language: string;\n  /** True while the closing fence has not arrived yet. */\n  open: boolean;\n}\n\nexport interface StreamingMessageComponents {\n  /** Replaces the whole fenced code block, header included. */\n  code?: (props: StreamingCodeProps) => ReactNode;\n  /** Replaces inline `code` spans. */\n  inlineCode?: (props: { children: string }) => ReactNode;\n  /** Replaces links. `href` has already passed the safe-URL check. */\n  link?: (props: { href: string; children: ReactNode }) => ReactNode;\n}\n\nexport interface StreamingMessageProps {\n  /** Markdown source. Pass the full text so far on every chunk; partial input is expected. */\n  content: string;\n  /** True while tokens are still arriving. Shows the caret and tolerates half-typed markup. */\n  streaming?: boolean;\n  /** \"assistant\" renders flat prose. \"user\" renders a quiet bubble. Default \"assistant\". */\n  role?: StreamingMessageRole;\n  /** Number the lines of fenced code blocks. Default false. */\n  lineNumbers?: boolean;\n  /** Called after a code block is copied to the clipboard. */\n  onCopy?: (code: string, language: string) => void;\n  /** Override the rendering of code blocks, inline code or links. */\n  components?: StreamingMessageComponents;\n  /** Accessible name for the message. Defaults to \"Assistant message\" or \"Your message\". */\n  label?: string;\n  tone?: BjorkTone;\n  className?: string;\n}\n\n// ---------------------------------------------------------------------------------------------------------------\n// Sample data\n// ---------------------------------------------------------------------------------------------------------------\n\nexport const SAMPLE_STREAMING_MARKDOWN = `Short answer: **debounce the writes, not the reads.** The editor already batches keystrokes, so the slow part is the \\`persist()\\` call that fires on every change.\n\n### What to change\n\n1. Wrap \\`persist\\` in a trailing debounce of about 400ms.\n2. Flush on \\`visibilitychange\\` so a closed tab never loses the last edit.\n3. Keep reads synchronous; the cache in \\`draftStore\\` is already warm.\n\n\\`\\`\\`ts\nimport { debounce } from \"@/lib/timing\";\n\nconst persistSoon = debounce(persist, 400);\n\neditor.on(\"change\", (doc) => persistSoon(doc));\ndocument.addEventListener(\"visibilitychange\", () => {\n  if (document.visibilityState === \"hidden\") persistSoon.flush();\n});\n\\`\\`\\`\n\n> On the Kestrel Mini test rig this cut write volume by **~92%** with no lost drafts across 1,200 forced tab closes.\n\nIf you need cross-device sync later, the [fieldnotes.dev write-up](https://fieldnotes.dev/debounce-vs-throttle) compares this with a throttled queue.`;\n\n// ---------------------------------------------------------------------------------------------------------------\n// Block parser\n// ---------------------------------------------------------------------------------------------------------------\n\ninterface ListItem {\n  text: string;\n  children: ListBlock | null;\n}\n\ninterface ListBlock {\n  type: \"list\";\n  ordered: boolean;\n  start: number;\n  items: ListItem[];\n}\n\ntype Block =\n  | { type: \"heading\"; level: 1 | 2 | 3; text: string }\n  | { type: \"paragraph\"; text: string }\n  | { type: \"quote\"; text: string }\n  | { type: \"hr\" }\n  | { type: \"code\"; language: string; code: string; open: boolean }\n  | { type: \"table\"; head: string[]; align: (\"left\" | \"center\" | \"right\")[]; rows: string[][] }\n  | ListBlock;\n\ninterface ParsedBlock {\n  /** The exact source this block came from. Unchanged source means an unchanged render. */\n  raw: string;\n  block: Block;\n}\n\nconst FENCE = /^ {0,3}(`{3,}|~{3,})\\s*([\\w+#.-]*)/;\nconst HEADING = /^ {0,3}(#{1,6})\\s+(.*?)\\s*#*\\s*$/;\nconst HR = /^ {0,3}([-*_])(\\s*\\1){2,}\\s*$/;\nconst QUOTE = /^ {0,3}>\\s?(.*)$/;\nconst LIST_ITEM = /^(\\s*)([-*+]|\\d{1,9}[.)])\\s+(.*)$/;\nconst TABLE_SEP = /^\\s*\\|?\\s*:?-{1,}:?\\s*(\\|\\s*:?-{1,}:?\\s*)*\\|?\\s*$/;\n// Lines that are only the start of a markup token. While streaming they are held back so \"##\" never shows as text.\nconst BARE_MARKER = /^\\s*(#{1,6}|[-*+>]|[-*_]{2}|\\d{1,9}[.)]?|`{1,2}|~{1,2}|\\|)\\s*$/;\n\nfunction splitRow(line: string): string[] {\n  let s = line.trim();\n  if (s.startsWith(\"|\")) s = s.slice(1);\n  if (s.endsWith(\"|\") && !s.endsWith(\"\\\\|\")) s = s.slice(0, -1);\n  return s.split(/(?<!\\\\)\\|/).map((c) => c.trim().replace(/\\\\\\|/g, \"|\"));\n}\n\nfunction isBlockStart(line: string): boolean {\n  return FENCE.test(line) || HEADING.test(line) || HR.test(line) || QUOTE.test(line) || LIST_ITEM.test(line);\n}\n\nfunction buildList(rows: { depth: number; ordered: boolean; start: number; text: string }[]): ListBlock {\n  const root: ListBlock = { type: \"list\", ordered: rows[0].ordered, start: rows[0].start, items: [] };\n  const stack: { depth: number; list: ListBlock }[] = [{ depth: rows[0].depth, list: root }];\n  for (const row of rows) {\n    while (stack.length > 1 && row.depth < stack[stack.length - 1].depth) stack.pop();\n    let top = stack[stack.length - 1];\n    if (row.depth > top.depth && top.list.items.length > 0) {\n      const parent = top.list.items[top.list.items.length - 1];\n      if (!parent.children) parent.children = { type: \"list\", ordered: row.ordered, start: row.start, items: [] };\n      top = { depth: row.depth, list: parent.children };\n      stack.push(top);\n    }\n    top.list.items.push({ text: row.text, children: null });\n  }\n  return root;\n}\n\n/** Splits markdown into blocks. With `partial`, a trailing half-typed marker line is held back. */\nexport function parseMarkdownBlocks(source: string, partial = false): ParsedBlock[] {\n  const lines = source.replace(/\\r\\n?/g, \"\\n\").split(\"\\n\");\n  if (partial && lines.length > 0 && BARE_MARKER.test(lines[lines.length - 1])) lines.pop();\n  const out: ParsedBlock[] = [];\n  let i = 0;\n\n  while (i < lines.length) {\n    const line = lines[i];\n    if (line.trim() === \"\") {\n      i++;\n      continue;\n    }\n\n    const fence = FENCE.exec(line);\n    if (fence) {\n      const marker = fence[1];\n      const body: string[] = [];\n      let j = i + 1;\n      let open = true;\n      while (j < lines.length) {\n        const t = lines[j].trim();\n        if (t.startsWith(marker[0].repeat(marker.length)) && t.replace(new RegExp(`\\\\${marker[0]}`, \"g\"), \"\") === \"\") {\n          open = false;\n          break;\n        }\n        body.push(lines[j]);\n        j++;\n      }\n      out.push({\n        raw: lines.slice(i, open ? j : j + 1).join(\"\\n\") + (open ? \"\\u0000open\" : \"\"),\n        block: { type: \"code\", language: fence[2].toLowerCase(), code: body.join(\"\\n\"), open },\n      });\n      i = open ? j : j + 1;\n      continue;\n    }\n\n    const heading = HEADING.exec(line);\n    if (heading) {\n      const level = Math.min(3, heading[1].length) as 1 | 2 | 3;\n      out.push({ raw: line, block: { type: \"heading\", level, text: heading[2] } });\n      i++;\n      continue;\n    }\n\n    if (HR.test(line)) {\n      out.push({ raw: line, block: { type: \"hr\" } });\n      i++;\n      continue;\n    }\n\n    if (QUOTE.test(line)) {\n      const body: string[] = [];\n      let j = i;\n      while (j < lines.length && QUOTE.test(lines[j])) {\n        body.push(QUOTE.exec(lines[j])![1]);\n        j++;\n      }\n      out.push({ raw: lines.slice(i, j).join(\"\\n\"), block: { type: \"quote\", text: body.join(\"\\n\") } });\n      i = j;\n      continue;\n    }\n\n    if (LIST_ITEM.test(line)) {\n      const rows: { depth: number; ordered: boolean; start: number; text: string }[] = [];\n      let j = i;\n      while (j < lines.length) {\n        const m = LIST_ITEM.exec(lines[j]);\n        if (m) {\n          const ordered = /\\d/.test(m[2]);\n          rows.push({\n            depth: Math.floor(m[1].replace(/\\t/g, \"  \").length / 2),\n            ordered,\n            start: ordered ? parseInt(m[2], 10) : 1,\n            text: m[3],\n          });\n          j++;\n        } else if (lines[j].trim() !== \"\" && /^\\s{2,}/.test(lines[j]) && rows.length > 0) {\n          rows[rows.length - 1].text += ` ${lines[j].trim()}`; // lazy continuation line\n          j++;\n        } else break;\n      }\n      out.push({ raw: lines.slice(i, j).join(\"\\n\"), block: buildList(rows) });\n      i = j;\n      continue;\n    }\n\n    // A table needs a header row and a separator. While streaming, a lone pipe row is shown as a header so the\n    // later separator does not flip a paragraph into a table.\n    const next = lines[i + 1];\n    const pipeRow = line.trim().startsWith(\"|\");\n    const isTable = line.includes(\"|\") && next !== undefined && TABLE_SEP.test(next);\n    const pendingTable = partial && pipeRow && i === lines.length - 1;\n    if (isTable || pendingTable) {\n      const head = splitRow(line);\n      const align = isTable\n        ? splitRow(next).map((c) =>\n            c.startsWith(\":\") && c.endsWith(\":\") ? \"center\" : c.endsWith(\":\") ? \"right\" : (\"left\" as const),\n          )\n        : head.map(() => \"left\" as const);\n      const rows: string[][] = [];\n      let j = isTable ? i + 2 : i + 1;\n      while (j < lines.length && lines[j].includes(\"|\") && lines[j].trim() !== \"\") {\n        rows.push(splitRow(lines[j]));\n        j++;\n      }\n      out.push({ raw: lines.slice(i, j).join(\"\\n\"), block: { type: \"table\", head, align, rows } });\n      i = j;\n      continue;\n    }\n\n    const body: string[] = [line];\n    let j = i + 1;\n    while (j < lines.length && lines[j].trim() !== \"\" && !isBlockStart(lines[j])) {\n      body.push(lines[j]);\n      j++;\n    }\n    out.push({ raw: body.join(\"\\n\"), block: { type: \"paragraph\", text: body.join(\"\\n\") } });\n    i = j;\n  }\n  return out;\n}\n\n// ---------------------------------------------------------------------------------------------------------------\n// Inline parser\n// ---------------------------------------------------------------------------------------------------------------\n\ntype Inline =\n  | { t: \"text\"; v: string }\n  | { t: \"code\"; v: string }\n  | { t: \"strong\"; c: Inline[] }\n  | { t: \"em\"; c: Inline[] }\n  | { t: \"link\"; href: string | null; c: Inline[] }\n  | { t: \"br\" };\n\nconst SAFE_URL = /^(https?:\\/\\/|mailto:|\\/(?!\\/)|#|\\.{1,2}\\/)/i;\n\nfunction findClose(s: string, marker: string, from: number): number {\n  let k = from;\n  while (k < s.length) {\n    const at = s.indexOf(marker, k);\n    if (at === -1) return -1;\n    // A single \"*\" closer must not be half of a \"**\".\n    if (marker.length === 1 && (s[at + 1] === marker || s[at - 1] === marker) && marker === \"*\") {\n      k = at + 2;\n      continue;\n    }\n    if (at > from && s[at - 1] !== \" \") return at;\n    k = at + 1;\n  }\n  return -1;\n}\n\n/**\n * Parses inline markdown. With `partial`, an unclosed marker styles the rest of the text instead of printing\n * the raw asterisks, so \"**bol\" renders as bold \"bol\" rather than flashing \"**bol\".\n */\nfunction parseInline(s: string, partial: boolean): Inline[] {\n  const out: Inline[] = [];\n  let buf = \"\";\n  const flush = () => {\n    if (buf) out.push({ t: \"text\", v: buf });\n    buf = \"\";\n  };\n  let i = 0;\n  while (i < s.length) {\n    const ch = s[i];\n    const rest = s.length - i;\n\n    if (ch === \"\\\\\" && rest > 1 && /[\\\\`*_[\\]()#|>-]/.test(s[i + 1])) {\n      buf += s[i + 1];\n      i += 2;\n      continue;\n    }\n    if (ch === \"\\n\") {\n      // Markdown hard break: two trailing spaces. Otherwise a soft break is a space.\n      if (buf.endsWith(\"  \")) {\n        buf = buf.trimEnd();\n        flush();\n        out.push({ t: \"br\" });\n      } else buf = `${buf.trimEnd()} `;\n      i++;\n      continue;\n    }\n    if (ch === \"`\") {\n      const end = s.indexOf(\"`\", i + 1);\n      if (end !== -1) {\n        flush();\n        out.push({ t: \"code\", v: s.slice(i + 1, end) });\n        i = end + 1;\n        continue;\n      }\n      if (partial) {\n        flush();\n        if (rest > 1) out.push({ t: \"code\", v: s.slice(i + 1) });\n        return out;\n      }\n    }\n    const double = s.startsWith(\"**\", i) || s.startsWith(\"__\", i);\n    if (double && rest > 2 && s[i + 2] !== \" \") {\n      const marker = s.slice(i, i + 2);\n      const end = s.indexOf(marker, i + 2);\n      if (end !== -1) {\n        flush();\n        out.push({ t: \"strong\", c: parseInline(s.slice(i + 2, end), false) });\n        i = end + 2;\n        continue;\n      }\n      if (partial) {\n        flush();\n        out.push({ t: \"strong\", c: parseInline(s.slice(i + 2), true) });\n        return out;\n      }\n    }\n    if (double && rest === 2 && partial) return (flush(), out);\n    const wordUnderscore = ch === \"_\" && /\\w/.test(s[i - 1] ?? \"\");\n    if ((ch === \"*\" || ch === \"_\") && !double && !wordUnderscore && rest > 1 && s[i + 1] !== \" \") {\n      const end = findClose(s, ch, i + 1);\n      if (end !== -1) {\n        flush();\n        out.push({ t: \"em\", c: parseInline(s.slice(i + 1, end), false) });\n        i = end + 1;\n        continue;\n      }\n      if (partial) {\n        flush();\n        out.push({ t: \"em\", c: parseInline(s.slice(i + 1), true) });\n        return out;\n      }\n    }\n    if ((ch === \"*\" || ch === \"_\") && rest === 1 && partial && !wordUnderscore) return (flush(), out);\n    if (ch === \"[\") {\n      const close = s.indexOf(\"]\", i + 1);\n      if (close !== -1 && s[close + 1] === \"(\") {\n        const end = s.indexOf(\")\", close + 2);\n        if (end !== -1) {\n          flush();\n          const url = s.slice(close + 2, end).trim().split(/\\s+/)[0] ?? \"\";\n          out.push({ t: \"link\", href: SAFE_URL.test(url) ? url : null, c: parseInline(s.slice(i + 1, close), false) });\n          i = end + 1;\n          continue;\n        }\n        if (partial) {\n          // \"[label](https://fie\" shows the label while the URL is still arriving.\n          flush();\n          out.push({ t: \"link\", href: null, c: parseInline(s.slice(i + 1, close), false) });\n          return out;\n        }\n      }\n      if (partial && (close === -1 || close === s.length - 1)) {\n        flush();\n        out.push(...parseInline(s.slice(i + 1, close === -1 ? undefined : close), true));\n        return out;\n      }\n    }\n    buf += ch;\n    i++;\n  }\n  flush();\n  return out;\n}\n\n// ---------------------------------------------------------------------------------------------------------------\n// Rendering\n// ---------------------------------------------------------------------------------------------------------------\n\nconst CSS =\n  \"@keyframes bjork-sm-caret{0%,45%{opacity:1}55%,100%{opacity:.2}}\" +\n  \"@keyframes bjork-sm-in{from{opacity:0;filter:blur(3px);transform:translateY(4px)}to{opacity:1;filter:blur(0);transform:none}}\" +\n  \".bjork-sm-caret{animation:bjork-sm-caret 1.05s ease-in-out infinite}\" +\n  \".bjork-sm-in{animation:bjork-sm-in 320ms cubic-bezier(0.23,1,0.32,1) both}\" +\n  \"@media (prefers-reduced-motion: reduce){.bjork-sm-caret{animation:none}.bjork-sm-in{animation:none}}\";\n\nfunction Caret() {\n  // 7 x 15 block; 2px down so it sits on the baseline of 14px text instead of the line box top.\n  return (\n    <span\n      aria-hidden=\"true\"\n      data-caret=\"\"\n      className=\"bjork-sm-caret ml-0.5 inline-block h-[15px] w-[7px] translate-y-[2px] rounded-[1.5px] bg-[color:var(--bjork-accent)]\"\n    />\n  );\n}\n\ninterface RenderCtx {\n  components?: StreamingMessageComponents;\n  lineNumbers: boolean;\n  onCopy?: (code: string, language: string) => void;\n}\n\nfunction renderInline(nodes: Inline[], ctx: RenderCtx, keyPrefix = \"\"): ReactNode[] {\n  return nodes.map((n, idx) => {\n    const key = `${keyPrefix}${idx}`;\n    switch (n.t) {\n      case \"text\":\n        return n.v;\n      case \"br\":\n        return <br key={key} />;\n      case \"code\":\n        return ctx.components?.inlineCode ? (\n          <span key={key}>{ctx.components.inlineCode({ children: n.v })}</span>\n        ) : (\n          <code\n            key={key}\n            className=\"rounded-[5px] border border-[color:var(--bjork-hair)] bg-[var(--bjork-field-inset)] px-[5px] py-px font-mono text-[12.5px] text-[color:var(--bjork-text)] [overflow-wrap:anywhere]\"\n          >\n            {n.v}\n          </code>\n        );\n      case \"strong\":\n        return (\n          <strong key={key} className=\"font-semibold text-[color:var(--bjork-text)]\">\n            {renderInline(n.c, ctx, `${key}.`)}\n          </strong>\n        );\n      case \"em\":\n        return (\n          <em key={key} className=\"italic\">\n            {renderInline(n.c, ctx, `${key}.`)}\n          </em>\n        );\n      case \"link\": {\n        const children = renderInline(n.c, ctx, `${key}.`);\n        if (!n.href) return <span key={key}>{children}</span>;\n        if (ctx.components?.link) return <span key={key}>{ctx.components.link({ href: n.href, children })}</span>;\n        const external = /^https?:/i.test(n.href);\n        return (\n          <a\n            key={key}\n            href={n.href}\n            target={external ? \"_blank\" : undefined}\n            rel={external ? \"noreferrer noopener\" : undefined}\n            className={cn(\n              \"rounded-[3px] font-medium text-[color:var(--bjork-accent-ink)] underline decoration-[color:var(--bjork-accent-muted)] decoration-1 underline-offset-[3px] transition-colors hover:decoration-[color:var(--bjork-accent)]\",\n              FOCUS_RING,\n            )}\n          >\n            {children}\n          </a>\n        );\n      }\n    }\n  });\n}\n\nfunction Inlines({ text, partial, ctx, caret }: { text: string; partial: boolean; ctx: RenderCtx; caret: boolean }) {\n  return (\n    <>\n      {renderInline(parseInline(text, partial), ctx)}\n      {caret && <Caret />}\n    </>\n  );\n}\n\nfunction CodeBlock({\n  code,\n  language,\n  open,\n  caret,\n  ctx,\n}: StreamingCodeProps & { caret: boolean; ctx: RenderCtx }) {\n  const [copied, setCopied] = useState(false);\n  const timer = useRef<number | undefined>(undefined);\n  useEffect(() => () => window.clearTimeout(timer.current), []);\n\n  const copy = async () => {\n    try {\n      await navigator.clipboard.writeText(code);\n    } catch {\n      return;\n    }\n    ctx.onCopy?.(code, language);\n    setCopied(true);\n    window.clearTimeout(timer.current);\n    timer.current = window.setTimeout(() => setCopied(false), 1600);\n  };\n\n  if (ctx.components?.code) return <>{ctx.components.code({ code, language, open })}</>;\n\n  const lines = code.split(\"\\n\");\n  return (\n    <figure className={cn(AI_WELL, \"my-4 overflow-hidden rounded-[10px]\")}>\n      <figcaption className=\"flex h-9 items-center justify-between gap-3 border-b border-[color:var(--bjork-border)] pl-3 pr-1\">\n        <span className=\"flex min-w-0 items-center gap-2 font-mono text-[10px] uppercase leading-none tracking-[0.08em] text-[color:var(--bjork-text-soft)]\">\n          {language || \"text\"}\n          {open && (\n            <span className=\"normal-case tracking-normal text-[color:var(--bjork-text-faint)]\">· writing</span>\n          )}\n        </span>\n        <button\n          type=\"button\"\n          onClick={copy}\n          aria-label={copied ? \"Copied\" : `Copy ${language || \"code\"}`}\n          className={cn(\n            \"flex h-7 cursor-pointer items-center gap-1.5 rounded-[7px] px-2 font-mono text-[11px] text-[color:var(--bjork-text-muted)] transition-colors hover:bg-[var(--bjork-surface-active)] hover:text-[color:var(--bjork-text)]\",\n            FOCUS_RING,\n            PRESS,\n          )}\n        >\n          <span className=\"relative grid size-3.5 place-items-center\">\n            <Copy\n              size={13}\n              strokeWidth={1.75}\n              className={cn(\"absolute transition-[background-color,border-color,color,opacity] duration-150\", copied ? \"scale-50 opacity-0\" : \"opacity-100\")}\n            />\n            <Check\n              size={13}\n              strokeWidth={2}\n              className={cn(\n                \"absolute text-[color:var(--bjork-accent-ink)] transition-[background-color,border-color,color,opacity] duration-150\",\n                copied ? \"opacity-100\" : \"scale-50 opacity-0\",\n              )}\n            />\n          </span>\n          <span className=\"min-w-[6ch] text-left\">{copied ? \"Copied\" : \"Copy\"}</span>\n        </button>\n      </figcaption>\n      <pre className=\"overflow-x-auto py-3 font-mono text-[12.5px] leading-5 text-[color:var(--bjork-text-medium)] [scrollbar-width:thin]\">\n        <code className=\"grid min-w-max\">\n          {lines.map((ln, idx) => (\n            <span key={idx} className=\"flex pr-4\">\n              {ctx.lineNumbers && (\n                <span\n                  aria-hidden=\"true\"\n                  className=\"sticky left-0 w-9 shrink-0 select-none bg-[var(--bjork-field-inset)] pr-3 text-right tabular-nums text-[color:var(--bjork-text-faint)]\"\n                >\n                  {idx + 1}\n                </span>\n              )}\n              <span className={cn(\"whitespace-pre\", !ctx.lineNumbers && \"pl-3.5\")}>\n                {ln || (idx === lines.length - 1 && caret ? \"\" : \"​\")}\n                {caret && idx === lines.length - 1 && <Caret />}\n              </span>\n            </span>\n          ))}\n        </code>\n      </pre>\n    </figure>\n  );\n}\n\nfunction ListView({ list, ctx, caretPath, partial }: { list: ListBlock; ctx: RenderCtx; caretPath: boolean; partial: boolean }) {\n  const Tag = list.ordered ? \"ol\" : \"ul\";\n  return (\n    <Tag\n      start={list.ordered && list.start !== 1 ? list.start : undefined}\n      className={cn(\"my-3 grid gap-1.5 pl-0\", list.ordered ? \"list-none\" : \"list-none\")}\n    >\n      {list.items.map((item, idx) => {\n        const last = idx === list.items.length - 1;\n        const caretHere = caretPath && last && !item.children;\n        return (\n          <li key={idx} className=\"relative pl-6\">\n            <span\n              aria-hidden=\"true\"\n              className={cn(\n                \"absolute left-0 top-0 flex h-[22px] w-[18px] items-center justify-end pr-1.5 text-[color:var(--bjork-text-soft)]\",\n                list.ordered && \"font-mono text-[12px] tabular-nums\",\n              )}\n            >\n              {list.ordered ? (\n                `${list.start + idx}.`\n              ) : (\n                <span className=\"size-[5px] rounded-full bg-[color:var(--bjork-text-faint)]\" />\n              )}\n            </span>\n            <Inlines text={item.text} partial={partial && last} ctx={ctx} caret={caretHere} />\n            {item.children && (\n              <ListView list={item.children} ctx={ctx} caretPath={caretPath && last} partial={partial && last} />\n            )}\n          </li>\n        );\n      })}\n    </Tag>\n  );\n}\n\ninterface BlockViewProps {\n  block: Block;\n  raw: string;\n  /** The last block while streaming: it tolerates half-typed markup and carries the caret. */\n  tail: boolean;\n  enter: boolean;\n  ctx: RenderCtx;\n}\n\nconst BlockView = memo(\n  function BlockView({ block, tail, enter, ctx }: BlockViewProps) {\n    const wrap = (node: ReactNode) => <div className={cn(enter && \"bjork-sm-in\")}>{node}</div>;\n    switch (block.type) {\n      case \"heading\": {\n        const cls =\n          block.level === 1\n            ? \"mt-6 mb-2 text-[19px] leading-7 font-semibold tracking-[-0.015em]\"\n            : block.level === 2\n              ? \"mt-6 mb-2 text-[16px] leading-6 font-semibold tracking-[-0.01em]\"\n              : \"mt-5 mb-1.5 text-[14px] leading-[22px] font-semibold\";\n        const H = `h${block.level + 1}` as \"h2\" | \"h3\" | \"h4\";\n        return wrap(\n          <H className={cn(cls, \"text-[color:var(--bjork-text)] first:mt-0\")}>\n            <Inlines text={block.text} partial={tail} ctx={ctx} caret={tail} />\n          </H>,\n        );\n      }\n      case \"paragraph\":\n        return wrap(\n          <p className=\"my-3\">\n            <Inlines text={block.text} partial={tail} ctx={ctx} caret={tail} />\n          </p>,\n        );\n      case \"quote\":\n        return wrap(\n          <blockquote className=\"my-4 border-l border-[color:var(--bjork-border-strong)] pl-4 text-[color:var(--bjork-text-muted)]\">\n            <Inlines text={block.text} partial={tail} ctx={ctx} caret={tail} />\n          </blockquote>,\n        );\n      case \"hr\":\n        return wrap(<hr className=\"my-6 border-0 border-t border-[color:var(--bjork-border)]\" />);\n      case \"list\":\n        return wrap(<ListView list={block} ctx={ctx} caretPath={tail} partial={tail} />);\n      case \"code\":\n        return wrap(<CodeBlock code={block.code} language={block.language} open={block.open} caret={tail && block.open} ctx={ctx} />);\n      case \"table\":\n        return wrap(\n          <div className=\"my-4 overflow-x-auto rounded-[10px] border border-[color:var(--bjork-border)]\">\n            <table className=\"w-full border-collapse text-left text-[13px] leading-5\">\n              <thead>\n                <tr className=\"border-b border-[color:var(--bjork-border)]\">\n                  {block.head.map((h, idx) => (\n                    <th\n                      key={idx}\n                      style={{ textAlign: block.align[idx] ?? \"left\" }}\n                      className=\"px-3 py-2 font-mono text-[10px] font-normal uppercase tracking-[0.08em] text-[color:var(--bjork-text-soft)]\"\n                    >\n                      {renderInline(parseInline(h, false), ctx)}\n                    </th>\n                  ))}\n                </tr>\n              </thead>\n              <tbody>\n                {block.rows.map((row, r) => (\n                  <tr key={r} className=\"border-b border-[color:var(--bjork-border)] last:border-0\">\n                    {block.head.map((_, c) => (\n                      <td key={c} style={{ textAlign: block.align[c] ?? \"left\" }} className=\"px-3 py-2 tabular-nums\">\n                        {renderInline(parseInline(row[c] ?? \"\", tail && r === block.rows.length - 1), ctx)}\n                      </td>\n                    ))}\n                  </tr>\n                ))}\n              </tbody>\n            </table>\n          </div>,\n        );\n    }\n  },\n  // Finished blocks keep the same source on every chunk, so only the tail block re-renders.\n  (a, b) => a.raw === b.raw && a.tail === b.tail && a.ctx === b.ctx,\n);\n\n// ---------------------------------------------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------------------------------------------\n\n/**\n * An assistant message that renders markdown as it streams. A small, safe parser (no HTML injection) handles\n * half-typed input, finished blocks are memoised, and a caret rides the end of the text while tokens arrive.\n */\nexport function StreamingMessage({\n  content,\n  streaming = false,\n  role = \"assistant\",\n  lineNumbers = false,\n  onCopy,\n  components,\n  label,\n  tone: toneProp,\n  className,\n}: StreamingMessageProps) {\n  const { style } = useAiTone(toneProp);\n  const reduce = useReduceMotion();\n  const blocks = useMemo(() => parseMarkdownBlocks(content, streaming), [content, streaming]);\n\n  const onCopyRef = useRef(onCopy);\n  useEffect(() => {\n    onCopyRef.current = onCopy;\n  });\n  const handleCopy = useCallback((code: string, language: string) => onCopyRef.current?.(code, language), []);\n  const ctx = useMemo<RenderCtx>(\n    () => ({ components, lineNumbers, onCopy: handleCopy }),\n    [components, lineNumbers, handleCopy],\n  );\n\n  // Announce the start and the end of a response, never individual tokens.\n  const [announce, setAnnounce] = useState({ streaming, message: \"\" });\n  if (announce.streaming !== streaming) {\n    setAnnounce({ streaming, message: streaming ? \"Response started\" : \"Response complete\" });\n  }\n\n  const isUser = role === \"user\";\n  const name = label ?? (isUser ? \"Your message\" : \"Assistant message\");\n\n  return (\n    <article\n      aria-label={name}\n      aria-busy={streaming || undefined}\n      className={cn(\n        \"@container w-full font-bjork-alpha text-[14px] leading-[22px] text-[color:var(--bjork-text-medium)] [overflow-wrap:break-word]\",\n        isUser && \"flex justify-end\",\n        className,\n      )}\n      style={style}\n    >\n      <style href=\"bjork-streaming-message\" precedence=\"default\">\n        {CSS}\n      </style>\n      <div\n        className={cn(\n          \"min-w-0 [&>div:first-child>*]:mt-0 [&>div:last-child>*]:mb-0\",\n          isUser\n            ? \"max-w-[85%] rounded-[16px] rounded-br-[6px] bg-[var(--bjork-raised)] px-3.5 py-2.5 text-[color:var(--bjork-text)]\"\n            : \"w-full\",\n        )}\n      >\n        {blocks.map((b, idx) => (\n          <BlockView\n            key={`${idx}:${b.block.type}`}\n            block={b.block}\n            raw={b.raw}\n            tail={streaming && idx === blocks.length - 1}\n            enter={streaming && !reduce}\n            ctx={ctx}\n          />\n        ))}\n        {streaming && blocks.length === 0 && (\n          <div>\n            <p className=\"my-0 h-[22px]\">\n              <Caret />\n            </p>\n          </div>\n        )}\n      </div>\n      <LiveRegion message={announce.message} />\n    </article>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/ai/streaming-message.tsx"
    },
    {
      "path": "components/bjork-ui/_core/a11y.tsx",
      "content": "import type { ReactNode } from \"react\";\n\n// Text for assistive tech only.\nexport function VisuallyHidden({ children }: { children: ReactNode }) {\n  return <span className=\"sr-only\">{children}</span>;\n}\n\n// Announces `message` when it changes. Same text re-rendered does not re-announce.\nexport function LiveRegion({\n  message,\n  politeness = \"polite\",\n}: {\n  message: string;\n  politeness?: \"polite\" | \"assertive\";\n}) {\n  return (\n    <span role=\"status\" aria-live={politeness} aria-atomic=\"true\" className=\"sr-only\">\n      {message}\n    </span>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/a11y.tsx"
    },
    {
      "path": "components/bjork-ui/ai/_shared.ts",
      "content": "\"use client\";\n\n// Shared plumbing for the bjork-ui AI components: tone tokens, reduced motion, controllable state and\n// number formatting. Every AI component imports from here so they read as one family.\n\nimport {\n  useCallback,\n  useLayoutEffect,\n  useRef,\n  useState,\n  useSyncExternalStore,\n  type CSSProperties,\n} from \"react\";\nimport { useReducedMotion } from \"framer-motion\";\nimport { BJORK_PALETTE, type BjorkTone } from \"@/components/bjork-ui/_core/palette\";\nimport { useBjorkTone } from \"@/components/bjork-ui/_core/tone\";\nimport { surfaceVars } from \"@/components/bjork-ui/_core/surface\";\n\nexport type { BjorkTone };\nexport type BjorkPalette = (typeof BJORK_PALETTE)[BjorkTone];\n\nconst noopSubscribe = () => () => {};\n\n/** False on the server and the first client render, true after hydration. */\nexport function useHydrated(): boolean {\n  return useSyncExternalStore(\n    noopSubscribe,\n    () => true,\n    () => false,\n  );\n}\n\n/** Hydration-safe reduced motion: both sides report \"not reduced\" until mount, then the real value applies. */\nexport function useReduceMotion(): boolean {\n  const hydrated = useHydrated();\n  const reduced = useReducedMotion();\n  return hydrated && reduced === true;\n}\n\n/**\n * Resolves the tone and returns the CSS custom properties for it. Spread `style` on the component root so an\n * explicit `tone` prop wins over the page theme, and status colours (success, warning, error) are always defined.\n */\nexport function useAiTone(toneProp?: BjorkTone) {\n  const tone = useBjorkTone(toneProp);\n  const pal = BJORK_PALETTE[tone];\n  const style = {\n    ...surfaceVars(tone),\n    \"--bjork-text\": pal.text,\n    \"--bjork-text-medium\": pal.textMedium,\n    \"--bjork-text-muted\": pal.textMuted,\n    \"--bjork-text-soft\": pal.textSoft,\n    \"--bjork-text-faint\": pal.textFaint,\n    \"--bjork-hair\": pal.hair,\n    \"--bjork-border\": pal.border,\n    \"--bjork-border-strong\": pal.borderStrong,\n    \"--bjork-surface\": pal.surface,\n    \"--bjork-raised\": pal.raised,\n    \"--bjork-surface-active\": pal.active,\n    \"--bjork-accent\": pal.accent,\n    \"--bjork-accent-ink\": pal.accentInk,\n    \"--bjork-accent-fill\": pal.accentFill,\n    \"--bjork-accent-soft\": pal.accentSoft,\n    \"--bjork-accent-muted\": pal.accentMuted,\n    \"--bjork-accent-foreground\": pal.accentFg,\n    \"--bjork-success\": pal.success,\n    \"--bjork-warning\": pal.warning,\n    \"--bjork-error\": pal.error,\n    \"--bjork-ring-offset\": pal.bg,\n    colorScheme: tone,\n  } as CSSProperties;\n  return { tone, pal, style };\n}\n\n/** Classes for the shared keyboard focus ring. */\nexport const FOCUS_RING =\n  \"outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--bjork-accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[color:var(--bjork-ring-offset)]\";\n\n/** Classes for the shared press feedback. */\nexport const PRESS = \"transition-transform duration-150 ease-out active:scale-[0.97] motion-reduce:transition-none\";\n\n/**\n * Controlled-or-uncontrolled state. Pass `value` to control it; `onChange` fires for every change either way.\n */\nexport function useControllable<T>(\n  value: T | undefined,\n  defaultValue: T,\n  onChange?: (next: T) => void,\n): [T, (next: T) => void] {\n  const controlled = value !== undefined;\n  const [inner, setInner] = useState(defaultValue);\n  const current = controlled ? value : inner;\n  const onChangeRef = useRef(onChange);\n  useLayoutEffect(() => {\n    onChangeRef.current = onChange;\n  });\n  const set = useCallback(\n    (next: T) => {\n      if (!controlled) setInner(next);\n      onChangeRef.current?.(next);\n    },\n    [controlled],\n  );\n  return [current, set];\n}\n\n/** 1234 -> \"1,234\", 18400 -> \"18.4k\", 1_200_000 -> \"1.2M\". */\nexport function formatTokens(n: number): string {\n  if (n < 10_000) return Math.round(n).toLocaleString(\"en-US\");\n  if (n < 1_000_000) return `${(n / 1000).toFixed(n < 100_000 ? 1 : 0).replace(/\\.0$/, \"\")}k`;\n  return `${(n / 1_000_000).toFixed(n < 10_000_000 ? 2 : 1).replace(/\\.?0+$/, \"\")}M`;\n}\n\n/** US dollars with sub-cent precision when it matters: 0.0042 -> \"$0.0042\", 12.5 -> \"$12.50\". */\nexport function formatCost(usd: number): string {\n  if (usd === 0) return \"$0.00\";\n  if (Math.abs(usd) < 0.01) return `$${usd.toFixed(4)}`;\n  if (Math.abs(usd) < 1) return `$${usd.toFixed(3)}`;\n  return `$${usd.toLocaleString(\"en-US\", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;\n}\n\n/** 640 -> \"640ms\", 4200 -> \"4.2s\", 95000 -> \"1m 35s\". */\nexport function formatDuration(ms: number): string {\n  if (ms < 1000) return `${Math.round(ms)}ms`;\n  if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;\n  const m = Math.floor(ms / 60_000);\n  const s = Math.round((ms % 60_000) / 1000);\n  return `${m}m ${s}s`;\n}\n\n/** 2_400_000 bytes -> \"2.4 MB\". */\nexport function formatBytes(bytes: number): string {\n  if (bytes < 1024) return `${bytes} B`;\n  const units = [\"KB\", \"MB\", \"GB\"];\n  let v = bytes / 1024;\n  let i = 0;\n  while (v >= 1024 && i < units.length - 1) {\n    v /= 1024;\n    i++;\n  }\n  return `${v.toFixed(v < 10 ? 1 : 0)} ${units[i]}`;\n}\n\n/** Shimmer keyframes for \"working\" text. Mount once with <style href=... precedence=\"default\">. */\nexport const SHIMMER_TEXT_CSS =\n  \"@keyframes bjork-ai-shimmer{from{background-position:100% 0}to{background-position:0% 0}}\" +\n  \".bjork-ai-shimmer{background-image:linear-gradient(90deg,var(--bjork-text-muted) 0%,var(--bjork-text-muted) 40%,var(--bjork-text) 50%,var(--bjork-text-muted) 60%,var(--bjork-text-muted) 100%);background-size:300% 100%;-webkit-background-clip:text;background-clip:text;color:transparent;animation:bjork-ai-shimmer 1.8s linear infinite}\" +\n  \"@media (prefers-reduced-motion: reduce){.bjork-ai-shimmer{animation:none;background:none;color:var(--bjork-text-muted)}}\";\n\n/** CANON §2. Panel = a component's own root surface; Card = a standalone card; Tile = a card inside a panel; Well = recessed code/field. */\nexport const AI_PANEL = \"rounded-[20px] border border-[color:var(--bjork-border-muted)] bg-[color:var(--bjork-surface)] p-4 shadow-[var(--bjork-shadow-surface)]\";\nexport const AI_CARD = \"rounded-[18px] border border-[color:var(--bjork-border-muted)] bg-[color:var(--bjork-surface)] shadow-[var(--bjork-shadow-surface)]\";\nexport const AI_TILE = \"rounded-[12px] border border-[color:var(--bjork-border-muted)] bg-[color:var(--bjork-surface)] shadow-[var(--bjork-shadow-surface)]\";\nexport const AI_WELL = \"rounded-[8px] border border-[color:var(--bjork-border)] bg-[color:var(--bjork-field-inset)] shadow-[var(--bjork-shadow-inset)]\";\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/ai/_shared.ts"
    },
    {
      "path": "components/bjork-ui/_core/palette.ts",
      "content": "export type BjorkTone = \"dark\" | \"light\";\n\n// Canvas, WebGL and SVG attribute colours. DOM code uses the CSS tokens with these as fallbacks.\nexport const BJORK_PALETTE = {\n  dark: {\n    bg: \"#050505\", stage: \"#111111\", surface: \"#121212\", raised: \"#161616\", active: \"#202020\",\n    border: \"#232323\", borderStrong: \"#343434\",\n    text: \"#ededed\", textMedium: \"rgba(237,237,237,0.72)\", textMuted: \"rgba(237,237,237,0.6)\",\n    textSoft: \"rgba(237,237,237,0.5)\", textFaint: \"rgba(237,237,237,0.22)\", hair: \"rgba(237,237,237,0.12)\",\n    accent: \"#ec5c13\", accentInk: \"#ec5c13\", accentFill: \"#b84a12\", accentSoft: \"rgba(236,92,19,0.12)\", accentMuted: \"rgba(236,92,19,0.24)\",\n    accentFg: \"#fff2ea\", success: \"#4cc38a\", warning: \"#f2b544\", error: \"#ff5c4d\",\n  },\n  light: {\n    bg: \"#f7f5ef\", stage: \"#f7f5ef\", surface: \"#fffcf6\", raised: \"#f5efe3\", active: \"#efe7d8\",\n    border: \"#eee6db\", borderStrong: \"#e1d7c8\",\n    text: \"#171717\", textMedium: \"rgba(23,23,23,0.78)\", textMuted: \"rgba(23,23,23,0.68)\",\n    textSoft: \"rgba(23,23,23,0.62)\", textFaint: \"rgba(23,23,23,0.22)\", hair: \"rgba(23,23,23,0.10)\",\n    accent: \"#ec7d43\", accentInk: \"#b4531f\", accentFill: \"#ec7d43\", accentSoft: \"rgba(236,125,67,0.10)\", accentMuted: \"rgba(236,125,67,0.20)\",\n    accentFg: \"#3f2112\", success: \"#1f8a55\", warning: \"#a86b00\", error: \"#c8361f\",\n  },\n} as const;\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/palette.ts"
    },
    {
      "path": "components/bjork-ui/_core/tone.ts",
      "content": "\"use client\";\n\nimport { useSyncExternalStore } from \"react\";\nimport { useTheme } from \"next-themes\";\nimport type { BjorkTone } from \"./palette\";\n\nconst subscribeNoop = () => () => {};\nconst getMounted = () => true;\nconst getServerMounted = () => false;\n\n/**\n * Resolves the tone for a component. An explicit `tone` always wins.\n * Otherwise it follows next-themes' resolved theme, but only after mount,\n * so the server render and first client render agree on dark.\n */\nexport function useBjorkTone(tone?: BjorkTone): BjorkTone {\n  const { resolvedTheme } = useTheme();\n  const mounted = useSyncExternalStore(subscribeNoop, getMounted, getServerMounted);\n  if (tone) return tone;\n  if (!mounted) return \"dark\";\n  return resolvedTheme === \"light\" ? \"light\" : \"dark\";\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/tone.ts"
    },
    {
      "path": "components/bjork-ui/_core/surface.ts",
      "content": "import type { CSSProperties } from \"react\";\nimport type { BjorkTone } from \"./palette\";\n\n// The library surface (CANON.md §1). Values mirror app/globals.css :root and .dark. Change both together.\nexport const BJORK_SURFACE = {\n  light: {\n    surface: \"#fffcf6\", surfaceMuted: \"rgba(250, 246, 237, 0.94)\", surfaceHover: \"#f5efe3\", surfaceActive: \"#efe7d8\",\n    panel: \"#f8f2e7\", menu: \"rgba(255, 252, 246, 0.98)\", field: \"rgba(255, 252, 246, 0.96)\",\n    fieldMuted: \"rgba(248, 242, 231, 0.82)\", fieldInset: \"rgba(239, 231, 216, 0.72)\",\n    border: \"#eee6db\", borderMuted: \"#f5ede2\", borderStrong: \"#e1d7c8\",\n    shadowSurface: \"inset 0 7px 14px rgba(88, 72, 49, 0.045), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.92), inset 1px 0 0 rgba(88, 72, 49, 0.026), inset -1px 0 0 rgba(255, 255, 255, 0.68), 0 14px 22px -9px rgba(66, 52, 33, 0.11)\",\n    shadowSoft: \"inset 0 1px 0 rgba(88, 72, 49, 0.045), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.86)\",\n    shadowPanel: \"inset 0 1px 0 rgba(88, 72, 49, 0.04), inset 1px 0 0 rgba(88, 72, 49, 0.024), inset -1px 0 0 rgba(255, 255, 255, 0.64), 0 18px 38px -30px rgba(66, 52, 33, 0.2)\",\n    shadowMenu: \"inset 0 1px 0 rgba(88, 72, 49, 0.04), inset 0 12px 24px rgba(88, 72, 49, 0.022), inset 1px 0 0 rgba(88, 72, 49, 0.024), inset -1px 0 0 rgba(255, 255, 255, 0.62), 0 22px 44px -26px rgba(66, 52, 33, 0.22)\",\n    shadowInset: \"inset 0 1px 10px rgba(88, 72, 49, 0.12), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.72)\",\n  },\n  dark: {\n    surface: \"#121212\", surfaceMuted: \"rgba(22, 22, 22, 0.92)\", surfaceHover: \"#161616\", surfaceActive: \"#202020\",\n    panel: \"#0d0d0d\", menu: \"rgba(18, 18, 18, 0.98)\", field: \"rgba(18, 18, 18, 0.95)\",\n    fieldMuted: \"rgba(18, 18, 18, 0.55)\", fieldInset: \"rgba(9, 9, 9, 0.75)\",\n    border: \"#232323\", borderMuted: \"#1c1c1c\", borderStrong: \"#343434\",\n    shadowSurface: \"inset 0 7px 14px rgba(255, 255, 255, 0.03), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.06), 0 14px 20px -6px rgba(0, 0, 0, 0.45)\",\n    shadowSoft: \"inset 0 1px 0 rgba(255, 255, 255, 0.045)\",\n    shadowPanel: \"inset 0 1px 0 rgba(255, 255, 255, 0.035), 0 18px 36px -28px rgba(0, 0, 0, 0.9)\",\n    shadowMenu: \"inset 0 1px 0 rgba(255, 255, 255, 0.055), inset 0 12px 24px rgba(255, 255, 255, 0.018), inset 0 -18px 26px rgba(0, 0, 0, 0.24), 0 22px 42px -22px rgba(0, 0, 0, 0.9)\",\n    shadowInset: \"inset 0 1px 10px rgba(0, 0, 0, 0.45)\",\n  },\n} as const;\n\n/** Writes the surface tokens for a pinned tone as --bjork-* custom properties. Spread first; kit-specific keys may override. */\nexport function surfaceVars(tone: BjorkTone): CSSProperties {\n  const s = BJORK_SURFACE[tone];\n  return {\n    \"--bjork-surface\": s.surface, \"--bjork-surface-muted\": s.surfaceMuted, \"--bjork-surface-hover\": s.surfaceHover,\n    \"--bjork-surface-active\": s.surfaceActive, \"--bjork-panel\": s.panel, \"--bjork-menu\": s.menu, \"--bjork-field\": s.field,\n    \"--bjork-field-muted\": s.fieldMuted, \"--bjork-field-inset\": s.fieldInset, \"--bjork-border\": s.border,\n    \"--bjork-border-muted\": s.borderMuted, \"--bjork-border-strong\": s.borderStrong,\n    \"--bjork-shadow-surface\": s.shadowSurface, \"--bjork-shadow-soft\": s.shadowSoft, \"--bjork-shadow-panel\": s.shadowPanel,\n    \"--bjork-shadow-menu\": s.shadowMenu, \"--bjork-shadow-inset\": s.shadowInset,\n  } as CSSProperties;\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/surface.ts"
    },
    {
      "path": "lib/utils.ts",
      "content": "import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n",
      "type": "registry:lib",
      "target": "@lib/utils.ts"
    }
  ],
  "cssVars": {
    "light": {
      "bjork-bg": "#f7f3ea",
      "bjork-surface": "#fffcf6",
      "bjork-surface-muted": "rgba(250, 246, 237, 0.94)",
      "bjork-surface-hover": "#f5efe3",
      "bjork-surface-active": "#efe7d8",
      "bjork-panel": "#f8f2e7",
      "bjork-menu": "rgba(255, 252, 246, 0.98)",
      "bjork-field": "rgba(255, 252, 246, 0.96)",
      "bjork-field-muted": "rgba(248, 242, 231, 0.82)",
      "bjork-field-inset": "rgba(239, 231, 216, 0.72)",
      "bjork-error-bg": "#fff0e8",
      "bjork-text": "#171717",
      "bjork-text-strong": "rgba(23, 23, 23, 0.9)",
      "bjork-text-medium": "rgba(23, 23, 23, 0.78)",
      "bjork-text-muted": "rgba(23, 23, 23, 0.68)",
      "bjork-text-soft": "rgba(23, 23, 23, 0.62)",
      "bjork-text-faint": "rgba(23, 23, 23, 0.22)",
      "bjork-inverted-text": "#fff8f0",
      "bjork-border": "#eee6db",
      "bjork-border-muted": "#f5ede2",
      "bjork-border-strong": "#e1d7c8",
      "bjork-thumb": "#a49b8e",
      "bjork-track": "#ded6ca",
      "bjork-accent": "#ec7d43",
      "bjork-accent-hover": "#f0935f",
      "bjork-accent-soft": "rgba(236, 125, 67, 0.1)",
      "bjork-accent-muted": "rgba(236, 125, 67, 0.2)",
      "bjork-accent-badge": "rgba(236, 125, 67, 0.16)",
      "bjork-accent-badge-foreground": "#8d421d",
      "bjork-accent-foreground": "#3f2112",
      "bjork-accent-fill": "#ec7d43",
      "bjork-accent-fill-hover": "#f0935f",
      "bjork-shadow-surface": "inset 0 7px 14px rgba(88, 72, 49, 0.045), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.92), inset 1px 0 0 rgba(88, 72, 49, 0.026), inset -1px 0 0 rgba(255, 255, 255, 0.68), 0 14px 22px -9px rgba(66, 52, 33, 0.11)",
      "bjork-shadow-soft": "inset 0 1px 0 rgba(88, 72, 49, 0.045), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.86)",
      "bjork-shadow-panel": "inset 0 1px 0 rgba(88, 72, 49, 0.04), inset 1px 0 0 rgba(88, 72, 49, 0.024), inset -1px 0 0 rgba(255, 255, 255, 0.64), 0 18px 38px -30px rgba(66, 52, 33, 0.2)",
      "bjork-shadow-menu": "inset 0 1px 0 rgba(88, 72, 49, 0.04), inset 0 12px 24px rgba(88, 72, 49, 0.022), inset 1px 0 0 rgba(88, 72, 49, 0.024), inset -1px 0 0 rgba(255, 255, 255, 0.62), 0 22px 44px -26px rgba(66, 52, 33, 0.22)",
      "bjork-shadow-inset": "inset 0 1px 10px rgba(88, 72, 49, 0.12), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.72)",
      "bjork-ring-offset": "#f7f3ea"
    },
    "dark": {
      "bjork-bg": "#111111",
      "bjork-surface": "#121212",
      "bjork-surface-muted": "rgba(22, 22, 22, 0.92)",
      "bjork-surface-hover": "#161616",
      "bjork-surface-active": "#202020",
      "bjork-panel": "#0d0d0d",
      "bjork-menu": "rgba(18, 18, 18, 0.98)",
      "bjork-field": "rgba(18, 18, 18, 0.95)",
      "bjork-field-muted": "rgba(18, 18, 18, 0.55)",
      "bjork-field-inset": "rgba(9, 9, 9, 0.75)",
      "bjork-error-bg": "#130d0a",
      "bjork-text": "#ededed",
      "bjork-text-strong": "rgba(237, 237, 237, 0.9)",
      "bjork-text-medium": "rgba(237, 237, 237, 0.72)",
      "bjork-text-muted": "rgba(237, 237, 237, 0.6)",
      "bjork-text-soft": "rgba(237, 237, 237, 0.5)",
      "bjork-text-faint": "rgba(237, 237, 237, 0.22)",
      "bjork-inverted-text": "#080808",
      "bjork-border": "#232323",
      "bjork-border-muted": "#1c1c1c",
      "bjork-border-strong": "#343434",
      "bjork-thumb": "#626262",
      "bjork-track": "#090909",
      "bjork-accent": "#ec5c13",
      "bjork-accent-hover": "#f07832",
      "bjork-accent-soft": "rgba(236, 92, 19, 0.12)",
      "bjork-accent-muted": "rgba(236, 92, 19, 0.24)",
      "bjork-accent-badge": "#b84a12",
      "bjork-accent-badge-foreground": "#fff2ea",
      "bjork-accent-foreground": "#fff2ea",
      "bjork-accent-fill": "#b84a12",
      "bjork-accent-fill-hover": "#a84a14",
      "bjork-shadow-surface": "inset 0 7px 14px rgba(255, 255, 255, 0.03), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.06), 0 14px 20px -6px rgba(0, 0, 0, 0.45)",
      "bjork-shadow-soft": "inset 0 1px 0 rgba(255, 255, 255, 0.045)",
      "bjork-shadow-panel": "inset 0 1px 0 rgba(255, 255, 255, 0.035), 0 18px 36px -28px rgba(0, 0, 0, 0.9)",
      "bjork-shadow-menu": "inset 0 1px 0 rgba(255, 255, 255, 0.055), inset 0 12px 24px rgba(255, 255, 255, 0.018), inset 0 -18px 26px rgba(0, 0, 0, 0.24), 0 22px 42px -22px rgba(0, 0, 0, 0.9)",
      "bjork-shadow-inset": "inset 0 1px 10px rgba(0, 0, 0, 0.45)",
      "bjork-ring-offset": "#080808"
    }
  },
  "css": {
    ".bjork-range": {
      "box-shadow": "inset 0 1px 1px rgba(255, 255, 255, 0.04), 0 0 14px rgba(189, 69, 20, 0.06)"
    },
    ".bjork-range::-webkit-slider-thumb": {
      "appearance": "none",
      "width": "16px",
      "height": "8px",
      "border": "0",
      "border-radius": "999px",
      "background": "var(--bjork-thumb)",
      "box-shadow": "inset 0 1px 0 rgba(255, 255, 255, 0.18), 0 0 0 1px rgba(0, 0, 0, 0.42)"
    },
    ".bjork-range::-moz-range-thumb": {
      "width": "16px",
      "height": "8px",
      "border": "0",
      "border-radius": "999px",
      "background": "var(--bjork-thumb)",
      "box-shadow": "inset 0 1px 0 rgba(255, 255, 255, 0.18), 0 0 0 1px rgba(0, 0, 0, 0.42)"
    },
    ".bjork-layered-button-accent, .bjork-layered-button-raised": {
      "position": "relative",
      "isolation": "isolate",
      "appearance": "none",
      "overflow": "hidden",
      "border": "0",
      "border-radius": "13px",
      "box-shadow": "inset 0 1px 0 #ffffff38, 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a",
      "transition": "filter 150ms ease-out, box-shadow 150ms ease-out"
    },
    ".bjork-layered-button-accent::after, .bjork-layered-button-raised::after": {
      "position": "absolute",
      "inset": "0",
      "pointer-events": "none",
      "content": "\"\"",
      "border": "1.5px solid transparent",
      "border-radius": "inherit",
      "background": "linear-gradient(#ffffffb8, #0000003d 41% 75%, #ffffff47) border-box",
      "mix-blend-mode": "overlay",
      "-webkit-mask-image": "linear-gradient(#000, #000), linear-gradient(#000, #000)",
      "mask-image": "linear-gradient(#000, #000), linear-gradient(#000, #000)",
      "-webkit-mask-clip": "padding-box, border-box",
      "mask-clip": "padding-box, border-box",
      "-webkit-mask-origin": "padding-box, border-box",
      "mask-origin": "padding-box, border-box",
      "-webkit-mask-composite": "xor",
      "mask-composite": "exclude"
    },
    ".bjork-layered-button-accent": {
      "color": "oklch(100% 0 0)",
      "text-shadow": "0 -1px #00000040",
      "background": "radial-gradient(ellipse at -20px top, rgba(255, 255, 255, 0.14), transparent), linear-gradient(180deg, #ec5c13 0%, #ec5c13 62%, color-mix(in oklab, #ec5c13 84%, black 16%) 100%)"
    },
    ".bjork-layered-button-raised": {
      "color": "oklch(100% 0 0)",
      "text-shadow": "0 -1px #00000026",
      "background": "radial-gradient(ellipse at -20px top, #ffffff38, #fff0), linear-gradient(180deg, oklch(0.1881 0.006 265), oklch(0.5493 0.0081 265))"
    },
    ".dark .bjork-layered-button-raised": {
      "color": "oklch(0.2574 0.0056 265)",
      "text-shadow": "0 -1px #00000026",
      "background": "radial-gradient(ellipse at -20px top, #ffffff38, #fff0), linear-gradient(180deg, color-mix(in oklab, oklch(0.9674 0.0013 265) 80%, oklch(0.2574 0.0056 265) 20%), color-mix(in oklab, oklch(0.669 0.0107 265) 80%, oklch(0.2574 0.0056 265) 20%))"
    },
    "@media (hover: hover)": {
      ".bjork-layered-button-accent:hover": {
        "filter": "brightness(1.08) saturate(1.04)"
      },
      ".bjork-layered-button-raised:hover": {
        "filter": "brightness(1.08) contrast(0.9)"
      }
    }
  },
  "meta": {
    "collection": "AI",
    "registryUrl": "https://ui.isaiahbjork.com/streaming-message.json",
    "installCommand": "npx shadcn@latest add https://ui.isaiahbjork.com/streaming-message.json"
  },
  "type": "registry:component"
}