{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "log-viewer",
  "title": "Log Viewer",
  "description": "High-throughput streaming log viewer with ANSI color parsing, search, level filtering, and tail auto-scroll.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "button",
    "input",
    "@drift-ui/use-controllable-state",
    "@drift-ui/virtual-list",
    "@drift-ui/shape"
  ],
  "files": [
    {
      "path": "registry/default/ui/log-viewer.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ArrowDownIcon, SearchIcon, TextWrapIcon, XIcon } from \"lucide-react\"\n\nimport { useControllableState } from \"@/registry/default/hooks/use-controllable-state\"\nimport {\n  filterLogs,\n  formatTimestamp,\n  getLogLevels,\n  parseAnsi,\n  type AnsiColor,\n  type AnsiSegment,\n  type LogLevel,\n  type LogLine,\n} from \"@/registry/default/lib/log-viewer.ts\"\nimport { cn } from \"@/registry/default/lib/utils\"\nimport { Button } from \"@/registry/default/ui/button\"\nimport { Input } from \"@/registry/default/ui/input\"\nimport { VirtualList } from \"@/registry/default/ui/virtual-list\"\n\nexport type { AnsiColor, AnsiSegment, LogLevel, LogLine }\n\nconst ANSI_COLOR_CLASSES: Record<AnsiColor, string> = {\n  black: \"text-zinc-600 dark:text-zinc-400\",\n  red: \"text-red-600 dark:text-red-400\",\n  green: \"text-emerald-600 dark:text-emerald-400\",\n  yellow: \"text-amber-600 dark:text-amber-400\",\n  blue: \"text-blue-600 dark:text-blue-400\",\n  magenta: \"text-purple-600 dark:text-purple-400\",\n  cyan: \"text-cyan-600 dark:text-cyan-400\",\n  white: \"text-zinc-200 dark:text-zinc-100\",\n  \"bright-black\": \"text-zinc-400 dark:text-zinc-500\",\n  \"bright-red\": \"text-red-500 dark:text-red-300\",\n  \"bright-green\": \"text-emerald-500 dark:text-emerald-300\",\n  \"bright-yellow\": \"text-amber-500 dark:text-amber-300\",\n  \"bright-blue\": \"text-blue-500 dark:text-blue-300\",\n  \"bright-magenta\": \"text-purple-500 dark:text-purple-300\",\n  \"bright-cyan\": \"text-cyan-500 dark:text-cyan-300\",\n  \"bright-white\": \"text-white\",\n}\n\nconst ANSI_BG_CLASSES: Record<AnsiColor, string> = {\n  black: \"bg-zinc-800 text-zinc-100\",\n  red: \"bg-red-500/20\",\n  green: \"bg-emerald-500/20\",\n  yellow: \"bg-amber-500/20\",\n  blue: \"bg-blue-500/20\",\n  magenta: \"bg-purple-500/20\",\n  cyan: \"bg-cyan-500/20\",\n  white: \"bg-zinc-200 dark:bg-zinc-700\",\n  \"bright-black\": \"bg-zinc-700\",\n  \"bright-red\": \"bg-red-500/30\",\n  \"bright-green\": \"bg-emerald-500/30\",\n  \"bright-yellow\": \"bg-amber-500/30\",\n  \"bright-blue\": \"bg-blue-500/30\",\n  \"bright-magenta\": \"bg-purple-500/30\",\n  \"bright-cyan\": \"bg-cyan-500/30\",\n  \"bright-white\": \"bg-white text-black\",\n}\n\nconst LEVEL_BADGE_CLASSES: Record<string, string> = {\n  error: \"bg-destructive/15 text-destructive border-destructive/20\",\n  warn: \"bg-amber-500/15 text-amber-600 dark:text-amber-400 border-amber-500/20\",\n  info: \"bg-blue-500/15 text-blue-600 dark:text-blue-400 border-blue-500/20\",\n  debug: \"bg-zinc-800 text-zinc-400 border-zinc-700\",\n  trace: \"bg-zinc-800 text-zinc-400 border-zinc-700\",\n}\n\nfunction AnsiMessage({ message }: { message: string }) {\n  const segments = React.useMemo(() => parseAnsi(message), [message])\n\n  return (\n    <span>\n      {segments.map((segment, index) => {\n        const { style } = segment\n        const className = cn(\n          style.color && ANSI_COLOR_CLASSES[style.color],\n          style.backgroundColor && ANSI_BG_CLASSES[style.backgroundColor],\n          style.bold && \"font-bold\",\n          style.dim && \"opacity-60\",\n          style.italic && \"italic\",\n          style.underline && \"underline\",\n          style.strikethrough && \"line-through\",\n          style.inverse && \"bg-foreground text-background\"\n        )\n\n        return className ? (\n          <span key={index} className={className}>\n            {segment.text}\n          </span>\n        ) : (\n          segment.text\n        )\n      })}\n    </span>\n  )\n}\n\nexport interface LogViewerProps extends Omit<\n  React.ComponentProps<\"div\">,\n  \"children\"\n> {\n  lines: LogLine[]\n  follow?: boolean\n  defaultFollow?: boolean\n  onFollowChange?: (follow: boolean) => void\n  searchable?: boolean\n  search?: string\n  defaultSearch?: string\n  onSearchChange?: (search: string) => void\n  filterable?: boolean\n  level?: string\n  defaultLevel?: string\n  onLevelChange?: (level: string) => void\n  showLineNumbers?: boolean\n  showTimestamps?: boolean\n  wrap?: boolean\n  defaultWrap?: boolean\n  onWrapChange?: (wrap: boolean) => void\n  rowHeight?: number\n  height?: number | string\n  emptyMessage?: React.ReactNode\n}\n\nfunction LogViewer({\n  lines = [],\n  follow: followProp,\n  defaultFollow = true,\n  onFollowChange,\n  searchable = true,\n  search: searchProp,\n  defaultSearch = \"\",\n  onSearchChange,\n  filterable = true,\n  level: levelProp,\n  defaultLevel = \"all\",\n  onLevelChange,\n  showLineNumbers = true,\n  showTimestamps = true,\n  wrap: wrapProp,\n  defaultWrap = false,\n  onWrapChange,\n  rowHeight = 24,\n  height = 400,\n  emptyMessage = \"No log lines matching criteria\",\n  className,\n  ...props\n}: LogViewerProps) {\n  const listRef = React.useRef<HTMLDivElement>(null)\n\n  const { value: follow, setValue: setFollow } = useControllableState<boolean>({\n    value: followProp,\n    defaultValue: defaultFollow,\n    onChange: onFollowChange,\n  })\n\n  const { value: search, setValue: setSearch } = useControllableState<string>({\n    value: searchProp,\n    defaultValue: defaultSearch,\n    onChange: onSearchChange,\n  })\n\n  const { value: level, setValue: setLevel } = useControllableState<string>({\n    value: levelProp,\n    defaultValue: defaultLevel,\n    onChange: onLevelChange,\n  })\n\n  const { value: wrap, setValue: setWrap } = useControllableState<boolean>({\n    value: wrapProp,\n    defaultValue: defaultWrap,\n    onChange: onWrapChange,\n  })\n\n  const indexedLines = React.useMemo(\n    () =>\n      lines.map((line, idx) => ({\n        ...line,\n        lineNumber: line.lineNumber ?? idx + 1,\n      })),\n    [lines]\n  )\n  const availableLevels = React.useMemo(\n    () => getLogLevels(indexedLines),\n    [indexedLines]\n  )\n  const filteredLines = React.useMemo(\n    () => filterLogs(indexedLines, { search, level }),\n    [indexedLines, search, level]\n  )\n\n  React.useEffect(() => {\n    const container = listRef.current\n    const content = container?.firstElementChild\n    if (!follow || !container || !content) return\n    const scrollToBottom = () => {\n      container.scrollTop = container.scrollHeight\n    }\n    scrollToBottom()\n    const observer = new ResizeObserver(scrollToBottom)\n    observer.observe(content)\n    return () => observer.disconnect()\n  }, [follow, filteredLines.length])\n\n  const handleScroll = (event: React.UIEvent<HTMLDivElement>) => {\n    const container = event.currentTarget\n    const distanceFromBottom =\n      container.scrollHeight - container.scrollTop - container.clientHeight\n    const isAtBottom = distanceFromBottom <= 24\n\n    if (follow && !isAtBottom) {\n      setFollow(false)\n    } else if (!follow && isAtBottom) {\n      setFollow(true)\n    }\n  }\n\n  const handleJumpToLatest = () => {\n    setFollow(true)\n    if (listRef.current) {\n      listRef.current.scrollTop = listRef.current.scrollHeight\n    }\n  }\n\n  const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n    if (event.key === \"Escape\" && search) {\n      event.preventDefault()\n      setSearch(\"\")\n    }\n    props.onKeyDown?.(event)\n  }\n\n  return (\n    <div\n      data-slot=\"log-viewer\"\n      role=\"region\"\n      aria-label=\"Log viewer\"\n      onKeyDown={handleKeyDown}\n      className={cn(\n        \"relative flex flex-col overflow-hidden rounded-xl border bg-card text-card-foreground shadow-sm\",\n        className\n      )}\n      {...props}\n    >\n      <div className=\"flex flex-wrap items-center justify-between gap-2 border-b bg-muted/20 p-2\">\n        <div className=\"flex min-w-48 flex-1 flex-wrap items-center gap-2\">\n          {searchable && (\n            <div className=\"relative max-w-xs min-w-40 flex-1\">\n              <SearchIcon className=\"absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2 text-muted-foreground\" />\n              <Input\n                type=\"text\"\n                value={search}\n                onChange={(e) => setSearch(e.target.value)}\n                onKeyDown={(e) => {\n                  if (e.key === \"Escape\" && search) {\n                    e.stopPropagation()\n                    setSearch(\"\")\n                  }\n                }}\n                placeholder=\"Search logs...\"\n                className=\"h-control-sm py-0 pr-7 pl-8 text-xs\"\n                aria-label=\"Search logs\"\n              />\n              {search && (\n                <button\n                  type=\"button\"\n                  onClick={() => setSearch(\"\")}\n                  className=\"absolute top-1/2 right-2 -translate-y-1/2 text-muted-foreground hover:text-foreground\"\n                  aria-label=\"Clear search\"\n                >\n                  <XIcon className=\"size-3\" />\n                </button>\n              )}\n            </div>\n          )}\n\n          {filterable && availableLevels.length > 0 && (\n            <div className=\"flex flex-wrap items-center gap-1\">\n              <Button\n                type=\"button\"\n                size=\"sm\"\n                variant={level === \"all\" ? \"secondary\" : \"ghost\"}\n                aria-pressed={level === \"all\"}\n                className=\"h-7 px-2 text-xs\"\n                onClick={() => setLevel(\"all\")}\n              >\n                All\n              </Button>\n              {availableLevels.map((lvl) => (\n                <Button\n                  key={lvl}\n                  type=\"button\"\n                  size=\"sm\"\n                  variant={level.toLowerCase() === lvl ? \"secondary\" : \"ghost\"}\n                  aria-pressed={level.toLowerCase() === lvl}\n                  className=\"h-7 px-2 text-xs capitalize\"\n                  onClick={() => setLevel(lvl)}\n                >\n                  {lvl}\n                </Button>\n              ))}\n            </div>\n          )}\n        </div>\n\n        <div className=\"flex items-center gap-3 text-xs text-muted-foreground\">\n          <span className=\"select-none\">\n            {filteredLines.length !== lines.length\n              ? `${filteredLines.length} of ${lines.length} lines`\n              : `${lines.length} lines`}\n          </span>\n\n          <Button\n            type=\"button\"\n            size=\"sm\"\n            variant={wrap ? \"secondary\" : \"ghost\"}\n            aria-pressed={wrap}\n            className=\"h-7 gap-1.5 px-2 text-xs\"\n            onClick={() => setWrap(!wrap)}\n          >\n            <TextWrapIcon className=\"size-3.5\" />\n            Wrap\n          </Button>\n\n          <Button\n            type=\"button\"\n            size=\"sm\"\n            variant={follow ? \"secondary\" : \"ghost\"}\n            className=\"h-7 gap-1.5 px-2 text-xs\"\n            onClick={() => setFollow(!follow)}\n            aria-label={follow ? \"Pause auto-scroll\" : \"Resume auto-scroll\"}\n          >\n            <span\n              className={cn(\n                \"size-2 rounded-full\",\n                follow ? \"animate-pulse bg-emerald-500\" : \"bg-muted-foreground\"\n              )}\n            />\n            Follow\n          </Button>\n        </div>\n      </div>\n\n      <div\n        role=\"log\"\n        aria-live=\"polite\"\n        aria-atomic=\"false\"\n        aria-label=\"Log output\"\n        className=\"relative flex-1\"\n        style={{ height: typeof height === \"number\" ? `${height}px` : height }}\n      >\n        {filteredLines.length === 0 ? (\n          <div className=\"flex h-full items-center justify-center p-6 text-sm text-muted-foreground\">\n            {emptyMessage}\n          </div>\n        ) : (\n          <VirtualList\n            ref={listRef}\n            aria-label=\"Log lines\"\n            items={filteredLines}\n            estimateSize={rowHeight}\n            getItemKey={(item) => item.id}\n            onScroll={handleScroll}\n            className=\"h-full overflow-x-auto bg-zinc-950 text-zinc-100\"\n          >\n            {(line, index) => (\n              <div\n                className={cn(\n                  \"group flex gap-3 px-3 font-mono text-xs select-text hover:bg-zinc-900\",\n                  wrap\n                    ? \"w-full items-baseline py-1 leading-4 wrap-anywhere whitespace-pre-wrap\"\n                    : \"w-max min-w-full items-center whitespace-pre\"\n                )}\n                style={wrap ? { minHeight: rowHeight } : { height: rowHeight }}\n              >\n                {showLineNumbers && (\n                  <span\n                    className={cn(\n                      \"w-10 shrink-0 text-right text-zinc-500 tabular-nums select-none\",\n                      !wrap &&\n                        \"sticky left-0 z-10 -ml-3 flex w-14 items-center justify-end self-stretch border-r border-zinc-800 bg-zinc-950 pr-2 group-hover:bg-zinc-900\"\n                    )}\n                  >\n                    {line.lineNumber ?? index + 1}\n                  </span>\n                )}\n                {showTimestamps && line.timestamp && (\n                  <span className=\"shrink-0 text-zinc-500 select-none\">\n                    {formatTimestamp(line.timestamp)}\n                  </span>\n                )}\n                {line.level && (\n                  <span\n                    className={cn(\n                      \"inline-flex w-12 shrink-0 items-center justify-center rounded border px-1 py-0.5 text-[10px] font-medium tracking-wider uppercase select-none\",\n                      LEVEL_BADGE_CLASSES[line.level.toLowerCase()] ??\n                        \"border-zinc-700 bg-zinc-800 text-zinc-400\"\n                    )}\n                  >\n                    {line.level.toUpperCase()}\n                  </span>\n                )}\n                <span className=\"min-w-0 flex-1\">\n                  <AnsiMessage message={line.message} />\n                </span>\n              </div>\n            )}\n          </VirtualList>\n        )}\n\n        {!follow && filteredLines.length > 0 && (\n          <div className=\"absolute right-4 bottom-3 z-10\">\n            <Button\n              type=\"button\"\n              size=\"sm\"\n              variant=\"secondary\"\n              onClick={handleJumpToLatest}\n              className=\"h-7 gap-1.5 text-xs shadow-md\"\n            >\n              <ArrowDownIcon className=\"size-3.5\" />\n              Jump to latest\n            </Button>\n          </div>\n        )}\n      </div>\n    </div>\n  )\n}\n\nexport { LogViewer }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/default/lib/log-viewer.ts",
      "content": "export type LogLevel = \"info\" | \"warn\" | \"error\" | \"debug\" | \"trace\" | string\n\nexport type LogLine = {\n  id: string | number\n  timestamp?: string | Date | number\n  level?: LogLevel\n  message: string\n  lineNumber?: number\n}\n\nexport type AnsiColor =\n  | \"black\"\n  | \"red\"\n  | \"green\"\n  | \"yellow\"\n  | \"blue\"\n  | \"magenta\"\n  | \"cyan\"\n  | \"white\"\n  | \"bright-black\"\n  | \"bright-red\"\n  | \"bright-green\"\n  | \"bright-yellow\"\n  | \"bright-blue\"\n  | \"bright-magenta\"\n  | \"bright-cyan\"\n  | \"bright-white\"\n\nexport type AnsiStyle = {\n  color?: AnsiColor\n  backgroundColor?: AnsiColor\n  bold?: boolean\n  dim?: boolean\n  italic?: boolean\n  underline?: boolean\n  strikethrough?: boolean\n  inverse?: boolean\n}\n\nexport type AnsiSegment = {\n  text: string\n  style: AnsiStyle\n}\n\nconst ANSI_FG_MAP: Record<number, AnsiColor> = {\n  30: \"black\",\n  31: \"red\",\n  32: \"green\",\n  33: \"yellow\",\n  34: \"blue\",\n  35: \"magenta\",\n  36: \"cyan\",\n  37: \"white\",\n}\n\nconst ANSI_BRIGHT_FG_MAP: Record<number, AnsiColor> = {\n  90: \"bright-black\",\n  91: \"bright-red\",\n  92: \"bright-green\",\n  93: \"bright-yellow\",\n  94: \"bright-blue\",\n  95: \"bright-magenta\",\n  96: \"bright-cyan\",\n  97: \"bright-white\",\n}\n\nconst ANSI_BG_MAP: Record<number, AnsiColor> = {\n  40: \"black\",\n  41: \"red\",\n  42: \"green\",\n  43: \"yellow\",\n  44: \"blue\",\n  45: \"magenta\",\n  46: \"cyan\",\n  47: \"white\",\n}\n\nconst ANSI_BRIGHT_BG_MAP: Record<number, AnsiColor> = {\n  100: \"bright-black\",\n  101: \"bright-red\",\n  102: \"bright-green\",\n  103: \"bright-yellow\",\n  104: \"bright-blue\",\n  105: \"bright-magenta\",\n  106: \"bright-cyan\",\n  107: \"bright-white\",\n}\n\nexport function map256ToAnsiColor(code: number): AnsiColor {\n  if (code >= 0 && code <= 7) {\n    return ANSI_FG_MAP[30 + code] ?? \"white\"\n  }\n  if (code >= 8 && code <= 15) {\n    return ANSI_BRIGHT_FG_MAP[90 + (code - 8)] ?? \"bright-white\"\n  }\n  if (code >= 16 && code <= 231) {\n    const n = code - 16\n    const r = Math.floor(n / 36)\n    const g = Math.floor((n % 36) / 6)\n    const b = n % 6\n\n    if (r === g && g === b) {\n      if (r < 2) return \"bright-black\"\n      if (r < 4) return \"white\"\n      return \"bright-white\"\n    }\n\n    if (r > g && r > b) {\n      if (g >= 3) return r >= 4 ? \"bright-yellow\" : \"yellow\"\n      if (b >= 3) return r >= 4 ? \"bright-magenta\" : \"magenta\"\n      return r >= 3 ? \"bright-red\" : \"red\"\n    }\n    if (g > r && g > b) {\n      if (b >= 3) return g >= 4 ? \"bright-cyan\" : \"cyan\"\n      if (r >= 3) return g >= 4 ? \"bright-yellow\" : \"yellow\"\n      return g >= 3 ? \"bright-green\" : \"green\"\n    }\n    if (b > r && b > g) {\n      if (r >= 3) return b >= 4 ? \"bright-magenta\" : \"magenta\"\n      if (g >= 3) return b >= 4 ? \"bright-cyan\" : \"cyan\"\n      return b >= 3 ? \"bright-blue\" : \"blue\"\n    }\n    if (r === g && r > b) return r >= 3 ? \"bright-yellow\" : \"yellow\"\n    if (g === b && g > r) return g >= 3 ? \"bright-cyan\" : \"cyan\"\n    if (r === b && r > g) return r >= 3 ? \"bright-magenta\" : \"magenta\"\n    return \"white\"\n  }\n  if (code >= 232 && code <= 255) {\n    if (code < 238) return \"bright-black\"\n    if (code < 248) return \"white\"\n    return \"bright-white\"\n  }\n  return \"white\"\n}\n\nexport function parseAnsi(rawText: string): AnsiSegment[] {\n  if (!rawText) return []\n\n  // 1. Strip OSC sequences, non-SGR CSI sequences (including private modes like ?25l), and standard 2-char escapes\n  const text = rawText\n    .replace(/\\x1b\\].*?(?:\\x07|\\x1b\\\\)/g, \"\")\n    .replace(/\\x1b\\[[0-9;?><=]*[ -/]*[@A-Z[\\\\\\]^_`a-ln-z{|}~]/g, \"\")\n    .replace(/\\x1b[@-Z\\\\-_]/g, \"\")\n\n  if (!text.includes(\"\\x1b[\")) {\n    return [{ text: text.replace(/\\x1b/g, \"\"), style: {} }]\n  }\n\n  const segments: AnsiSegment[] = []\n  const sgrRegex = /\\x1b\\[([0-9;:]*)m/g\n  let lastIndex = 0\n  let currentStyle: AnsiStyle = {}\n  let match: RegExpExecArray | null\n\n  while ((match = sgrRegex.exec(text)) !== null) {\n    if (match.index > lastIndex) {\n      const chunk = text.slice(lastIndex, match.index)\n      if (chunk.length > 0) {\n        segments.push({ text: chunk, style: { ...currentStyle } })\n      }\n    }\n\n    const paramStr = match[1]\n    const params = paramStr\n      ? paramStr.split(/[;:]/).map((n) => Number.parseInt(n, 10))\n      : [0]\n\n    for (let i = 0; i < params.length; i++) {\n      const code = params[i]\n      if (Number.isNaN(code)) {\n        continue\n      }\n      if (code === 0) {\n        currentStyle = {}\n      } else if (code === 1) {\n        currentStyle.bold = true\n      } else if (code === 2) {\n        currentStyle.dim = true\n      } else if (code === 3) {\n        currentStyle.italic = true\n      } else if (code === 4) {\n        currentStyle.underline = true\n      } else if (code === 7) {\n        currentStyle.inverse = true\n      } else if (code === 9) {\n        currentStyle.strikethrough = true\n      } else if (code === 21 || code === 22) {\n        delete currentStyle.bold\n        delete currentStyle.dim\n      } else if (code === 23) {\n        delete currentStyle.italic\n      } else if (code === 24) {\n        delete currentStyle.underline\n      } else if (code === 27) {\n        delete currentStyle.inverse\n      } else if (code === 29) {\n        delete currentStyle.strikethrough\n      } else if (code >= 30 && code <= 37) {\n        currentStyle.color = ANSI_FG_MAP[code]\n      } else if (code === 39) {\n        delete currentStyle.color\n      } else if (code >= 40 && code <= 47) {\n        currentStyle.backgroundColor = ANSI_BG_MAP[code]\n      } else if (code === 49) {\n        delete currentStyle.backgroundColor\n      } else if (code >= 90 && code <= 97) {\n        currentStyle.color = ANSI_BRIGHT_FG_MAP[code]\n      } else if (code >= 100 && code <= 107) {\n        currentStyle.backgroundColor = ANSI_BRIGHT_BG_MAP[code]\n      } else if (code === 38 || code === 48) {\n        const isFg = code === 38\n        if (params[i + 1] === 5 && params[i + 2] !== undefined) {\n          const mapped = map256ToAnsiColor(params[i + 2])\n          if (isFg) currentStyle.color = mapped\n          else currentStyle.backgroundColor = mapped\n          i += 2\n        } else if (params[i + 1] === 2) {\n          let r: number | undefined\n          let g: number | undefined\n          let b: number | undefined\n          let consumed = 4\n\n          if (Number.isNaN(params[i + 2])) {\n            // Colon-form with empty color-space: 38:2::r:g:b\n            r = params[i + 3]\n            g = params[i + 4]\n            b = params[i + 5]\n            consumed = 5\n          } else if (\n            params[i + 5] !== undefined &&\n            !Number.isNaN(params[i + 2]) &&\n            !Number.isNaN(params[i + 3]) &&\n            !Number.isNaN(params[i + 4]) &&\n            !Number.isNaN(params[i + 5])\n          ) {\n            // Colon-form with color-space ID: 38:2:cs:r:g:b\n            r = params[i + 3]\n            g = params[i + 4]\n            b = params[i + 5]\n            consumed = 5\n          } else {\n            // Semicolon-form: 38;2;r;g;b or 38:2:r:g:b\n            r = params[i + 2]\n            g = params[i + 3]\n            b = params[i + 4]\n            consumed = 4\n          }\n\n          if (\n            r !== undefined &&\n            g !== undefined &&\n            b !== undefined &&\n            !Number.isNaN(r) &&\n            !Number.isNaN(g) &&\n            !Number.isNaN(b)\n          ) {\n            const rStep = Math.min(5, Math.max(0, Math.floor(r / 51)))\n            const gStep = Math.min(5, Math.max(0, Math.floor(g / 51)))\n            const bStep = Math.min(5, Math.max(0, Math.floor(b / 51)))\n            const mapped = map256ToAnsiColor(\n              16 + 36 * rStep + 6 * gStep + bStep\n            )\n            if (isFg) currentStyle.color = mapped\n            else currentStyle.backgroundColor = mapped\n          }\n          i += consumed\n        }\n      }\n    }\n\n    lastIndex = sgrRegex.lastIndex\n  }\n\n  if (lastIndex < text.length) {\n    const chunk = text.slice(lastIndex)\n    if (chunk.length > 0) {\n      segments.push({ text: chunk, style: { ...currentStyle } })\n    }\n  }\n\n  return segments\n}\n\nexport function stripAnsi(text: string): string {\n  if (!text) return \"\"\n  return parseAnsi(text)\n    .map((segment) => segment.text)\n    .join(\"\")\n}\n\nexport function filterLogs(\n  lines: readonly LogLine[],\n  options: { search?: string; level?: string } = {}\n): LogLine[] {\n  const { search, level } = options\n  const hasSearch = Boolean(search && search.trim())\n  const searchLower = hasSearch ? search!.trim().toLowerCase() : \"\"\n  const hasLevel = Boolean(level && level !== \"all\")\n  const levelLower = hasLevel ? level!.toLowerCase() : \"\"\n\n  if (!hasSearch && !hasLevel) {\n    return lines.slice()\n  }\n\n  return lines.filter((line) => {\n    if (hasLevel) {\n      const lineLevel = (line.level ?? \"\").toLowerCase()\n      if (lineLevel !== levelLower) {\n        return false\n      }\n    }\n\n    if (hasSearch) {\n      const plainMessage = stripAnsi(line.message).toLowerCase()\n      const levelText = (line.level ?? \"\").toLowerCase()\n      const timeText = line.timestamp\n        ? formatTimestamp(line.timestamp).toLowerCase()\n        : \"\"\n      if (\n        !plainMessage.includes(searchLower) &&\n        !levelText.includes(searchLower) &&\n        !timeText.includes(searchLower)\n      ) {\n        return false\n      }\n    }\n\n    return true\n  })\n}\n\nexport function getLogLevels(lines: readonly LogLine[]): string[] {\n  const levels = new Set<string>()\n  for (const line of lines) {\n    if (line.level) {\n      levels.add(line.level.toLowerCase())\n    }\n  }\n  return Array.from(levels).sort()\n}\n\nexport function formatTimestamp(timestamp?: string | Date | number): string {\n  if (timestamp === undefined || timestamp === null) return \"\"\n  const date = timestamp instanceof Date ? timestamp : new Date(timestamp)\n  if (Number.isNaN(date.getTime())) return String(timestamp)\n\n  const hours = String(date.getHours()).padStart(2, \"0\")\n  const minutes = String(date.getMinutes()).padStart(2, \"0\")\n  const seconds = String(date.getSeconds()).padStart(2, \"0\")\n  return `${hours}:${minutes}:${seconds}`\n}\n",
      "type": "registry:lib"
    }
  ],
  "type": "registry:ui"
}