{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ansi-log",
  "title": "ANSI Log",
  "description": "Renders raw terminal output — escape sequences and all — as styled, theme-aware HTML. Reach for it wherever a process's own output has to be shown inside a page: CI and build logs, deploy and release output, npm/pnpm/cargo/docker build output piped into a dashboard, test-runner results, job and worker logs in an admin panel, an agent or LLM tool-call transcript, git and lint output in a code-review UI, or the output pane of a web terminal. Common asks it answers: \"ansi to html react\", \"render ANSI colours in the browser\", \"ci log viewer component\", \"terminal output component react\", \"build log with colours\", \"convert ANSI escape codes\", \"shadcn log viewer\", \"docker logs in a web UI\", \"colored console output in React\". It handles the parts a hand-rolled converter gets wrong. A carriage return moves the cursor instead of breaking the line, so a progress bar that redraws itself stays one line reading \"100%\" rather than turning into a hundred lines of noise — and the tail a shorter redraw does not cover survives, exactly as on a real terminal. Erase-in-line (all three modes), backspace, the 16 named colours, the full 256-colour palette including the 6x6x6 cube and the 24-step grey ramp, 24-bit truecolor, both the semicolon and the colon spelling of extended colour that libvte and kitty emit, bold, dim, italic, underline, strike and reverse video. Every sequence it does not implement — cursor moves, hide-cursor, alternate-screen, window-title OSC — is consumed rather than printed as visible gibberish, which is the usual failure of a parser that only knows about the colour sequence. OSC 8 hyperlinks keep their label and drop their target deliberately: a URL in a log is exactly as attacker-supplied as the log is, and turning it into a live anchor would put javascript: one click away. Colour follows your theme instead of a fixed terminal palette — the 16 named colours are light/dark pairs chosen against the panel, and backgrounds are drawn as a translucent wash rather than a solid block, so text can never land on a saturated slab below contrast in one theme or the other. The scroll region takes keyboard focus, since a log that only scrolls with a mouse puts the right-hand end of every long line out of reach. The optional line-number gutter is not selectable, so dragging across the log copies the log and not a column of numbers, and maxLines keeps the end of the log — the failure is at the bottom — while saying out loud how many earlier lines it dropped instead of quietly presenting a suffix as the whole thing. No dependencies and no hooks, so it renders inside a React server component with no \"use client\" of its own and ships no client JavaScript. shadcn/ui has nothing of the kind: there is no log, terminal or ANSI item in its registry, and a plain code block shows escape sequences as literal characters.",
  "dependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/ui/ansi-log.tsx",
      "content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\ninterface AnsiLogProps\n  extends Omit<React.ComponentPropsWithoutRef<\"div\">, \"children\"> {\n  /** Raw output, escape sequences and all — exactly what the process wrote. */\n  text: string\n  /** Draw a line-number gutter. It is not selectable, so copying gives the log alone. */\n  showLineNumbers?: boolean\n  /** Keep at most this many lines, from the end (default 5000). A dropped-lines notice is shown. */\n  maxLines?: number\n  /** Wrap long lines instead of scrolling them horizontally (default false). */\n  wrap?: boolean\n  /** Accessible name of the scrollable region (default \"Log output\"). */\n  label?: string\n}\n\n/** 0–7 are the standard colours, 8–15 their bright variants. */\ntype AnsiColor =\n  | { kind: \"index\"; index: number }\n  | { kind: \"rgb\"; r: number; g: number; b: number }\n\ninterface AnsiStyle {\n  fg: AnsiColor | null\n  bg: AnsiColor | null\n  bold: boolean\n  dim: boolean\n  italic: boolean\n  underline: boolean\n  inverse: boolean\n  strike: boolean\n}\n\ninterface AnsiSpan {\n  text: string\n  style: AnsiStyle\n}\n\nconst PLAIN: AnsiStyle = {\n  fg: null,\n  bg: null,\n  bold: false,\n  dim: false,\n  italic: false,\n  underline: false,\n  inverse: false,\n  strike: false,\n}\n\n/**\n * Foreground tones, ANSI order: black, red, green, yellow, blue, magenta, cyan, white, then the\n * eight bright variants. Every one is a light/dark pair chosen against the panel behind it, which\n * is what makes the whole table safe: the text colour is the *only* thing that ever decides\n * legibility here, because backgrounds below are washes rather than solid blocks. ANSI \"white\" is\n * really light grey and ANSI \"bright black\" is really the dimmed-text colour, so those two borrow\n * the theme's own muted token rather than pretending to be white and black.\n */\nconst FG_CLASS = [\n  \"text-neutral-800 dark:text-neutral-400\",\n  \"text-red-600 dark:text-red-400\",\n  \"text-emerald-600 dark:text-emerald-400\",\n  \"text-amber-600 dark:text-amber-400\",\n  \"text-blue-600 dark:text-blue-400\",\n  \"text-fuchsia-600 dark:text-fuchsia-400\",\n  \"text-cyan-600 dark:text-cyan-400\",\n  \"text-neutral-500 dark:text-neutral-300\",\n  \"text-muted-foreground\",\n  \"text-red-500 dark:text-red-300\",\n  \"text-emerald-500 dark:text-emerald-300\",\n  \"text-amber-500 dark:text-amber-300\",\n  \"text-blue-500 dark:text-blue-300\",\n  \"text-fuchsia-500 dark:text-fuchsia-300\",\n  \"text-cyan-500 dark:text-cyan-300\",\n  \"text-foreground\",\n]\n\n/**\n * Backgrounds as a translucent wash of the hue instead of the solid block a terminal paints. A\n * terminal owns the whole surface and can pair any background with any foreground; a log panel\n * sits inside a themed page, and a solid saturated block there both fights the page and drags the\n * text on top of it below contrast — 16 foregrounds against 16 backgrounds is 256 pairs, and\n * several of them fail in one theme or the other. A wash cannot: whatever is on top keeps its\n * contrast against the panel, so the one invariant above holds for every combination.\n */\nconst BG_WASH_CLASS = [\n  \"bg-neutral-500/20 dark:bg-neutral-400/25\",\n  \"bg-red-500/20 dark:bg-red-400/25\",\n  \"bg-emerald-500/20 dark:bg-emerald-400/25\",\n  \"bg-amber-500/20 dark:bg-amber-400/25\",\n  \"bg-blue-500/20 dark:bg-blue-400/25\",\n  \"bg-fuchsia-500/20 dark:bg-fuchsia-400/25\",\n  \"bg-cyan-500/20 dark:bg-cyan-400/25\",\n  \"bg-neutral-400/20 dark:bg-neutral-300/25\",\n  \"bg-muted\",\n  \"bg-red-400/25 dark:bg-red-300/25\",\n  \"bg-emerald-400/25 dark:bg-emerald-300/25\",\n  \"bg-amber-400/25 dark:bg-amber-300/25\",\n  \"bg-blue-400/25 dark:bg-blue-300/25\",\n  \"bg-fuchsia-400/25 dark:bg-fuchsia-300/25\",\n  \"bg-cyan-400/25 dark:bg-cyan-300/25\",\n  \"bg-foreground/15\",\n]\n\n/**\n * The solid pair, used only for reverse video (SGR 7), where a wash would not read as inverted at\n * all. Tones are picked so the theme's `background` token — near-white in light, near-black in\n * dark — clears 4.5:1 on top of them in both directions, which is why light mode reaches for the\n * 600/700 end and dark mode for 300/400.\n */\nconst BG_SOLID_CLASS = [\n  \"bg-neutral-700 dark:bg-neutral-400\",\n  \"bg-red-700 dark:bg-red-400\",\n  \"bg-emerald-700 dark:bg-emerald-400\",\n  \"bg-amber-700 dark:bg-amber-400\",\n  \"bg-blue-700 dark:bg-blue-400\",\n  \"bg-fuchsia-700 dark:bg-fuchsia-400\",\n  \"bg-cyan-700 dark:bg-cyan-400\",\n  \"bg-neutral-500 dark:bg-neutral-300\",\n  \"bg-muted-foreground\",\n  \"bg-red-600 dark:bg-red-300\",\n  \"bg-emerald-600 dark:bg-emerald-300\",\n  \"bg-amber-600 dark:bg-amber-300\",\n  \"bg-blue-600 dark:bg-blue-300\",\n  \"bg-fuchsia-600 dark:bg-fuchsia-300\",\n  \"bg-cyan-600 dark:bg-cyan-300\",\n  \"bg-foreground\",\n]\n\n/** The six levels of the xterm 6×6×6 colour cube, and the start/step of its 24-step grey ramp. */\nconst CUBE_LEVELS = [0, 95, 135, 175, 215, 255]\nconst GREY_BASE = 8\nconst GREY_STEP = 10\n\nconst isFinalByte = (ch: string) => ch >= \"@\" && ch <= \"~\"\n\nconst clampByte = (n: number) => (n < 0 ? 0 : n > 255 ? 255 : Math.trunc(n))\n\n/**\n * An index into the 256-colour palette. 0–15 stay symbolic so they follow the theme through the\n * tables above; everything past that is a fixed point in the cube or the grey ramp and can only be\n * carried through literally.\n */\nfunction paletteColor(index: number): AnsiColor | null {\n  if (!Number.isInteger(index) || index < 0 || index > 255) return null\n  if (index < 16) return { kind: \"index\", index }\n  if (index < 232) {\n    // The index is a three-digit base-6 number, most significant digit first. Red is that leading\n    // digit and cannot exceed 5 on its own, which is why it is the one channel with no modulus.\n    const n = index - 16\n    return {\n      kind: \"rgb\",\n      r: CUBE_LEVELS[Math.floor(n / 36)],\n      g: CUBE_LEVELS[Math.floor(n / 6) % 6],\n      b: CUBE_LEVELS[n % 6],\n    }\n  }\n  const v = GREY_BASE + (index - 232) * GREY_STEP\n  return { kind: \"rgb\", r: v, g: v, b: v }\n}\n\nconst toNumber = (part: string) => {\n  if (part === \"\") return 0 // An omitted parameter means zero, so `ESC[;31m` resets then reddens.\n  return /^\\d+$/.test(part) ? Number(part) : NaN\n}\n\n/**\n * The extended-colour argument of SGR 38/48, in either spelling: `38;5;n` / `38;2;r;g;b` with\n * semicolons, or `38:5:n` / `38:2:r:g:b` with colons. The colon form is the one the standard\n * actually specifies, it is what libvte and kitty emit, and it may carry an empty colour-space slot\n * (`38:2::r:g:b`) — so the RGB triple is read from the end rather than from a fixed offset.\n */\nfunction extendedColor(args: string[]): AnsiColor | null {\n  const mode = toNumber(args[0] ?? \"\")\n  if (mode === 5) return paletteColor(toNumber(args[1] ?? \"\"))\n  if (mode === 2) {\n    const nums = args.slice(1).map(toNumber)\n    const rgb = nums.slice(-3)\n    if (rgb.length < 3 || rgb.some((n) => !Number.isFinite(n))) return null\n    return { kind: \"rgb\", r: clampByte(rgb[0]), g: clampByte(rgb[1]), b: clampByte(rgb[2]) }\n  }\n  return null\n}\n\n/** Applies one SGR sequence's parameters to a style, returning the new style. */\nfunction applySgr(style: AnsiStyle, params: string): AnsiStyle {\n  const parts = params === \"\" ? [\"0\"] : params.split(\";\")\n  const next = { ...style }\n  for (let i = 0; i < parts.length; i++) {\n    const part = parts[i]\n    // A colon-delimited parameter is self-contained: it carries its own arguments, so it is read\n    // whole and does not consume the parameters that follow it.\n    if (part.includes(\":\")) {\n      const sub = part.split(\":\")\n      const lead = toNumber(sub[0])\n      if (lead === 38 || lead === 48) {\n        const color = extendedColor(sub.slice(1))\n        if (color) {\n          if (lead === 38) next.fg = color\n          else next.bg = color\n        }\n      }\n      continue\n    }\n    const n = toNumber(part)\n    if (!Number.isFinite(n)) continue\n    if (n === 38 || n === 48) {\n      // Semicolon form: the arguments are the parameters that follow, and they are consumed here\n      // so that a trailing `1` in `38;5;1;1m` is read as bold rather than as another colour.\n      const isTruecolor = toNumber(parts[i + 1] ?? \"\") === 2\n      const args = parts.slice(i + 1, i + (isTruecolor ? 5 : 3))\n      const color = extendedColor(args)\n      if (color) {\n        if (n === 38) next.fg = color\n        else next.bg = color\n      }\n      i += args.length\n      continue\n    }\n    if (n === 0) {\n      next.fg = null\n      next.bg = null\n      next.bold = false\n      next.dim = false\n      next.italic = false\n      next.underline = false\n      next.inverse = false\n      next.strike = false\n    } else if (n === 1) next.bold = true\n    else if (n === 2) next.dim = true\n    else if (n === 3) next.italic = true\n    else if (n === 4) next.underline = true\n    else if (n === 7) next.inverse = true\n    else if (n === 9) next.strike = true\n    // 21 is \"doubly underlined\" in the standard and \"bold off\" in most terminals; both readings\n    // end with less emphasis than before, so it is treated as the latter.\n    else if (n === 21 || n === 22) {\n      next.bold = false\n      next.dim = false\n    } else if (n === 23) next.italic = false\n    else if (n === 24) next.underline = false\n    else if (n === 27) next.inverse = false\n    else if (n === 29) next.strike = false\n    else if (n >= 30 && n <= 37) next.fg = { kind: \"index\", index: n - 30 }\n    else if (n === 39) next.fg = null\n    else if (n >= 40 && n <= 47) next.bg = { kind: \"index\", index: n - 40 }\n    else if (n === 49) next.bg = null\n    else if (n >= 90 && n <= 97) next.fg = { kind: \"index\", index: n - 90 + 8 }\n    else if (n >= 100 && n <= 107) next.bg = { kind: \"index\", index: n - 100 + 8 }\n  }\n  return next\n}\n\nconst sameColor = (a: AnsiColor | null, b: AnsiColor | null) => {\n  if (a === b) return true\n  if (!a || !b || a.kind !== b.kind) return false\n  if (a.kind === \"index\" && b.kind === \"index\") return a.index === b.index\n  if (a.kind === \"rgb\" && b.kind === \"rgb\") return a.r === b.r && a.g === b.g && a.b === b.b\n  return false\n}\n\n/**\n * Compared field by field rather than by reference: `ESC[31m…ESC[0m…ESC[31m` builds two separate\n * style objects that mean the same thing, and comparing references would emit two adjacent spans\n * that a reader cannot tell apart but a diff of the markup can.\n */\nconst sameStyle = (a: AnsiStyle, b: AnsiStyle) =>\n  a.bold === b.bold &&\n  a.dim === b.dim &&\n  a.italic === b.italic &&\n  a.underline === b.underline &&\n  a.inverse === b.inverse &&\n  a.strike === b.strike &&\n  sameColor(a.fg, b.fg) &&\n  sameColor(a.bg, b.bg)\n\n/**\n * Parses raw output into lines of styled spans.\n *\n * The line is held as one cell per column rather than as a growing string, because a carriage\n * return is an instruction to move the cursor, not to start a new line: `50%\\r100%` is one line\n * reading \"100%\", and `Downloading\\rDone` is one line reading \"Donenoading\" on a real terminal —\n * the tail that the shorter write did not cover survives. Progress bars, spinners and `docker\n * pull` all lean on exactly that, and a parser that treats `\\r` as \"discard the line so far\" or,\n * worse, as a line break turns a tidy one-line progress bar into a hundred lines of noise.\n */\nfunction parseAnsi(input: string): AnsiSpan[][] {\n  const lines: AnsiSpan[][] = []\n  let chars: string[] = []\n  let styles: AnsiStyle[] = []\n  let column = 0\n  let style = PLAIN\n  // Whether the line has been drawn on at all. Asking \"are there any cells left?\" instead would\n  // conflate a line that was never started with one whose content was erased — a progress bar\n  // ending in `\\r ESC[K` and no newline would leave a line on screen and no line in the output.\n  let touched = false\n\n  const flush = () => {\n    const spans: AnsiSpan[] = []\n    for (let i = 0; i < chars.length; i++) {\n      const last = spans[spans.length - 1]\n      if (last && sameStyle(styles[i], last.style)) last.text += chars[i]\n      else spans.push({ text: chars[i], style: styles[i] })\n    }\n    lines.push(spans)\n    chars = []\n    styles = []\n    column = 0\n    touched = false\n  }\n\n  // The cursor never sits past the end of the line: it only moves right by writing, which extends\n  // the line under it, and the one operation that shortens a line — erase-to-end, below — cuts it\n  // off exactly at the cursor. So this can always assign in place, and there is never a hole to\n  // backfill. (The gap-fill that used to stand here was unreachable on 200,000 randomised inputs\n  // of returns, backspaces and all three erase modes.)\n  const write = (ch: string) => {\n    touched = true\n    chars[column] = ch\n    styles[column] = style\n    column++\n  }\n\n  const eraseInLine = (params: string) => {\n    touched = true\n    const mode = toNumber(params.split(\";\")[0] ?? \"\")\n    if (mode === 0) {\n      // To the end of the line. Trailing cells are simply dropped: nothing is drawn to the right\n      // edge here the way a terminal paints the background out to its own width.\n      chars.length = column\n      styles.length = column\n    } else if (mode === 1 || mode === 2) {\n      const to = mode === 2 ? chars.length : Math.min(column + 1, chars.length)\n      for (let i = 0; i < to; i++) {\n        chars[i] = \" \"\n        styles[i] = PLAIN\n      }\n    }\n  }\n\n  const text = typeof input === \"string\" ? input : \"\"\n\n  for (let i = 0; i < text.length; ) {\n    const ch = text[i]\n\n    if (ch === \"\\x1b\") {\n      const next = text[i + 1]\n      if (next === \"[\") {\n        // A control sequence runs until its final byte; the parameter and intermediate bytes in\n        // between are what varies. Scanning for that byte is what keeps an unsupported sequence —\n        // a cursor move, a colour query, `ESC[?25l` to hide the cursor — from being printed as\n        // visible gibberish, which is the failure mode of a parser that only knows about `m`.\n        let j = i + 2\n        while (j < text.length && !isFinalByte(text[j])) j++\n        if (j >= text.length) break // Truncated at the end of a chunk: there is nothing to draw.\n        const params = text.slice(i + 2, j)\n        if (text[j] === \"m\") style = applySgr(style, params)\n        else if (text[j] === \"K\") eraseInLine(params)\n        i = j + 1\n        continue\n      }\n      if (next === \"]\") {\n        // An operating-system command — a window title, or OSC 8's hyperlinks. The payload is\n        // dropped rather than rendered: the link target in an OSC 8 sequence is attacker-supplied\n        // whenever the log is, and turning it into a live anchor would put `javascript:` one build\n        // step away from a click.\n        let j = i + 2\n        while (j < text.length) {\n          if (text[j] === \"\\x07\") break\n          if (text[j] === \"\\x1b\" && text[j + 1] === \"\\\\\") break\n          j++\n        }\n        if (j >= text.length) break\n        i = text[j] === \"\\x07\" ? j + 1 : j + 2\n        continue\n      }\n      if (next === undefined) break\n      // Charset selection (`ESC(B`) takes one more byte than the other two-byte escapes.\n      i += next === \"(\" || next === \")\" || next === \"#\" || next === \"%\" ? 3 : 2\n      continue\n    }\n\n    if (ch === \"\\n\") {\n      flush()\n      i++\n      continue\n    }\n    if (ch === \"\\r\") {\n      column = 0\n      i++\n      continue\n    }\n    if (ch === \"\\b\") {\n      if (column > 0) column--\n      i++\n      continue\n    }\n    // Any other C0 control (a bell, a vertical tab, a form feed) and DEL have no printable width,\n    // and the replacement glyph a font picks for them is worse than nothing. The tab is the one\n    // exception: it is kept as a cell and laid out by the CSS tab-size.\n    if (ch !== \"\\t\" && (ch < \" \" || ch === \"\\x7f\")) {\n      i++\n      continue\n    }\n\n    // By code point, not by UTF-16 unit: an emoji in a build log is a single cell, and writing its\n    // halves into two cells would let a later carriage return overwrite one of them and leave a\n    // lone surrogate behind.\n    const cp = text.codePointAt(i)\n    const char = cp === undefined ? text[i] : String.fromCodePoint(cp)\n    write(char)\n    i += char.length\n  }\n\n  // A trailing newline ends the last line rather than starting an empty one, so \"done\\n\" is one\n  // line — but \"done\\n\\n\" keeps the blank line the second newline really does mean.\n  if (touched) flush()\n  return lines\n}\n\n/** Digits grouped in threes, without Intl: a locale-dependent separator would differ between the\n * server and the browser and break hydration for the sake of one comma. */\nconst groupDigits = (n: number) => String(n).replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\")\n\n/** Perceived lightness, used only to put readable text on a reverse-video block of a colour that\n * came from the log itself and so cannot be checked in advance. */\nconst isLight = (c: { r: number; g: number; b: number }) =>\n  (c.r * 299 + c.g * 587 + c.b * 114) / 1000 > 140\n\nfunction spanAttrs(style: AnsiStyle) {\n  const classes: string[] = []\n  const inline: React.CSSProperties = {}\n\n  if (style.inverse) {\n    // Reverse video puts the colour in the block and the page's own background colour on top of\n    // it, always. The tempting reading — swap the two slots and look each one up as usual — puts a\n    // foreground tone on a block that is the panel's opposite: those tones are picked for contrast\n    // against the panel, so `ESC[31;7m` lands red on near-black in light mode and red on near-white\n    // in dark, around 3:1 both ways. Painting the block instead keeps the colour, keeps the\n    // inversion, and holds contrast by construction, because the text is a token the theme already\n    // guarantees against `foreground`.\n    //\n    // A terminal inverting `ESC[41;7m` would instead put red *text* on a white block; here it\n    // reads as a red block, which loses that distinction and keeps the colour and the legibility.\n    const block = style.fg ?? style.bg\n    if (block === null) {\n      classes.push(\"bg-foreground\", \"text-background\")\n    } else if (block.kind === \"index\") {\n      classes.push(BG_SOLID_CLASS[block.index], \"text-background\")\n    } else {\n      // A 24-bit block out of the log cannot be checked in advance, so the lightness of the colour\n      // underneath is what decides whether the text on it is black or white.\n      inline.backgroundColor = `rgb(${block.r} ${block.g} ${block.b})`\n      inline.color = isLight(block) ? \"rgb(0 0 0)\" : \"rgb(255 255 255)\"\n    }\n  } else {\n    if (style.bg !== null) {\n      if (style.bg.kind === \"index\") classes.push(BG_WASH_CLASS[style.bg.index])\n      else inline.backgroundColor = `rgb(${style.bg.r} ${style.bg.g} ${style.bg.b} / 0.25)`\n    }\n    if (style.fg !== null) {\n      if (style.fg.kind === \"index\") classes.push(FG_CLASS[style.fg.index])\n      else inline.color = `rgb(${style.fg.r} ${style.fg.g} ${style.fg.b})`\n    }\n  }\n\n  if (style.bold) classes.push(\"font-bold\")\n  if (style.dim) classes.push(\"opacity-70\")\n  if (style.italic) classes.push(\"italic\")\n  if (style.underline) classes.push(\"underline\")\n  if (style.strike) classes.push(\"line-through\")\n\n  return {\n    className: classes.length ? cn(...classes) : undefined,\n    style: Object.keys(inline).length ? inline : undefined,\n  }\n}\n\nexport const AnsiLog = React.forwardRef<HTMLDivElement, AnsiLogProps>(\n  function AnsiLog(\n    {\n      className,\n      text,\n      showLineNumbers = false,\n      maxLines = 5000,\n      wrap = false,\n      label = \"Log output\",\n      ...props\n    },\n    ref\n  ) {\n    const lines = parseAnsi(text)\n\n    // Kept from the end, because the reason anyone opens a log is at the bottom of it: the failure\n    // that stopped the build, not the hundred cache-hit lines that preceded it. The count that was\n    // dropped is then said out loud — a viewer that silently shows a suffix reads as a complete log\n    // and is the reason someone spends an afternoon looking for a line that was never rendered.\n    const limit = Number.isFinite(maxLines) && maxLines > 0 ? Math.floor(maxLines) : lines.length\n    const dropped = Math.max(0, lines.length - limit)\n    const shown = dropped > 0 ? lines.slice(dropped) : lines\n    const gutterWidth = String(lines.length).length\n\n    return (\n      <div\n        ref={ref}\n        className={cn(\"overflow-hidden rounded-lg border bg-muted/50\", className)}\n        {...props}\n      >\n        {/* A region that scrolls has to be reachable by keyboard, or the right-hand end of every\n            long line is simply unavailable to anyone not using a mouse. */}\n        <pre\n          tabIndex={0}\n          role=\"group\"\n          aria-label={label}\n          className={cn(\n            \"overflow-x-auto p-4 font-mono text-[13px] leading-relaxed outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:ring-inset\",\n            wrap ? \"whitespace-pre-wrap break-words\" : \"whitespace-pre\"\n          )}\n        >\n          {dropped > 0 ? (\n            <React.Fragment key=\"dropped\">\n              {showLineNumbers ? (\n                <span\n                  aria-hidden=\"true\"\n                  className=\"inline-block select-none pr-4 text-right\"\n                  style={{ width: `${gutterWidth}ch` }}\n                />\n              ) : null}\n              <span className=\"italic text-muted-foreground\">\n                {`… ${groupDigits(dropped)} earlier ${dropped === 1 ? \"line\" : \"lines\"} not shown`}\n              </span>\n              {\"\\n\"}\n            </React.Fragment>\n          ) : null}\n          {shown.map((spans, i) => (\n            <React.Fragment key={dropped + i}>\n              {showLineNumbers ? (\n                // Not selectable, so dragging across the log copies the log and not a column of\n                // numbers down its left edge; hidden from assistive technology, which announces\n                // lines in order anyway and does not need each one counted at it.\n                <span\n                  aria-hidden=\"true\"\n                  className=\"inline-block select-none pr-4 text-right text-muted-foreground\"\n                  style={{ width: `${gutterWidth}ch` }}\n                >\n                  {dropped + i + 1}\n                </span>\n              ) : null}\n              {spans.map((span, j) => {\n                const attrs = spanAttrs(span.style)\n                return attrs.className || attrs.style ? (\n                  <span key={j} className={attrs.className} style={attrs.style}>\n                    {span.text}\n                  </span>\n                ) : (\n                  // Unstyled runs are emitted as bare text. Most of a log is unstyled, and one\n                  // span per run of plain output would double the size of the markup for nothing.\n                  <React.Fragment key={j}>{span.text}</React.Fragment>\n                )\n              })}\n              {i < shown.length - 1 ? \"\\n\" : null}\n            </React.Fragment>\n          ))}\n        </pre>\n      </div>\n    )\n  }\n)\nAnsiLog.displayName = \"AnsiLog\"\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}