{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "log-stream",
  "type": "registry:component",
  "title": "Log Stream",
  "description": "Live logs that stay readable: level filters with counts, search that highlights as it filters, pause and follow with a “new lines” pill, and bursts of the same error folded into one “12 similar” row.",
  "categories": [
    "component",
    "Dev Tools"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/devl/log-stream.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { ArrowDownIcon, ChevronRightIcon, PauseIcon, PlayIcon, SearchIcon, XIcon } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { type ReactNode, useEffect, useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\n\nexport interface LogLine {\n  id: string;\n  ts: number;\n  level: LogLevel;\n  service: string;\n  message: string;\n}\n\nexport interface LogStreamProps {\n  initialLines?: LogLine[];\n  subscribe?: (push: (line: LogLine) => void) => () => void;\n  limit?: number;\n  foldAfter?: number;\n  title?: string;\n  className?: string;\n}\n\nconst LEVELS: { id: LogLevel; label: string; short: string; text: string; dot: string }[] = [\n  { id: \"debug\", label: \"Debug\", short: \"DBG\", text: \"text-muted-foreground\", dot: \"bg-muted-foreground/60\" },\n  { id: \"info\", label: \"Info\", short: \"INF\", text: \"text-info-foreground\", dot: \"bg-info\" },\n  { id: \"warn\", label: \"Warn\", short: \"WRN\", text: \"text-warning-foreground\", dot: \"bg-warning\" },\n  { id: \"error\", label: \"Error\", short: \"ERR\", text: \"text-destructive-foreground\", dot: \"bg-destructive\" },\n];\nconst levelInfo = Object.fromEntries(LEVELS.map((l) => [l.id, l])) as Record<LogLevel, (typeof LEVELS)[number]>;\n\nconst template = (m: string) => m.replace(/\\b[0-9a-f]{6,}\\b/gi, \"#\").replace(/\\d+(\\.\\d+)?/g, \"#\");\n\nconst pad = (n: number, w = 2) => String(n).padStart(w, \"0\");\nconst clock = (ts: number) => {\n  const d = new Date(ts);\n  return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}`;\n};\n\nfunction Mark({ text, q }: { text: string; q: string }) {\n  if (!q) return <>{text}</>;\n  const out: ReactNode[] = [];\n  const lower = text.toLowerCase();\n  let at = 0;\n  let hit = lower.indexOf(q);\n  while (hit !== -1) {\n    out.push(text.slice(at, hit));\n    out.push(\n      <mark className=\"rounded-[calc(var(--radius)*0.3)] bg-warning/30 px-px text-inherit dark:bg-warning/35\" key={hit}>\n        {text.slice(hit, hit + q.length)}\n      </mark>,\n    );\n    at = hit + q.length;\n    hit = lower.indexOf(q, at);\n  }\n  out.push(text.slice(at));\n  return <>{out}</>;\n}\n\ntype Item = { kind: \"line\"; line: LogLine } | { kind: \"group\"; id: string; lines: LogLine[] };\n\nfunction Row({ line, q, nested, fresh }: { line: LogLine; q: string; nested?: boolean; fresh: boolean }) {\n  const reduce = useReducedMotion();\n  const lvl = levelInfo[line.level];\n  return (\n    <motion.div\n      animate={{ opacity: 1, y: 0 }}\n      className={cn(\n        \"grid grid-cols-[88px_30px_minmax(0,1fr)] items-baseline gap-x-2.5 px-3 py-[3px] sm:grid-cols-[88px_30px_64px_minmax(0,1fr)]\",\n        line.level === \"error\" && !nested && \"bg-destructive/[0.06]\",\n        line.level === \"warn\" && !nested && \"bg-warning/[0.06]\",\n        nested && \"ps-6\",\n      )}\n      initial={fresh && !reduce ? { opacity: 0, y: 4 } : false}\n      transition={{ duration: 0.16, ease: [0.23, 1, 0.32, 1] }}\n    >\n      <span className=\"text-muted-foreground tabular-nums\">{clock(line.ts)}</span>\n      <span className={cn(\"font-semibold\", lvl.text)}>{lvl.short}</span>\n      <span className=\"truncate text-muted-foreground max-sm:hidden\">{line.service}</span>\n      <span className=\"min-w-0 break-words\">\n        <Mark q={q} text={line.message} />\n      </span>\n    </motion.div>\n  );\n}\n\nexport function LogStream({ initialLines = [], subscribe, limit = 500, foldAfter = 3, title = \"Logs\", className }: LogStreamProps) {\n  const reduce = useReducedMotion();\n  const [lines, setLines] = useState<LogLine[]>(initialLines);\n  const [shownCount, setShownCount] = useState(initialLines.length);\n  const [paused, setPaused] = useState(false);\n  const [follow, setFollow] = useState(true);\n  const [levels, setLevels] = useState<Set<LogLevel>>(new Set([\"debug\", \"info\", \"warn\", \"error\"]));\n  const [query, setQuery] = useState(\"\");\n  const [open, setOpen] = useState<Set<string>>(new Set());\n  const [rate, setRate] = useState(0);\n  const listRef = useRef<HTMLDivElement>(null);\n  const seen = useRef(new Set<string>(initialLines.map((l) => l.id)));\n  const pausedRef = useRef(paused);\n  pausedRef.current = paused;\n\n  useEffect(() => {\n    if (!subscribe) return;\n    setLines(initialLines);\n    return subscribe((line) => {\n      setLines((ls) => {\n        const next = [...ls, line];\n        return next.length > limit ? next.slice(next.length - limit) : next;\n      });\n    });\n  }, [subscribe, limit]);\n\n  const linesRef = useRef(lines);\n  linesRef.current = lines;\n  useEffect(() => {\n    const t = setInterval(() => {\n      const now = Date.now();\n      const ls = linesRef.current;\n      let n = 0;\n      for (let i = ls.length - 1; i >= 0 && now - ls[i].ts < 5000; i--) n++;\n      setRate(n / 5);\n    }, 1000);\n    return () => clearInterval(t);\n  }, []);\n\n  useEffect(() => {\n    if (!paused) setShownCount(lines.length);\n  }, [lines.length, paused]);\n  const visible = paused ? lines.slice(0, Math.min(shownCount, lines.length)) : lines;\n  const newWhilePaused = paused ? Math.max(0, lines.length - shownCount) : 0;\n\n  const q = query.trim().toLowerCase();\n  const counts = useMemo(() => {\n    const c: Record<LogLevel, number> = { debug: 0, info: 0, warn: 0, error: 0 };\n    for (const l of visible) c[l.level]++;\n    return c;\n  }, [visible]);\n\n  const filtered = useMemo(\n    () => visible.filter((l) => levels.has(l.level) && (!q || l.message.toLowerCase().includes(q) || l.service.includes(q))),\n    [visible, levels, q],\n  );\n\n  const items = useMemo(() => {\n    const out: Item[] = [];\n    let i = 0;\n    while (i < filtered.length) {\n      const first = filtered[i];\n      const key = `${first.level}|${first.service}|${template(first.message)}`;\n      let j = i + 1;\n      while (j < filtered.length && `${filtered[j].level}|${filtered[j].service}|${template(filtered[j].message)}` === key) j++;\n      if (j - i >= foldAfter) out.push({ kind: \"group\", id: first.id, lines: filtered.slice(i, j) });\n      else for (let k = i; k < j; k++) out.push({ kind: \"line\", line: filtered[k] });\n      i = j;\n    }\n    return out;\n  }, [filtered, foldAfter]);\n\n  const freshIds = useMemo(() => {\n    const fresh = new Set<string>();\n    for (const l of filtered) if (!seen.current.has(l.id)) fresh.add(l.id);\n    return fresh;\n  }, [filtered]);\n  useEffect(() => {\n    for (const id of freshIds) seen.current.add(id);\n  }, [freshIds]);\n\n  const [behind, setBehind] = useState(0);\n  const lastLen = useRef(filtered.length);\n  useLayoutEffect(() => {\n    const el = listRef.current;\n    const added = filtered.length - lastLen.current;\n    lastLen.current = filtered.length;\n    if (!el) return;\n    if (follow && !paused) {\n      el.scrollTop = el.scrollHeight;\n      setBehind(0);\n    } else if (added > 0 && !paused) setBehind((b) => b + added);\n  }, [filtered.length, follow, paused, items.length]);\n\n  const jump = () => {\n    setPaused(false);\n    setFollow(true);\n    setBehind(0);\n    requestAnimationFrame(() => {\n      const el = listRef.current;\n      if (el) el.scrollTo({ top: el.scrollHeight, behavior: reduce ? \"auto\" : \"smooth\" });\n    });\n  };\n\n  const toggleLevel = (lvl: LogLevel) =>\n    setLevels((s) => {\n      const n = new Set(s);\n      if (n.has(lvl) && n.size > 1) n.delete(lvl);\n      else n.add(lvl);\n      return n;\n    });\n\n  const pending = paused ? newWhilePaused : behind;\n\n  return (\n    <div className={cn(\"flex w-full min-w-0 max-w-[640px] flex-col overflow-hidden rounded-2xl border bg-card text-sm shadow-xs/5\", className)}>\n      <div className=\"flex flex-wrap items-center gap-2 border-b px-3 py-2.5 sm:flex-nowrap\">\n        <div aria-label=\"Levels\" className=\"flex shrink-0 items-center gap-1\" role=\"group\">\n          {LEVELS.map((l) => {\n            const on = levels.has(l.id);\n            return (\n              <button\n                aria-pressed={on}\n                className={cn(\n                  \"inline-flex h-8 items-center gap-1.5 rounded-full border px-2.5 text-xs outline-none transition-[background-color,border-color,color,scale] duration-150 focus-visible:ring-2 focus-visible:ring-ring active:scale-[0.97]\",\n                  on ? \"border-border bg-background text-foreground shadow-xs/5 dark:bg-input/32\" : \"border-transparent text-muted-foreground/70 [@media(hover:hover)]:hover:text-foreground\",\n                )}\n                key={l.id}\n                onClick={() => toggleLevel(l.id)}\n                type=\"button\"\n              >\n                <span className={cn(\"size-1.5 rounded-full transition-opacity max-sm:hidden\", l.dot, !on && \"opacity-40\")} />\n                {l.label}\n                <span className=\"text-muted-foreground tabular-nums\">{counts[l.id]}</span>\n              </button>\n            );\n          })}\n        </div>\n        <div className=\"flex min-w-0 flex-1 items-center gap-1.5 max-sm:basis-full\">\n          <label className=\"relative flex h-8 min-w-0 flex-1 items-center rounded-lg border border-input bg-background shadow-xs/5 ring-ring/24 transition-shadow has-focus-visible:border-ring has-focus-visible:ring-[3px] dark:bg-input/32\">\n            <SearchIcon className=\"ms-2 size-3.5 shrink-0 text-muted-foreground\" />\n            <input\n              aria-label=\"Search logs\"\n              className=\"h-full min-w-0 flex-1 bg-transparent px-1.5 text-xs outline-none placeholder:text-muted-foreground/72\"\n              onChange={(e) => setQuery(e.target.value)}\n              onKeyDown={(e) => e.key === \"Escape\" && setQuery(\"\")}\n              placeholder=\"Search\"\n              value={query}\n            />\n            {query && (\n              <button aria-label=\"Clear search\" className=\"me-1 grid size-6 place-items-center rounded-md text-muted-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring [@media(hover:hover)]:hover:text-foreground\" onClick={() => setQuery(\"\")} type=\"button\">\n                <XIcon className=\"size-3\" />\n              </button>\n            )}\n          </label>\n          <button\n            aria-label={paused ? \"Resume stream\" : \"Pause stream\"}\n            aria-pressed={paused}\n            className=\"inline-flex h-8 shrink-0 items-center gap-1.5 rounded-lg border border-input bg-background px-2.5 text-xs shadow-xs/5 outline-none transition-[background-color,scale] duration-150 focus-visible:ring-2 focus-visible:ring-ring active:scale-[0.97] dark:bg-input/32 [@media(hover:hover)]:hover:bg-accent\"\n            onClick={() => (paused ? jump() : setPaused(true))}\n            type=\"button\"\n          >\n            {paused ? <PlayIcon className=\"size-3\" /> : <PauseIcon className=\"size-3\" />}\n            <span className=\"w-11 text-start\">{paused ? \"Resume\" : \"Pause\"}</span>\n          </button>\n        </div>\n      </div>\n\n      <div className=\"relative\">\n        <div\n          aria-label={title}\n          aria-live=\"off\"\n          className=\"h-[320px] overflow-y-auto py-1 font-mono text-[11.5px] leading-[18px] outline-none [scrollbar-width:thin] focus-visible:shadow-[inset_0_0_0_2px_var(--color-ring)]\"\n          onScroll={(e) => {\n            const el = e.currentTarget;\n            const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 12;\n            if (atBottom !== follow) setFollow(atBottom);\n            if (atBottom) setBehind(0);\n          }}\n          ref={listRef}\n          role=\"log\"\n          tabIndex={0}\n        >\n          {items.length === 0 && (\n            <div className=\"grid h-full place-items-center px-6 text-center font-sans\">\n              <div>\n                <p className=\"font-medium text-sm\">{q ? `No lines match “${query.trim()}”` : \"Waiting for logs…\"}</p>\n                <p className=\"mt-1 text-muted-foreground text-xs\">{q ? \"Try fewer words or turn on more levels.\" : \"New lines show up here as they arrive.\"}</p>\n              </div>\n            </div>\n          )}\n          {items.map((item) => {\n            if (item.kind === \"line\") return <Row fresh={freshIds.has(item.line.id)} key={item.line.id} line={item.line} q={q} />;\n            const isOpen = open.has(item.id);\n            const last = item.lines[item.lines.length - 1];\n            const lvl = levelInfo[last.level];\n            const freshGroup = item.lines.some((l) => freshIds.has(l.id));\n            return (\n              <div className={cn(last.level === \"error\" ? \"bg-destructive/[0.06]\" : last.level === \"warn\" ? \"bg-warning/[0.06]\" : \"\")} key={item.id}>\n                <button\n                  aria-expanded={isOpen}\n                  className=\"grid w-full grid-cols-[88px_30px_minmax(0,1fr)] items-baseline gap-x-2.5 px-3 py-[3px] text-start outline-none focus-visible:shadow-[inset_0_0_0_2px_var(--color-ring)] sm:grid-cols-[88px_30px_64px_minmax(0,1fr)] [@media(hover:hover)]:hover:bg-accent/40\"\n                  onClick={() =>\n                    setOpen((s) => {\n                      const n = new Set(s);\n                      if (n.has(item.id)) n.delete(item.id);\n                      else n.add(item.id);\n                      return n;\n                    })\n                  }\n                  type=\"button\"\n                >\n                  <span className=\"text-muted-foreground tabular-nums\">{clock(last.ts)}</span>\n                  <span className={cn(\"font-semibold\", lvl.text)}>{lvl.short}</span>\n                  <span className=\"truncate text-muted-foreground max-sm:hidden\">{last.service}</span>\n                  <span className=\"flex min-w-0 items-baseline gap-2\">\n                    <span className=\"min-w-0 truncate\">\n                      <Mark q={q} text={last.message} />\n                    </span>\n                    <motion.span\n                      animate={{ scale: 1 }}\n                      className={cn(\n                        \"inline-flex shrink-0 items-center gap-0.5 self-center rounded-full px-1.5 font-sans font-medium text-[10.5px] tabular-nums leading-4\",\n                        last.level === \"error\" ? \"bg-destructive/12 text-destructive-foreground\" : \"bg-muted text-muted-foreground\",\n                      )}\n                      initial={freshGroup && !reduce ? { scale: 1.15 } : false}\n                      key={item.lines.length}\n                      transition={{ type: \"spring\", stiffness: 600, damping: 20 }}\n                    >\n                      <motion.span animate={{ rotate: isOpen ? 90 : 0 }} className=\"grid place-items-center\" transition={{ duration: 0.15 }}>\n                        <ChevronRightIcon className=\"size-2.5\" />\n                      </motion.span>\n                      {item.lines.length} similar\n                    </motion.span>\n                  </span>\n                </button>\n                <AnimatePresence initial={false}>\n                  {isOpen && (\n                    <motion.div\n                      animate={{ height: \"auto\", opacity: 1 }}\n                      className=\"overflow-hidden border-destructive/20 border-s-2 ms-3\"\n                      exit={{ height: 0, opacity: 0 }}\n                      initial={{ height: 0, opacity: 0 }}\n                      transition={reduce ? { duration: 0 } : { duration: 0.2, ease: [0.23, 1, 0.32, 1] }}\n                    >\n                      {item.lines.map((l) => (\n                        <Row fresh={false} key={l.id} line={l} nested q={q} />\n                      ))}\n                    </motion.div>\n                  )}\n                </AnimatePresence>\n              </div>\n            );\n          })}\n        </div>\n\n        <AnimatePresence>\n          {pending > 0 && (\n            <motion.button\n              animate={{ opacity: 1, y: 0, scale: 1 }}\n              className=\"absolute bottom-3 left-1/2 inline-flex h-7 -translate-x-1/2 items-center gap-1.5 rounded-full bg-primary px-3 font-medium text-primary-foreground text-xs shadow-lg/10 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 active:scale-[0.97]\"\n              exit={{ opacity: 0, y: 6, scale: 0.96 }}\n              initial={reduce ? { opacity: 0 } : { opacity: 0, y: 6, scale: 0.96 }}\n              onClick={jump}\n              transition={{ duration: 0.18, ease: [0.23, 1, 0.32, 1] }}\n              type=\"button\"\n            >\n              <ArrowDownIcon className=\"size-3\" />\n              <span className=\"tabular-nums\">\n                {pending} new {pending === 1 ? \"line\" : \"lines\"}\n              </span>\n            </motion.button>\n          )}\n        </AnimatePresence>\n      </div>\n\n      <div className=\"flex h-9 items-center gap-2 border-t px-3 text-muted-foreground text-xs\">\n        <span className=\"relative flex size-2 shrink-0\">\n          {!paused && <span className=\"absolute inset-0 animate-ping rounded-full bg-success/50 motion-reduce:hidden\" />}\n          <span className={cn(\"relative size-2 rounded-full\", paused ? \"bg-muted-foreground/50\" : \"bg-success\")} />\n        </span>\n        <span className=\"min-w-0 flex-1 truncate\">\n          {paused ? \"Paused · buffering\" : follow ? \"Live · following\" : \"Live · scrolled back\"}\n          <span className=\"tabular-nums\"> · {rate.toFixed(1)} lines/s</span>\n        </span>\n        <span className=\"shrink-0 tabular-nums\">\n          {q ? `${filtered.length} of ${visible.length}` : `${lines.length.toLocaleString(\"en-US\")} lines`}\n        </span>\n      </div>\n    </div>\n  );\n}\n"
    }
  ]
}