{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "log-viewer-ascii-tail",
  "title": "Log Viewer ASCII Tail",
  "description": "A live `tail -f` log pane with a real ring buffer, a per-line severity glyph column, and follow-mode that detaches the instant you scroll up and reports how many lines arrived while you were reading.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/log-viewer-ascii-tail/component.tsx",
      "content": "\"use client\";\n\nimport {\n  useCallback,\n  useEffect,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n  type KeyboardEvent as ReactKeyboardEvent,\n} from \"react\";\n\n// ---------------------------------------------------------------------------\n// TailPane — a `tail -f` log viewer with a real bounded ring buffer, an ASCII\n// severity column and follow-mode that detaches the instant you scroll up.\n//\n// The three things that make it honest rather than a log-shaped animation:\n//  1. memory is bounded — appends overwrite the oldest slot of a fixed\n//     Array(capacity); nothing grows without limit, however long it runs.\n//  2. the stream is deterministic — one LCG drives both the inter-line delay\n//     and the severity draw, so cadence jitters like real traffic instead of\n//     ticking, and the same seed always replays the same log.\n//  3. appends never render per-line — they land in a ref and flush to React\n//     through ONE rAF-coalesced setState per frame, and only the rows inside\n//     the viewport are in the DOM (fixed 18px rows, absolute positioning).\n// ---------------------------------------------------------------------------\n\nexport type LogSeverity = \"DEBUG\" | \"INFO\" | \"WARN\" | \"ERROR\";\n\nexport type LogLine = {\n  /** monotonically increasing sequence number; also its ring slot (seq % capacity) */\n  seq: number;\n  /** pre-formatted HH:MM:SS.mmm wall clock */\n  time: string;\n  sev: LogSeverity;\n  service: string;\n  message: string;\n};\n\nexport interface LogViewerAsciiTailProps {\n  /** ring buffer size — appends past this overwrite the oldest line. Default 2000. */\n  capacity?: number;\n  /** LCG seed; the same seed replays the same stream exactly. Default 20240804. */\n  seed?: number;\n  /** lines synthesised before the stream starts, so the pane is dense at rest. Default 200. */\n  initialLines?: number;\n  /** service column values drawn from. */\n  services?: string[];\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n  /** accessible name for the log pane */\n  \"aria-label\"?: string;\n}\n\nconst ROW_H = 18; // px — fixed, which is what makes windowing trivial\nconst OVERSCAN = 6; // rows rendered beyond the viewport, each side\nconst NEAR_BOTTOM = 8; // px — inside this, follow re-attaches\nconst SERVICE_W = 11; // chars, padded so the pipes form true vertical rules\n\nconst LEVELS: LogSeverity[] = [\"DEBUG\", \"INFO\", \"WARN\", \"ERROR\"];\nconst LEVEL_RANK: Record<LogSeverity, number> = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 };\n\nconst GLYPH: Record<LogSeverity, string> = {\n  DEBUG: \"·\", // ·\n  INFO: \"─\", // ─\n  WARN: \"≡\", // ≡\n  ERROR: \"█\", // █\n};\n\n// glyph AND weight together, never hue alone. The faintest ink is a muted\n// tint, NOT `text-border` — --border is tuned for 1px hairlines and is\n// invisible as type on the light theme (#ebebeb on #ffffff).\nconst FAINT = \"text-ns-muted/45\";\n\nconst SEV_INK: Record<LogSeverity, string> = {\n  DEBUG: FAINT,\n  INFO: \"text-ns-muted\",\n  WARN: \"text-foreground font-medium\",\n  ERROR: \"text-ns-accent font-semibold\",\n};\n\nconst DEFAULT_SERVICES = [\n  \"api-gateway\",\n  \"auth-worker\",\n  \"orders-svc\",\n  \"ledger-sync\",\n  \"edge-cache\",\n  \"search-idx\",\n];\n\nconst MESSAGES: Record<LogSeverity, string[]> = {\n  DEBUG: [\n    \"cache hit ratio 0.94 over 512 keys\",\n    \"span emitted trace=9f2a1c parent=3bd881\",\n    \"pool idle 12/32 connections\",\n    \"heartbeat ack in 4ms\",\n    \"resolved 3 dns records ttl=30s\",\n    \"compacted 18 segments into 6\",\n    \"token introspection cached 240s\",\n  ],\n  INFO: [\n    \"GET /v2/orders 200 in 38ms\",\n    \"POST /v2/checkout 201 in 112ms\",\n    \"session refreshed for user 84213\",\n    \"published 128 events to orders.v2\",\n    \"shard 4 rebalanced in 1.8s\",\n    \"index snapshot written 41MB\",\n    \"GET /health 200 in 2ms\",\n    \"invoice 4471 settled in 74ms\",\n  ],\n  WARN: [\n    \"retry 2/5 after connection reset\",\n    \"queue depth 8420 above soft limit\",\n    \"p99 latency 812ms over budget\",\n    \"clock skew 340ms against peer 3\",\n    \"rate limit 80% consumed for key ak_19\",\n  ],\n  ERROR: [\n    \"upstream 502 after 3 retries\",\n    \"deadline exceeded on shard 7\",\n    \"checksum mismatch on segment 41\",\n    \"connection refused to ledger:5432\",\n  ],\n};\n\n// deterministic LCG — one stream drives delay, severity, service and message\nfunction nextSeed(s: number): number {\n  return (s * 1664525 + 1013904223) >>> 0;\n}\n\nfunction formatClock(msOfDay: number): string {\n  const ms = Math.floor(msOfDay) % 86_400_000;\n  const h = Math.floor(ms / 3_600_000);\n  const m = Math.floor(ms / 60_000) % 60;\n  const s = Math.floor(ms / 1000) % 60;\n  const f = Math.floor(ms) % 1000;\n  return `${String(h).padStart(2, \"0\")}:${String(m).padStart(2, \"0\")}:${String(s).padStart(2, \"0\")}.${String(f).padStart(3, \"0\")}`;\n}\n\nfunction drawSeverity(r: number): LogSeverity {\n  if (r < 0.42) return \"DEBUG\";\n  if (r < 0.82) return \"INFO\";\n  if (r < 0.95) return \"WARN\";\n  return \"ERROR\";\n}\n\n/** splits a message around every case-insensitive occurrence of `q` */\nfunction inkMatches(message: string, q: string) {\n  if (!q) return [{ text: message, hit: false }];\n  const out: { text: string; hit: boolean }[] = [];\n  const hay = message.toLowerCase();\n  const needle = q.toLowerCase();\n  let i = 0;\n  for (;;) {\n    const at = hay.indexOf(needle, i);\n    if (at === -1) break;\n    if (at > i) out.push({ text: message.slice(i, at), hit: false });\n    out.push({ text: message.slice(at, at + needle.length), hit: true });\n    i = at + needle.length;\n  }\n  if (i < message.length) out.push({ text: message.slice(i), hit: false });\n  return out;\n}\n\nconst useIsoLayoutEffect = typeof window === \"undefined\" ? useEffect : useLayoutEffect;\n\nexport function LogViewerAsciiTail({\n  capacity = 2000,\n  seed = 20240804,\n  initialLines = 200,\n  services = DEFAULT_SERVICES,\n  className = \"\",\n  \"aria-label\": ariaLabel = \"Application log, live tail\",\n}: LogViewerAsciiTailProps) {\n  // an inline `services={[...]}` prop would otherwise be a new array every\n  // render, re-creating appendOne and re-running the seed effect forever;\n  // rebuilding from a joined key makes the identity depend on the CONTENT\n  const serviceKey = services.join(\"|\");\n  const serviceList = useMemo(() => serviceKey.split(\"|\"), [serviceKey]);\n\n  // --- ring buffer ---------------------------------------------------------\n  const ringRef = useRef<(LogLine | undefined)[]>([]);\n  const totalRef = useRef(0); // total lines ever appended; head slot = total % capacity\n  const rngRef = useRef(seed >>> 0);\n  const clockRef = useRef(43_471_882); // 12:04:31.882, a fixed start so SSR/CSR agree\n\n  // --- flush plumbing (one setState per frame, never one per line) ----------\n  const [tick, setTick] = useState(0);\n  const rafRef = useRef(0);\n  const newRef = useRef(0);\n  const [newCount, setNewCount] = useState(0);\n\n  // --- follow mode ---------------------------------------------------------\n  const paneRef = useRef<HTMLDivElement>(null);\n  const followRef = useRef(true);\n  const [follow, setFollow] = useState(true);\n\n  // --- windowing -----------------------------------------------------------\n  const [range, setRange] = useState({ start: 0, end: 60 });\n\n  // --- filters -------------------------------------------------------------\n  const [minLevel, setMinLevel] = useState<LogSeverity>(\"DEBUG\");\n  const [query, setQuery] = useState(\"\");\n\n  // --- motion / lifecycle --------------------------------------------------\n  const [reduced, setReduced] = useState(false);\n  const [running, setRunning] = useState(true);\n  const runningRef = useRef(true);\n  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  const scheduleFlush = useCallback(() => {\n    if (rafRef.current) return;\n    rafRef.current = requestAnimationFrame(() => {\n      rafRef.current = 0;\n      setNewCount(newRef.current);\n      setTick((t) => t + 1);\n    });\n  }, []);\n\n  /** synthesise exactly one line and write it into its ring slot */\n  const appendOne = useCallback(() => {\n    let s = rngRef.current;\n\n    s = nextSeed(s);\n    const delay = 90 + (s / 4_294_967_296) * 260;\n    clockRef.current += delay;\n\n    s = nextSeed(s);\n    const sev = drawSeverity(s / 4_294_967_296);\n\n    s = nextSeed(s);\n    const service =\n      serviceList[Math.floor((s / 4_294_967_296) * serviceList.length)] ?? serviceList[0]!;\n\n    s = nextSeed(s);\n    const pool = MESSAGES[sev];\n    const message = pool[Math.floor((s / 4_294_967_296) * pool.length)] ?? pool[0]!;\n\n    rngRef.current = s;\n\n    const seq = totalRef.current;\n    ringRef.current[seq % capacity] = {\n      seq,\n      time: formatClock(clockRef.current),\n      sev,\n      service,\n      message,\n    };\n    totalRef.current = seq + 1;\n    if (!followRef.current) newRef.current += 1;\n    return delay;\n  }, [capacity, serviceList]);\n\n  // reduced motion\n  useEffect(() => {\n    const mql = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    const update = () => {\n      setReduced(mql.matches);\n      if (mql.matches) {\n        setRunning(false);\n        runningRef.current = false;\n      }\n    };\n    update();\n    mql.addEventListener(\"change\", update);\n    return () => mql.removeEventListener(\"change\", update);\n  }, []);\n\n  // seed the buffer once — the pane is dense and legible before any motion\n  useEffect(() => {\n    ringRef.current = new Array(capacity);\n    totalRef.current = 0;\n    rngRef.current = seed >>> 0;\n    clockRef.current = 43_471_882;\n    for (let i = 0; i < initialLines; i++) appendOne();\n    newRef.current = 0;\n    setNewCount(0);\n    setTick((t) => t + 1);\n  }, [capacity, seed, initialLines, appendOne]);\n\n  // the stream: a self-rescheduling timeout whose delay is drawn from the LCG,\n  // paused while the tab is hidden and while `running` is false\n  useEffect(() => {\n    runningRef.current = running;\n    if (!running) return;\n\n    let cancelled = false;\n    // document.hidden truly stops the generator (no polling); visibilitychange\n    // restarts it, so a backgrounded tab costs nothing.\n    const step = () => {\n      if (cancelled || document.hidden) {\n        timerRef.current = null;\n        return;\n      }\n      const delay = appendOne();\n      scheduleFlush();\n      timerRef.current = setTimeout(step, delay);\n    };\n    timerRef.current = setTimeout(step, 120);\n\n    const onVis = () => {\n      if (!document.hidden && !cancelled && timerRef.current === null) {\n        timerRef.current = setTimeout(step, 120);\n      }\n    };\n    document.addEventListener(\"visibilitychange\", onVis);\n\n    return () => {\n      cancelled = true;\n      document.removeEventListener(\"visibilitychange\", onVis);\n      if (timerRef.current) clearTimeout(timerRef.current);\n      timerRef.current = null;\n    };\n  }, [running, appendOne, scheduleFlush]);\n\n  useEffect(() => () => cancelAnimationFrame(rafRef.current), []);\n\n  // --- derived views -------------------------------------------------------\n  const lines = useMemo(() => {\n    const total = totalRef.current;\n    const from = Math.max(0, total - capacity);\n    const out: LogLine[] = [];\n    for (let i = from; i < total; i++) {\n      const l = ringRef.current[i % capacity];\n      if (l) out.push(l);\n    }\n    return out;\n    // tick is the flush signal; the data itself lives in refs\n  }, [tick, capacity]);\n\n  const rows = useMemo(() => {\n    const min = LEVEL_RANK[minLevel];\n    const q = query.trim().toLowerCase();\n    return lines.filter(\n      (l) =>\n        LEVEL_RANK[l.sev] >= min &&\n        (q === \"\" ||\n          l.message.toLowerCase().includes(q) ||\n          l.service.toLowerCase().includes(q))\n    );\n  }, [lines, minLevel, query]);\n\n  // --- scroll plumbing -----------------------------------------------------\n  const measure = useCallback(() => {\n    const el = paneRef.current;\n    if (!el) return;\n    const start = Math.max(0, Math.floor(el.scrollTop / ROW_H) - OVERSCAN);\n    const end = Math.ceil((el.scrollTop + el.clientHeight) / ROW_H) + OVERSCAN;\n    setRange((r) => (r.start === start && r.end === end ? r : { start, end }));\n  }, []);\n\n  const attach = useCallback(() => {\n    followRef.current = true;\n    newRef.current = 0;\n    setNewCount(0);\n    setFollow(true);\n    const el = paneRef.current;\n    if (el) el.scrollTop = el.scrollHeight;\n    measure();\n  }, [measure]);\n\n  const onScroll = useCallback(() => {\n    const el = paneRef.current;\n    if (!el) return;\n    const distance = el.scrollHeight - el.scrollTop - el.clientHeight;\n    if (distance > NEAR_BOTTOM) {\n      if (followRef.current) {\n        followRef.current = false;\n        setFollow(false);\n      }\n    } else if (!followRef.current) {\n      followRef.current = true;\n      newRef.current = 0;\n      setNewCount(0);\n      setFollow(true);\n    }\n    measure();\n  }, [measure]);\n\n  // pin to the bottom while following — after every flush and every filter change\n  useIsoLayoutEffect(() => {\n    const el = paneRef.current;\n    if (!el) return;\n    if (followRef.current) el.scrollTop = el.scrollHeight;\n    measure();\n  }, [rows, measure]);\n\n  const onKeyDown = (e: ReactKeyboardEvent<HTMLDivElement>) => {\n    const el = paneRef.current;\n    if (!el) return;\n    if (e.key === \"End\") {\n      e.preventDefault();\n      attach();\n    } else if (e.key === \"Home\") {\n      e.preventDefault();\n      el.scrollTop = 0;\n    } else if (e.key === \"ArrowDown\") {\n      e.preventDefault();\n      el.scrollTop += ROW_H;\n    } else if (e.key === \"ArrowUp\") {\n      e.preventDefault();\n      el.scrollTop -= ROW_H;\n    } else if (e.key === \"PageDown\") {\n      e.preventDefault();\n      el.scrollTop += el.clientHeight - ROW_H;\n    } else if (e.key === \"PageUp\") {\n      e.preventDefault();\n      el.scrollTop -= el.clientHeight - ROW_H;\n    }\n  };\n\n  // roving tabindex: selecting with the arrow keys must MOVE focus too,\n  // otherwise the ring is left behind on a button that is now tabIndex=-1\n  const radioRefs = useRef<Partial<Record<LogSeverity, HTMLButtonElement | null>>>({});\n\n  const moveLevel = (to: LogSeverity) => {\n    setMinLevel(to);\n    radioRefs.current[to]?.focus();\n  };\n\n  const onLevelKeyDown = (e: ReactKeyboardEvent<HTMLDivElement>) => {\n    const i = LEVELS.indexOf(minLevel);\n    if (e.key === \"ArrowRight\" || e.key === \"ArrowDown\") {\n      e.preventDefault();\n      moveLevel(LEVELS[Math.min(LEVELS.length - 1, i + 1)]!);\n    } else if (e.key === \"ArrowLeft\" || e.key === \"ArrowUp\") {\n      e.preventDefault();\n      moveLevel(LEVELS[Math.max(0, i - 1)]!);\n    } else if (e.key === \"Home\") {\n      e.preventDefault();\n      moveLevel(LEVELS[0]!);\n    } else if (e.key === \"End\") {\n      e.preventDefault();\n      moveLevel(LEVELS[LEVELS.length - 1]!);\n    }\n  };\n\n  const visible = rows.slice(range.start, range.end);\n  const inputId = \"log-tail-filter\";\n\n  return (\n    <div\n      className={`flex h-full min-h-0 flex-col overflow-hidden rounded-md border border-border bg-background font-mono text-xs ${className}`}\n    >\n      {/* header: the level control doubles as the glyph legend */}\n      <div className=\"flex flex-wrap items-center gap-x-4 gap-y-2 border-b border-border px-3 py-2\">\n        <span className=\"text-ns-muted\">\n          tail -f <span className=\"text-foreground\">/var/log/edge.log</span>\n        </span>\n\n        <div\n          role=\"radiogroup\"\n          aria-label=\"Minimum severity\"\n          onKeyDown={onLevelKeyDown}\n          className=\"flex items-center overflow-hidden rounded-sm border border-border\"\n        >\n          {LEVELS.map((lv) => {\n            const on = lv === minLevel;\n            return (\n              <button\n                key={lv}\n                ref={(node) => {\n                  radioRefs.current[lv] = node;\n                }}\n                type=\"button\"\n                role=\"radio\"\n                aria-checked={on}\n                tabIndex={on ? 0 : -1}\n                onClick={() => setMinLevel(lv)}\n                className={`px-2 py-1 whitespace-pre transition-colors duration-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-ns-accent ${\n                  on ? \"bg-surface text-foreground\" : \"text-ns-muted hover:text-foreground\"\n                }`}\n              >\n                <span className={on ? \"\" : SEV_INK[lv]}>{GLYPH[lv]}</span> {lv}\n              </button>\n            );\n          })}\n        </div>\n\n        <div className=\"flex items-center gap-2\">\n          <label htmlFor={inputId} className=\"text-ns-muted\">\n            grep\n          </label>\n          <input\n            id={inputId}\n            type=\"search\"\n            value={query}\n            onChange={(e) => setQuery(e.target.value)}\n            placeholder=\"502\"\n            spellCheck={false}\n            className=\"w-28 rounded-sm border border-border bg-surface px-2 py-1 font-mono text-xs text-foreground placeholder:text-ns-muted/60 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-ns-accent\"\n          />\n        </div>\n\n        <span className=\"ml-auto tabular-nums text-ns-muted\">\n          {rows.length}/{lines.length} lines · ring {capacity}\n        </span>\n      </div>\n\n      <div className=\"relative flex min-h-0 flex-1\">\n        {/* the log pane */}\n        <div\n          ref={paneRef}\n          data-log-pane\n          role=\"log\"\n          aria-live=\"off\"\n          aria-label={ariaLabel}\n          tabIndex={0}\n          onScroll={onScroll}\n          onKeyDown={onKeyDown}\n          className=\"min-h-0 flex-1 overflow-y-auto overflow-x-hidden focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-ns-accent\"\n        >\n          <div className=\"relative w-full\" style={{ height: rows.length * ROW_H }}>\n            {visible.map((l, i) => (\n              <div\n                key={l.seq}\n                className=\"absolute inset-x-0 flex items-center whitespace-pre px-3 text-ns-muted transition-colors duration-100 hover:bg-surface hover:text-foreground\"\n                style={{ top: (range.start + i) * ROW_H, height: ROW_H }}\n              >\n                <span className=\"tabular-nums\">{l.time}</span>\n                <span className={FAINT}>{\" │ \"}</span>\n                <span className={SEV_INK[l.sev]}>{GLYPH[l.sev]}</span>\n                <span className={FAINT}>{\" │ \"}</span>\n                <span>{l.service.padEnd(SERVICE_W)}</span>\n                <span className={FAINT}>{\" │ \"}</span>\n                <span>\n                  {inkMatches(l.message, query.trim()).map((seg, k) => (\n                    <span key={k} className={seg.hit ? \"text-ns-accent\" : undefined}>\n                      {seg.text}\n                    </span>\n                  ))}\n                </span>\n              </div>\n            ))}\n          </div>\n        </div>\n\n        {/* detach bar — the count of what arrived while you were reading */}\n        {!follow && (\n          <button\n            type=\"button\"\n            onClick={attach}\n            className=\"absolute inset-x-0 bottom-0 border-t border-border bg-surface py-1.5 text-center whitespace-pre text-ns-muted transition-colors duration-100 hover:text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-ns-accent\"\n          >\n            {\"└─ \"}\n            <span className=\"tabular-nums text-ns-accent\">{newCount}</span>\n            {` new line${newCount === 1 ? \"\" : \"s\"} ─ End to follow ─┘`}\n          </button>\n        )}\n      </div>\n\n      {reduced && (\n        <div className=\"flex items-center justify-between gap-3 border-t border-border px-3 py-2\">\n          <span className=\"text-ns-muted\">\n            {running\n              ? `stream running — ${lines.length} lines in the ring`\n              : `stream paused — ${lines.length} lines loaded`}\n          </span>\n          <button\n            type=\"button\"\n            onClick={() => setRunning((r) => !r)}\n            className=\"rounded-sm border border-border px-2 py-1 whitespace-pre text-ns-muted transition-colors duration-100 hover:border-ns-muted hover:text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-ns-accent\"\n          >\n            {running ? \"■ pause stream\" : \"▸ resume stream\"}\n          </button>\n        </div>\n      )}\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/log-viewer-ascii-tail.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ns-muted": "var(--ns-muted)",
      "color-ns-accent": "var(--ns-accent)",
      "color-surface": "var(--surface)"
    },
    "light": {
      "ns-muted": "#4d4d4d",
      "ns-accent": "#006bff",
      "surface": "#fafafa"
    },
    "dark": {
      "ns-muted": "#8f8f8f",
      "surface": "#171717"
    }
  },
  "meta": {
    "collection": "core",
    "tags": [
      "logs",
      "terminal",
      "streaming",
      "ascii",
      "mono",
      "virtualized",
      "observability",
      "developer-tools"
    ],
    "instruction": "Build <LogViewerAsciiTail capacity? seed? initialLines? services? className? aria-label?> as a Geist Mono `tail -f` pane whose honesty is structural rather than decorative. RING BUFFER: a ref-held `Array(capacity)` (default 2000) with a monotonic `total` counter; a line's ring slot is `seq % capacity`, so an append overwrites the oldest slot and memory is bounded no matter how long the stream runs — there is no growing array anywhere on the path. The ordered view is read back from `max(0, total-capacity)` to `total`. STREAM: one deterministic LCG, `s = (s * 1664525 + 1013904223) >>> 0`, drives everything, so a given `seed` replays byte-identically: draw one for the inter-line delay `90 + (s/2^32)*260` ms (cadence therefore jitters like real traffic instead of ticking on a fixed interval, and the same draw advances a virtual wall clock so timestamps agree with the delays), one for the severity from a weighted table (DEBUG 0.42, INFO 0.40, WARN 0.13, ERROR 0.05), one for the service, one for the message. The generator is a self-rescheduling setTimeout, never setInterval. FLUSH: appends land in the ref and are coalesced into ONE rAF-scheduled setState per frame — a burst of lines inside a single frame costs a single render, never one render per line. RENDERING: rows are a fixed 18px tall and absolutely positioned inside a spacer of `rows.length * 18`, and only the rows intersecting the viewport (plus 6 of overscan each side) are in the DOM, so a full 2000-line buffer is a few dozen nodes. LINE FORMAT: `12:04:31.882 │ · │ api-gateway  │ upstream 502 after 3 retries` — the service column is padEnd'd to 11 chars and the separators are drawn in `text-ns-muted/45`, so the pipes read as true faint vertical rules down the pane (NOT `text-border` — `--border` is tuned for 1px hairlines and is invisible as type on the light theme). SEVERITY COLUMN: glyph AND weight together, never hue alone — DEBUG `·` in `text-ns-muted/45`, INFO `─` in `text-ns-muted`, WARN `≡` in `text-foreground font-medium`, ERROR `█` in `text-ns-accent font-semibold`. The four-glyph column is the component's whole visual texture at rest: a mostly quiet field of `·` and `─` with the occasional accent `█` standing out, which is what makes an error burst findable without reading a word. FOLLOW: while following, every flush sets `scrollTop = scrollHeight`; the scroll handler drops follow the moment `scrollHeight - scrollTop - clientHeight > 8`, and a sticky bottom button renders `└─ 37 new lines ─ End to follow ─┘` with the count (inked in `text-ns-accent`) of appends since detach. Clicking that bar, pressing End, or scrolling back inside 8px re-attaches and zeroes the counter — the counter is the component's actual promise: nothing scrolled past you unannounced. Home/ArrowUp/ArrowDown/PageUp/PageDown also scroll the pane from the keyboard. There is no minimap, gutter or density index of any kind — the severity column and the line count are the whole read. FILTER: a `role=radiogroup` minimum-level segmented control with roving tabindex and Arrow/Home/End navigation (selecting with the keyboard also MOVES focus to the newly checked radio, so the ring never strands on a now-`tabIndex=-1` button) — each option shows its own severity glyph, so the control doubles as the legend for the column — plus a substring `input[type=search]` matched against message and service; matches are re-inked to `text-ns-accent` in place by splitting the message around each occurrence. Both re-derive the visible set straight from the ring buffer, and the header shows `visible/total lines · ring N`. ACCESSIBILITY: the pane is `role=log` with an `aria-label`, tabIndex=0, and an explicit `aria-live=\"off\"` — a live region emitting ten lines a second is unusable with a screen reader, so announcement is deliberately opt-out and the pane is navigable as a focusable scroll region instead. Every focusable control (pane, level radios, filter input, the detach bar) carries a visible `focus-visible` accent outline. LIFECYCLE: `document.hidden` stops the generator entirely (the timeout is not rescheduled) and `visibilitychange` restarts it. REDUCED MOTION: `prefers-reduced-motion: reduce` seeds the same `initialLines` (default 200) of real data and then does not start the generator at all, rendering a `▸ resume stream` toggle instead — the data on screen is honest, only the motion is opt-in. THEME: every colour is a token (`--background --foreground --ns-muted --border --surface --ns-accent`); there is no hex anywhere, no canvas, and no dependencies. Row hover raises a line from `text-ns-muted` to `text-foreground` over a `bg-surface` chip in 100ms and relaxes back on pointer-out."
  },
  "type": "registry:ui"
}