{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"log-explorer","type":"registry:block","title":"Log Explorer","description":"Log stream viewer with a volume histogram you click or drag across to narrow the time window, level filters, a text query with highlighted matches, a virtualized monospace table, live tail that follows new lines until you scroll up and then shows a Jump to latest pill, and a side panel with each line's structured fields as JSON.","author":"kara <ztzyrant@gmail.com>","dependencies":["@tanstack/react-virtual","clsx","lucide-react","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/log-explorer.tsx","type":"registry:component","target":"@components/motion/log-explorer.tsx","content":"\"use client\";\n// karaui.kamisama-v0.my.id/components/blocks/log-explorer\n\nimport { useVirtualizer } from \"@tanstack/react-virtual\";\nimport { ArrowDown, Search, X } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport {\n  type KeyboardEvent as ReactKeyboardEvent,\n  type ReactNode,\n  useCallback,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { ToggleGroup } from \"@/components/motion/segmented-control\";\nimport { EASE_OUT, SPRING_LAYOUT, SPRING_PANEL } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\n\nexport interface LogEntry {\n  id: string;\n  /** Date, epoch ms or ISO string. */\n  timestamp: Date | number | string;\n  level: LogLevel;\n  service: string;\n  message: string;\n  /** Structured context shown as JSON in the detail panel. */\n  fields?: Record<string, unknown>;\n}\n\nexport interface LogExplorerProps {\n  /** Log lines, oldest first (they are sorted defensively). */\n  logs: LogEntry[];\n  /** Controlled live-tail mode. While on, new lines follow the bottom. */\n  live?: boolean;\n  defaultLive?: boolean;\n  onLiveChange?: (live: boolean) => void;\n  /** Histogram buckets across the time span. Default 32. */\n  buckets?: number;\n  /** IANA zone for timestamps. Default \"UTC\" so server and client agree. */\n  timeZone?: string;\n  /** Default \"en-US\". */\n  locale?: string;\n  /** Height of the scrolling log body in px. Default 240. */\n  height?: number;\n  /** Fixed row height in px (rows are virtualized). Default 28. */\n  rowHeight?: number;\n  className?: string;\n}\n\nconst LEVELS: LogLevel[] = [\"debug\", \"info\", \"warn\", \"error\"];\n\n// Token colors mixed toward the foreground so text keeps contrast in both themes.\nconst LEVEL_BADGE: Record<LogLevel, string> = {\n  debug: \"bg-foreground/5 text-muted-foreground\",\n  info: \"bg-[color-mix(in_oklab,var(--chart-1)_14%,transparent)] text-[color-mix(in_oklab,var(--chart-1)_70%,var(--foreground))]\",\n  warn: \"bg-[color-mix(in_oklab,var(--warning)_18%,transparent)] text-[color-mix(in_oklab,var(--warning)_50%,var(--foreground))]\",\n  error:\n    \"bg-[color-mix(in_oklab,var(--destructive)_15%,transparent)] text-[color-mix(in_oklab,var(--destructive)_75%,var(--foreground))]\",\n};\nconst LEVEL_DOT: Record<LogLevel, string> = {\n  debug: \"bg-muted-foreground\",\n  info: \"bg-[var(--chart-1)]\",\n  warn: \"bg-[var(--warning)]\",\n  error: \"bg-[var(--destructive)]\",\n};\n\nfunction toMs(t: LogEntry[\"timestamp\"]) {\n  return t instanceof Date ? t.getTime() : typeof t === \"number\" ? t : Date.parse(t);\n}\n\nfunction escapeRegExp(s: string) {\n  return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n/** Wraps every case-insensitive occurrence of `query` in <mark>. */\nfunction highlight(text: string, query: string): ReactNode {\n  if (!query) return text;\n  const parts = text.split(new RegExp(`(${escapeRegExp(query)})`, \"gi\"));\n  return parts.map((part, i) =>\n    i % 2 === 1 ? (\n      // biome-ignore lint/suspicious/noArrayIndexKey: split segments never reorder.\n      <mark key={i} className=\"rounded-sm bg-[color-mix(in_oklab,var(--warning)_40%,transparent)] px-px text-foreground\">\n        {part}\n      </mark>\n    ) : (\n      part\n    ),\n  );\n}\n\nconst JSON_TOKEN = /(\"(?:\\\\.|[^\"\\\\])*\")(\\s*:)?|\\b(true|false|null)\\b|-?\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?/g;\n\n/** Pretty JSON with keys, strings, numbers and literals tinted by theme tokens. */\nfunction JsonView({ value }: { value: unknown }) {\n  const text = JSON.stringify(value, null, 2) ?? \"undefined\";\n  const out: ReactNode[] = [];\n  let last = 0;\n  for (const m of text.matchAll(JSON_TOKEN)) {\n    const at = m.index ?? 0;\n    if (at > last) out.push(text.slice(last, at));\n    const cls = m[2]\n      ? \"text-muted-foreground\"\n      : m[1]\n        ? \"text-[color-mix(in_oklab,var(--success)_60%,var(--foreground))]\"\n        : m[3]\n          ? \"text-[color-mix(in_oklab,var(--chart-5)_70%,var(--foreground))]\"\n          : \"text-[color-mix(in_oklab,var(--chart-1)_75%,var(--foreground))]\";\n    out.push(\n      <span key={at} className={cls}>\n        {m[1] ?? m[0]}\n      </span>,\n    );\n    if (m[2]) out.push(m[2]);\n    last = at + m[0].length;\n  }\n  out.push(text.slice(last));\n  return (\n    <pre className=\"overflow-x-auto rounded-lg border border-border bg-background/70 p-2 font-mono text-[11px] leading-relaxed [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\">\n      {out}\n    </pre>\n  );\n}\n\n/**\n * Log stream viewer: a volume histogram you can click or brush to narrow the\n * time window, level toggles, a text query with highlighted matches, and a\n * virtualized table. Live tail follows new lines (they slide in) until you\n * scroll up, then a \"Jump to latest\" pill counts what arrived. Clicking a row\n * opens a detail panel with the structured fields as JSON.\n */\nexport function LogExplorer({\n  logs,\n  live: liveProp,\n  defaultLive = false,\n  onLiveChange,\n  buckets: bucketCount = 32,\n  timeZone = \"UTC\",\n  locale = \"en-US\",\n  height = 240,\n  rowHeight = 28,\n  className,\n}: LogExplorerProps) {\n  const reduce = useReducedMotion() ?? false;\n  const baseId = useId();\n  const [liveState, setLiveState] = useState(defaultLive);\n  const live = liveProp ?? liveState;\n  const setLive = (next: boolean) => {\n    if (liveProp === undefined) setLiveState(next);\n    onLiveChange?.(next);\n  };\n\n  const [levels, setLevels] = useState<string[]>(LEVELS);\n  const [query, setQuery] = useState(\"\");\n  const [range, setRange] = useState<[number, number] | null>(null);\n  const [openId, setOpenId] = useState<string | null>(null);\n\n  const sorted = useMemo(\n    () =>\n      logs\n        .map((l) => ({ ...l, ms: toMs(l.timestamp) }))\n        .sort((a, b) => a.ms - b.ms),\n    [logs],\n  );\n  const q = query.trim();\n  const matches = useMemo(() => {\n    const lower = q.toLowerCase();\n    return sorted.filter(\n      (l) =>\n        levels.includes(l.level) &&\n        (!lower ||\n          l.message.toLowerCase().includes(lower) ||\n          l.service.toLowerCase().includes(lower)),\n    );\n  }, [sorted, levels, q]);\n\n  // Histogram over the full time span, counting lines that pass level + query.\n  const domain = useMemo<[number, number]>(() => {\n    if (!sorted.length) return [0, 1];\n    const lo = sorted[0].ms;\n    const hi = sorted[sorted.length - 1].ms;\n    return [lo, hi > lo ? hi : lo + 1];\n  }, [sorted]);\n  const bucketMs = (domain[1] - domain[0]) / bucketCount;\n  const histogram = useMemo(() => {\n    const bins = Array.from({ length: bucketCount }, () => ({ total: 0, warn: 0, error: 0 }));\n    for (const l of matches) {\n      const i = Math.min(bucketCount - 1, Math.floor((l.ms - domain[0]) / bucketMs));\n      bins[i].total++;\n      if (l.level === \"warn\") bins[i].warn++;\n      if (l.level === \"error\") bins[i].error++;\n    }\n    return bins;\n  }, [matches, domain, bucketMs, bucketCount]);\n  const peak = Math.max(1, ...histogram.map((b) => b.total));\n  const edge = (i: number) => domain[0] + i * bucketMs;\n  const inRange = (i: number) =>\n    range !== null && edge(i + 1) > range[0] && edge(i) < range[1];\n\n  const rows = useMemo(\n    () => (range ? matches.filter((l) => l.ms >= range[0] && l.ms < range[1]) : matches),\n    [matches, range],\n  );\n  const errorCount = rows.filter((r) => r.level === \"error\").length;\n\n  const timeFmt = useMemo(\n    () =>\n      new Intl.DateTimeFormat(locale, {\n        hour: \"2-digit\",\n        minute: \"2-digit\",\n        second: \"2-digit\",\n        fractionalSecondDigits: 3,\n        hourCycle: \"h23\",\n        timeZone,\n      }),\n    [locale, timeZone],\n  );\n  const shortFmt = useMemo(\n    () =>\n      new Intl.DateTimeFormat(locale, {\n        hour: \"2-digit\",\n        minute: \"2-digit\",\n        second: \"2-digit\",\n        hourCycle: \"h23\",\n        timeZone,\n      }),\n    [locale, timeZone],\n  );\n\n  // ── brushing ────────────────────────────────────────────────────────────\n  const brush = useRef<{ anchor: number; moved: boolean; prev: [number, number] | null } | null>(null);\n  const [focusBar, setFocusBar] = useState(0);\n  const barRefs = useRef<(HTMLButtonElement | null)[]>([]);\n  const selectBars = useCallback(\n    (a: number, b: number) => {\n      const lo = Math.min(a, b);\n      const hi = Math.max(a, b);\n      setRange([domain[0] + lo * bucketMs, domain[0] + (hi + 1) * bucketMs]);\n    },\n    [domain, bucketMs],\n  );\n  // Read through a ref so one window listener always sees the latest domain.\n  const brushEnd = useRef(() => {});\n  brushEnd.current = () => {\n    const b = brush.current;\n    brush.current = null;\n    if (!b || b.moved) return;\n    // A plain click on the bar that is already the whole selection clears it.\n    if (b.prev && b.prev[0] === edge(b.anchor) && b.prev[1] === edge(b.anchor + 1)) setRange(null);\n  };\n  useEffect(() => {\n    const end = () => brushEnd.current();\n    window.addEventListener(\"pointerup\", end);\n    window.addEventListener(\"pointercancel\", end);\n    return () => {\n      window.removeEventListener(\"pointerup\", end);\n      window.removeEventListener(\"pointercancel\", end);\n    };\n  }, []);\n  const onBarKey = (e: ReactKeyboardEvent<HTMLButtonElement>, i: number) => {\n    let next: number | null = null;\n    if (e.key === \"ArrowRight\") next = Math.min(bucketCount - 1, i + 1);\n    else if (e.key === \"ArrowLeft\") next = Math.max(0, i - 1);\n    else if (e.key === \"Home\") next = 0;\n    else if (e.key === \"End\") next = bucketCount - 1;\n    else if (e.key === \"Escape\" && range) {\n      e.preventDefault();\n      setRange(null);\n      return;\n    }\n    if (next === null) return;\n    e.preventDefault();\n    setFocusBar(next);\n    barRefs.current[next]?.focus();\n    if (e.shiftKey) selectBars(brush.current?.anchor ?? i, next);\n  };\n\n  // ── list, follow-the-tail ───────────────────────────────────────────────\n  const scrollRef = useRef<HTMLDivElement>(null);\n  const virtualizer = useVirtualizer({\n    count: rows.length,\n    getScrollElement: () => scrollRef.current,\n    estimateSize: () => rowHeight,\n    overscan: 8,\n  });\n  const stick = useRef(true);\n  const [atBottom, setAtBottom] = useState(true);\n  const [unseen, setUnseen] = useState(0);\n  const prev = useRef({ logs, count: rows.length });\n\n  const scrollToEnd = useCallback(() => {\n    const el = scrollRef.current;\n    if (!el) return;\n    el.scrollTop = el.scrollHeight;\n    stick.current = true;\n    setAtBottom(true);\n    setUnseen(0);\n  }, []);\n\n  // Lines that arrive after the first render slide in; the flag is read only\n  // when a row mounts, so ids are marked known right after each commit.\n  const known = useRef<Set<string> | null>(null);\n  if (known.current === null) known.current = new Set(logs.map((l) => l.id));\n  useEffect(() => {\n    for (const l of logs) known.current?.add(l.id);\n  }, [logs]);\n\n  useLayoutEffect(() => {\n    const p = prev.current;\n    prev.current = { logs, count: rows.length };\n    // Only new data counts; a filter change that reveals more rows doesn't.\n    if (p.logs === logs) return;\n    const added = rows.length - p.count;\n    if (added <= 0) return;\n    if (live && stick.current) {\n      const el = scrollRef.current;\n      if (el) el.scrollTop = el.scrollHeight;\n    } else if (!stick.current) {\n      setUnseen((n) => n + added);\n    }\n  }, [logs, rows.length, live]);\n\n  // Start at the newest line, like `tail`.\n  useLayoutEffect(() => {\n    const el = scrollRef.current;\n    if (el) el.scrollTop = el.scrollHeight;\n  }, []);\n\n  const onScroll = () => {\n    const el = scrollRef.current;\n    if (!el) return;\n    const bottom = el.scrollHeight - el.scrollTop - el.clientHeight < rowHeight;\n    stick.current = bottom;\n    setAtBottom(bottom);\n    if (bottom) setUnseen(0);\n  };\n\n  // ── detail panel ────────────────────────────────────────────────────────\n  const open = openId ? (sorted.find((l) => l.id === openId) ?? null) : null;\n  const panelRef = useRef<HTMLElement>(null);\n  const returnFocus = useRef<HTMLElement | null>(null);\n  const openRow = (id: string, el: HTMLElement) => {\n    returnFocus.current = el;\n    setOpenId(id);\n  };\n  const closePanel = () => {\n    setOpenId(null);\n    returnFocus.current?.focus({ preventScroll: true });\n  };\n  // Keyed by id: `open` is a fresh object on every live update.\n  useEffect(() => {\n    if (openId) requestAnimationFrame(() => panelRef.current?.focus());\n  }, [openId]);\n\n  const columns = \"grid-cols-[5.75rem_3.25rem_5.5rem_minmax(0,1fr)]\";\n  const virtualRows = virtualizer.getVirtualItems();\n\n  return (\n    <section\n      aria-label=\"Log explorer\"\n      className={cn(\n        \"flex w-full flex-col gap-3 rounded-2xl border border-border bg-card p-3 text-card-foreground\",\n        className,\n      )}\n    >\n      {/* Toolbar */}\n      <div className=\"flex flex-wrap items-center gap-2\">\n        <label className=\"relative flex min-w-36 flex-1 items-center\">\n          <span className=\"sr-only\">Filter messages</span>\n          <Search className=\"pointer-events-none absolute left-2.5 size-3.5 text-muted-foreground\" aria-hidden />\n          <input\n            type=\"search\"\n            value={query}\n            onChange={(e) => setQuery(e.target.value)}\n            placeholder=\"Filter messages…\"\n            className=\"h-9 w-full rounded-xl border border-border bg-background pr-2 pl-8 font-mono text-xs outline-none transition-colors placeholder:font-sans placeholder:text-muted-foreground hover:border-foreground/20 focus-visible:border-foreground/40 focus-visible:ring-2 focus-visible:ring-ring/40 [&::-webkit-search-cancel-button]:hidden\"\n          />\n        </label>\n        <ToggleGroup\n          type=\"multiple\"\n          size=\"sm\"\n          aria-label=\"Log levels\"\n          value={levels}\n          onValueChange={setLevels}\n          items={LEVELS.map((l) => ({\n            value: l,\n            label: l,\n            icon: <span className={cn(\"size-1.5 rounded-full\", LEVEL_DOT[l])} />,\n          }))}\n          className=\"p-0.5\"\n        />\n        <button\n          type=\"button\"\n          aria-pressed={live}\n          onClick={() => {\n            const next = !live;\n            setLive(next);\n            if (next) scrollToEnd();\n          }}\n          className={cn(\n            \"inline-flex h-9 items-center gap-1.5 rounded-xl border border-border px-2.5 text-xs font-medium outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring\",\n            live\n              ? \"border-[color-mix(in_oklab,var(--success)_45%,transparent)] bg-[color-mix(in_oklab,var(--success)_12%,transparent)] text-foreground\"\n              : \"border-border text-muted-foreground hover:text-foreground\",\n          )}\n        >\n          <span className=\"relative grid size-2 place-items-center\">\n            {live ? (\n              <motion.span\n                aria-hidden\n                className=\"absolute inset-0 rounded-full bg-[var(--success)]\"\n                animate={reduce ? { opacity: [1, 0.4, 1] } : { opacity: [0.7, 0], scale: [1, 2.4] }}\n                transition={{ duration: 1.4, ease: EASE_OUT, repeat: Number.POSITIVE_INFINITY }}\n              />\n            ) : null}\n            <span\n              aria-hidden\n              className={cn(\"relative size-2 rounded-full\", live ? \"bg-[var(--success)]\" : \"bg-muted-foreground/50\")}\n            />\n          </span>\n          Live\n        </button>\n      </div>\n\n      {/* Histogram — click a bar or drag across bars to narrow the window. */}\n      <div>\n        <div className=\"mb-1 flex items-center justify-between gap-2 text-[11px] text-muted-foreground\">\n          <span className=\"tabular-nums\" aria-live=\"polite\">\n            {rows.length.toLocaleString(locale)} lines\n            {errorCount ? (\n              <>\n                {\" · \"}\n                <span className=\"text-[color-mix(in_oklab,var(--destructive)_75%,var(--foreground))]\">\n                  {errorCount} errors\n                </span>\n              </>\n            ) : null}\n          </span>\n          <AnimatePresence initial={false}>\n            {range ? (\n              <motion.button\n                key=\"range\"\n                type=\"button\"\n                onClick={() => setRange(null)}\n                initial={{ opacity: 0, scale: reduce ? 1 : 0.9 }}\n                animate={{ opacity: 1, scale: 1 }}\n                exit={{ opacity: 0, transition: { duration: 0.12 } }}\n                transition={SPRING_LAYOUT}\n                className=\"inline-flex h-5 items-center gap-1 rounded-full bg-foreground/5 pr-1 pl-2 font-mono tabular-nums text-foreground outline-none hover:bg-foreground/10 focus-visible:ring-2 focus-visible:ring-ring\"\n              >\n                {shortFmt.format(range[0])}–{shortFmt.format(range[1])}\n                <X className=\"size-3\" aria-hidden />\n                <span className=\"sr-only\">Clear time range</span>\n              </motion.button>\n            ) : null}\n          </AnimatePresence>\n        </div>\n        {/* biome-ignore lint/a11y/useSemanticElements: a strip of toggle bars, not a form; group names it */}\n        <div\n          role=\"group\"\n          aria-label=\"Log volume over time. Arrow keys move, Enter selects, Shift extends.\"\n          className=\"flex h-12 touch-none items-end gap-px select-none\"\n        >\n          {histogram.map((bin, i) => {\n            const selected = inRange(i);\n            const dim = range !== null && !selected;\n            const label = `${shortFmt.format(edge(i))} to ${shortFmt.format(edge(i + 1))}: ${bin.total} lines, ${bin.error} errors`;\n            return (\n              <button\n                // biome-ignore lint/suspicious/noArrayIndexKey: buckets are positional.\n                key={i}\n                ref={(el) => {\n                  barRefs.current[i] = el;\n                }}\n                type=\"button\"\n                aria-label={label}\n                aria-pressed={selected}\n                title={label}\n                tabIndex={i === focusBar ? 0 : -1}\n                onFocus={() => setFocusBar(i)}\n                onKeyDown={(e) => onBarKey(e, i)}\n                onPointerDown={(e) => {\n                  if (e.button !== 0) return;\n                  // Touch captures the pointer to the first bar; release it so\n                  // pointerenter fires on the bars the finger crosses.\n                  e.currentTarget.releasePointerCapture?.(e.pointerId);\n                  brush.current = { anchor: i, moved: false, prev: range };\n                  selectBars(i, i);\n                }}\n                onPointerEnter={() => {\n                  const b = brush.current;\n                  if (!b) return;\n                  b.moved = b.moved || i !== b.anchor;\n                  selectBars(b.anchor, i);\n                }}\n                onClick={(e) => {\n                  // Keyboard activation (pointer clicks are handled above).\n                  if (e.detail !== 0) return;\n                  if (e.shiftKey && range) {\n                    const anchor = Math.floor((range[0] - domain[0]) / bucketMs);\n                    selectBars(anchor, i);\n                  } else if (selected && range && edge(i) === range[0] && edge(i + 1) === range[1]) {\n                    setRange(null);\n                  } else selectBars(i, i);\n                }}\n                className=\"group relative flex h-full min-w-0 flex-1 items-end rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n              >\n                <span\n                  aria-hidden\n                  className={cn(\n                    \"absolute inset-0 rounded-sm transition-colors\",\n                    selected ? \"bg-foreground/[0.06]\" : \"group-hover:bg-foreground/[0.04]\",\n                  )}\n                />\n                {/* scaleY from the bottom edge: transform only, HTML element. */}\n                <motion.span\n                  aria-hidden\n                  initial={false}\n                  animate={{ scaleY: bin.total / peak, opacity: dim ? 0.35 : 1 }}\n                  transition={reduce ? { duration: 0 } : { duration: 0.35, ease: EASE_OUT }}\n                  className=\"relative flex h-full w-full origin-bottom flex-col justify-end overflow-hidden rounded-t-xs\"\n                >\n                  <span\n                    className={cn(\"w-full flex-1\", selected ? \"bg-[var(--chart-1)]\" : \"bg-foreground/25\")}\n                  />\n                  {bin.total ? (\n                    <>\n                      <span\n                        className=\"w-full bg-[var(--warning)]\"\n                        style={{ height: `${(bin.warn / bin.total) * 100}%` }}\n                      />\n                      <span\n                        className=\"w-full bg-[var(--destructive)]\"\n                        style={{ height: `${(bin.error / bin.total) * 100}%` }}\n                      />\n                    </>\n                  ) : null}\n                </motion.span>\n              </button>\n            );\n          })}\n        </div>\n        <div className=\"mt-1 flex justify-between font-mono text-[10px] tabular-nums text-muted-foreground\">\n          <span>{sorted.length ? shortFmt.format(domain[0]) : \"\"}</span>\n          <span>{sorted.length ? shortFmt.format((domain[0] + domain[1]) / 2) : \"\"}</span>\n          <span>{sorted.length ? shortFmt.format(domain[1]) : \"\"}</span>\n        </div>\n      </div>\n\n      {/* Table */}\n      <div className=\"relative overflow-hidden rounded-xl border border-border bg-background/40\">\n        <div\n          aria-hidden\n          className={cn(\n            \"grid gap-2 border-b border-border px-2.5 py-1.5 text-[10px] font-semibold tracking-wide text-muted-foreground uppercase\",\n            columns,\n          )}\n        >\n          <span>Time ({timeZone === \"UTC\" ? \"UTC\" : \"local\"})</span>\n          <span>Level</span>\n          <span>Service</span>\n          <span>Message</span>\n        </div>\n        <div\n          ref={scrollRef}\n          onScroll={onScroll}\n          className=\"relative overflow-y-auto [scrollbar-color:var(--border-strong)_transparent] [scrollbar-width:thin]\"\n          style={{ height }}\n        >\n          {rows.length === 0 ? (\n            <p className=\"grid h-full place-items-center text-xs text-muted-foreground\">\n              No lines match these filters.\n            </p>\n          ) : (\n            <ul\n              aria-label={`${rows.length} log lines`}\n              className=\"relative w-full\"\n              style={{ height: virtualizer.getTotalSize() }}\n            >\n              {virtualRows.map((v) => {\n                const l = rows[v.index];\n                const fresh = !known.current?.has(l.id);\n                return (\n                  <motion.li\n                    key={l.id}\n                    initial={fresh ? { opacity: 0, y: reduce ? 0 : 8 } : false}\n                    animate={{ opacity: 1, y: 0 }}\n                    transition={{ duration: 0.28, ease: EASE_OUT }}\n                    className=\"absolute left-0 w-full\"\n                    style={{ top: v.start, height: rowHeight }}\n                  >\n                    <button\n                      type=\"button\"\n                      onClick={(e) => openRow(l.id, e.currentTarget)}\n                      aria-expanded={openId === l.id}\n                      className={cn(\n                        \"grid h-full w-full items-center gap-2 px-2.5 text-left text-xs outline-none transition-colors focus-visible:bg-foreground/[0.06] focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset\",\n                        columns,\n                        openId === l.id\n                          ? \"bg-foreground/[0.07]\"\n                          : l.level === \"error\"\n                            ? \"bg-[color-mix(in_oklab,var(--destructive)_6%,transparent)] hover:bg-[color-mix(in_oklab,var(--destructive)_10%,transparent)]\"\n                            : \"hover:bg-foreground/[0.04]\",\n                      )}\n                    >\n                      <time\n                        dateTime={new Date(l.ms).toISOString()}\n                        className=\"font-mono text-[11px] tabular-nums text-muted-foreground\"\n                      >\n                        {timeFmt.format(l.ms)}\n                      </time>\n                      <span\n                        className={cn(\n                          \"inline-flex h-4.5 w-fit items-center rounded-sm px-1 font-mono text-[10px] font-semibold uppercase\",\n                          LEVEL_BADGE[l.level],\n                        )}\n                      >\n                        {l.level}\n                      </span>\n                      <span className=\"truncate text-muted-foreground\">{highlight(l.service, q)}</span>\n                      <span className=\"truncate font-mono text-[11.5px] text-foreground\">{highlight(l.message, q)}</span>\n                    </button>\n                  </motion.li>\n                );\n              })}\n            </ul>\n          )}\n        </div>\n\n        <AnimatePresence>\n          {!atBottom && (live || unseen > 0) ? (\n            <motion.button\n              key=\"jump\"\n              type=\"button\"\n              onClick={scrollToEnd}\n              initial={{ opacity: 0, y: reduce ? 0 : 10 }}\n              animate={{ opacity: 1, y: 0 }}\n              exit={{ opacity: 0, y: reduce ? 0 : 6, transition: { duration: 0.14 } }}\n              transition={SPRING_PANEL}\n              className=\"absolute bottom-3 left-1/2 z-10 -ml-[4.5rem] inline-flex h-8 w-36 items-center justify-center gap-1.5 rounded-full bg-primary text-xs font-medium text-primary-foreground shadow-lg outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\"\n            >\n              <ArrowDown className=\"size-3.5\" aria-hidden />\n              Jump to latest\n              {unseen > 0 ? <span className=\"tabular-nums opacity-70\">+{unseen}</span> : null}\n            </motion.button>\n          ) : null}\n        </AnimatePresence>\n\n        <AnimatePresence>\n          {open ? (\n            <motion.aside\n              key=\"detail\"\n              ref={panelRef}\n              aria-labelledby={`${baseId}-detail`}\n              tabIndex={-1}\n              onKeyDown={(e) => {\n                if (e.key === \"Escape\") {\n                  e.stopPropagation();\n                  closePanel();\n                }\n              }}\n              initial={{ x: reduce ? 0 : \"100%\", opacity: reduce ? 0 : 1 }}\n              animate={{ x: 0, opacity: 1 }}\n              exit={{ x: reduce ? 0 : \"100%\", opacity: reduce ? 0 : 1, transition: { duration: 0.18, ease: EASE_OUT } }}\n              transition={reduce ? { duration: 0.15 } : SPRING_PANEL}\n              className=\"absolute inset-y-0 right-0 z-20 flex w-[min(20rem,85%)] flex-col gap-2 overflow-y-auto border-l border-border bg-card p-3 shadow-2xl outline-none [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\"\n            >\n              <div className=\"flex items-center justify-between gap-2\">\n                <span className=\"flex items-center gap-2\">\n                  <span\n                    className={cn(\n                      \"inline-flex h-5 items-center rounded-sm px-1.5 font-mono text-[10px] font-semibold uppercase\",\n                      LEVEL_BADGE[open.level],\n                    )}\n                  >\n                    {open.level}\n                  </span>\n                  <h4 id={`${baseId}-detail`} className=\"text-sm font-semibold\">\n                    {open.service}\n                  </h4>\n                </span>\n                <button\n                  type=\"button\"\n                  onClick={closePanel}\n                  aria-label=\"Close details\"\n                  className=\"grid size-7 place-items-center rounded-lg text-muted-foreground outline-none transition-colors hover:bg-foreground/5 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\"\n                >\n                  <X className=\"size-4\" aria-hidden />\n                </button>\n              </div>\n              <p className=\"font-mono text-xs break-words text-foreground\">{open.message}</p>\n              <dl className=\"grid grid-cols-[auto_minmax(0,1fr)] gap-x-3 gap-y-1 text-[11px]\">\n                <dt className=\"text-muted-foreground\">timestamp</dt>\n                <dd className=\"font-mono tabular-nums\">{new Date(open.ms).toISOString()}</dd>\n                <dt className=\"text-muted-foreground\">id</dt>\n                <dd className=\"truncate font-mono\">{open.id}</dd>\n              </dl>\n              <JsonView\n                value={{ level: open.level, service: open.service, message: open.message, ...open.fields }}\n              />\n            </motion.aside>\n          ) : null}\n        </AnimatePresence>\n      </div>\n    </section>\n  );\n}\n"},{"path":"components/motion/segmented-control.tsx","type":"registry:component","target":"@components/motion/segmented-control.tsx","content":"\"use client\";\n\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport {\n  useCallback,\n  useId,\n  useRef,\n  useState,\n  type KeyboardEvent,\n  type ReactNode,\n} from \"react\";\nimport { SPRING_LAYOUT, SPRING_PRESS, SPRING_SWAP } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\n// SegmentedControl vs Tabs: `Tabs` (tabs.tsx, variants pill/segment/underline)\n// switches *panels* with tablist/tab semantics. `SegmentedControl` is a form\n// control: it picks one *value* with radiogroup/radio semantics, can post that\n// value with a form via `name`, supports disabled options, icon-only options\n// and a full-width layout, and its thumb is a raised card surface rather than\n// a filled accent pill. Reach for Tabs when each option owns content below it.\n\nexport type SegmentedControlSize = \"sm\" | \"md\" | \"lg\";\n\nexport interface SegmentedControlOption {\n  value: string;\n  label?: ReactNode;\n  icon?: ReactNode;\n  disabled?: boolean;\n  /** Required when the option has only an icon. */\n  \"aria-label\"?: string;\n}\n\nconst trackSize: Record<SegmentedControlSize, string> = {\n  sm: \"h-8 p-0.5 text-xs\",\n  md: \"h-10 p-1 text-sm\",\n  lg: \"h-12 p-1 text-sm\",\n};\n\nconst itemSize: Record<SegmentedControlSize, string> = {\n  sm: \"gap-1.5 px-2.5 [&_svg]:size-3.5\",\n  md: \"gap-2 px-3.5 [&_svg]:size-4\",\n  lg: \"gap-2 px-5 [&_svg]:size-[18px]\",\n};\n\nfunction useControllable<T>(\n  value: T | undefined,\n  defaultValue: T,\n  onChange: ((next: T) => void) | undefined,\n) {\n  const [internal, setInternal] = useState<T>(defaultValue);\n  const controlled = value !== undefined;\n  const current = controlled ? value : internal;\n  const set = useCallback(\n    (next: T) => {\n      if (!controlled) setInternal(next);\n      onChange?.(next);\n    },\n    [controlled, onChange],\n  );\n  return [current, set] as const;\n}\n\n/** Index of the next enabled option from `from` in `dir`, wrapping around. */\nfunction nextEnabled(options: { disabled?: boolean }[], from: number, dir: 1 | -1) {\n  const n = options.length;\n  for (let step = 1; step <= n; step++) {\n    const i = (((from + dir * step) % n) + n) % n;\n    if (!options[i].disabled) return i;\n  }\n  return from;\n}\n\nexport interface SegmentedControlProps {\n  options: SegmentedControlOption[];\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  size?: SegmentedControlSize;\n  /** Stretch to the container width with equal-width segments. */\n  fullWidth?: boolean;\n  disabled?: boolean;\n  /** When set, a hidden input posts the value with the surrounding form. */\n  name?: string;\n  \"aria-label\"?: string;\n  \"aria-labelledby\"?: string;\n  className?: string;\n  thumbClassName?: string;\n}\n\nexport function SegmentedControl({\n  options,\n  value,\n  defaultValue,\n  onValueChange,\n  size = \"md\",\n  fullWidth = false,\n  disabled = false,\n  name,\n  className,\n  thumbClassName,\n  ...aria\n}: SegmentedControlProps) {\n  const [current, setValue] = useControllable(\n    value,\n    defaultValue ?? options.find((o) => !o.disabled)?.value ?? \"\",\n    onValueChange,\n  );\n  const reduce = useReducedMotion();\n  const layoutId = useId();\n  const refs = useRef<(HTMLButtonElement | null)[]>([]);\n  const items = options.map((o) => ({ ...o, disabled: disabled || o.disabled }));\n  const selectedIndex = items.findIndex((o) => o.value === current);\n  // Roving tab stop: the checked radio, or the first enabled one when none is.\n  const tabStop = selectedIndex >= 0 && !items[selectedIndex].disabled\n    ? selectedIndex\n    : items.findIndex((o) => !o.disabled);\n\n  const onKeyDown = (event: KeyboardEvent<HTMLButtonElement>, index: number) => {\n    let next: number | null = null;\n    switch (event.key) {\n      case \"ArrowRight\":\n      case \"ArrowDown\":\n        next = nextEnabled(items, index, 1);\n        break;\n      case \"ArrowLeft\":\n      case \"ArrowUp\":\n        next = nextEnabled(items, index, -1);\n        break;\n      case \"Home\":\n        next = nextEnabled(items, -1, 1);\n        break;\n      case \"End\":\n        next = nextEnabled(items, items.length, -1);\n        break;\n      default:\n        return;\n    }\n    event.preventDefault();\n    if (next === null || items[next].disabled) return;\n    // Selection follows focus, as the WAI-ARIA radio group pattern prescribes.\n    refs.current[next]?.focus();\n    if (items[next].value !== current) setValue(items[next].value);\n  };\n\n  return (\n    // layoutRoot keeps the thumb's shared-layout projection local, so scrolled\n    // or fixed ancestors never leak offsets into its glide.\n    <motion.div\n      layoutRoot\n      role=\"radiogroup\"\n      aria-disabled={disabled || undefined}\n      {...aria}\n      className={cn(\n        \"relative isolate items-stretch rounded-xl border border-border bg-muted\",\n        fullWidth ? \"flex w-full\" : \"inline-flex\",\n        trackSize[size],\n        disabled && \"opacity-60\",\n        className,\n      )}\n    >\n      {name ? <input type=\"hidden\" name={name} value={current} /> : null}\n      {items.map((option, index) => {\n        const checked = option.value === current;\n        const iconOnly = !option.label;\n        return (\n          <motion.button\n            key={option.value}\n            ref={(el) => {\n              refs.current[index] = el;\n            }}\n            type=\"button\"\n            role=\"radio\"\n            aria-checked={checked}\n            aria-label={option[\"aria-label\"]}\n            disabled={option.disabled}\n            tabIndex={index === tabStop ? 0 : -1}\n            data-state={checked ? \"checked\" : \"unchecked\"}\n            onClick={() => !checked && setValue(option.value)}\n            onKeyDown={(e) => onKeyDown(e, index)}\n            whileTap={reduce || option.disabled ? undefined : { scale: 0.96 }}\n            transition={SPRING_PRESS}\n            className={cn(\n              \"relative inline-flex min-w-0 select-none items-center justify-center whitespace-nowrap rounded-[9px] font-medium outline-none transition-colors duration-200\",\n              \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-muted\",\n              \"disabled:cursor-not-allowed disabled:opacity-50\",\n              fullWidth && \"flex-1\",\n              itemSize[size],\n              iconOnly && \"aspect-square px-0\",\n              checked\n                ? \"text-foreground\"\n                : \"text-muted-foreground enabled:hover:text-foreground\",\n            )}\n          >\n            {checked ? (\n              <motion.span\n                layoutId={layoutId}\n                aria-hidden\n                transition={reduce ? { duration: 0 } : SPRING_LAYOUT}\n                className={cn(\n                  \"absolute inset-0 -z-10 rounded-[9px] border border-border bg-card shadow-sm\",\n                  thumbClassName,\n                )}\n              />\n            ) : null}\n            {option.icon ? (\n              <span aria-hidden className=\"inline-flex shrink-0\">\n                {option.icon}\n              </span>\n            ) : null}\n            {option.label ? <span className=\"truncate\">{option.label}</span> : null}\n          </motion.button>\n        );\n      })}\n    </motion.div>\n  );\n}\n\n/* ------------------------------------------------------------------------ */\n/* ToggleGroup                                                               */\n/* ------------------------------------------------------------------------ */\n\nexport interface ToggleGroupItem {\n  value: string;\n  label?: ReactNode;\n  icon?: ReactNode;\n  disabled?: boolean;\n  /** Required when the item has only an icon. */\n  \"aria-label\"?: string;\n}\n\ntype ToggleGroupBase = {\n  items: ToggleGroupItem[];\n  size?: SegmentedControlSize;\n  disabled?: boolean;\n  \"aria-label\"?: string;\n  \"aria-labelledby\"?: string;\n  className?: string;\n};\n\nexport type ToggleGroupSingleProps = ToggleGroupBase & {\n  type: \"single\";\n  /** The pressed item, or `\"\"` when none is pressed. */\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  /** Disallow un-pressing the active item, so one is always pressed. */\n  required?: boolean;\n};\n\nexport type ToggleGroupMultipleProps = ToggleGroupBase & {\n  type: \"multiple\";\n  value?: string[];\n  defaultValue?: string[];\n  onValueChange?: (value: string[]) => void;\n};\n\nexport type ToggleGroupProps = ToggleGroupSingleProps | ToggleGroupMultipleProps;\n\nconst toggleSize: Record<SegmentedControlSize, string> = {\n  sm: \"h-7 min-w-7 gap-1.5 px-2 text-xs [&_svg]:size-3.5\",\n  md: \"h-9 min-w-9 gap-2 px-2.5 text-sm [&_svg]:size-4\",\n  lg: \"h-11 min-w-11 gap-2 px-3.5 text-sm [&_svg]:size-[18px]\",\n};\n\nexport function ToggleGroup(props: ToggleGroupProps) {\n  const { items, size = \"md\", disabled = false, className } = props;\n  const reduce = useReducedMotion();\n  const refs = useRef<(HTMLButtonElement | null)[]>([]);\n  const [focusIndex, setFocusIndex] = useState<number | null>(null);\n\n  // Normalise both modes to a string[] internally.\n  const single = props.type === \"single\";\n  const [current, setCurrent] = useControllable<string[]>(\n    props.value === undefined ? undefined : single ? (props.value ? [props.value as string] : []) : (props.value as string[]),\n    single\n      ? props.defaultValue\n        ? [props.defaultValue as string]\n        : []\n      : ((props.defaultValue as string[] | undefined) ?? []),\n    (next) => {\n      if (props.type === \"single\") props.onValueChange?.(next[0] ?? \"\");\n      else props.onValueChange?.(next);\n    },\n  );\n\n  const list = items.map((i) => ({ ...i, disabled: disabled || i.disabled }));\n  const firstEnabled = list.findIndex((i) => !i.disabled);\n  const tabStop =\n    focusIndex !== null && !list[focusIndex]?.disabled ? focusIndex : firstEnabled;\n\n  const toggle = (itemValue: string) => {\n    const pressed = current.includes(itemValue);\n    if (props.type === \"single\") {\n      if (pressed && props.required) return;\n      setCurrent(pressed ? [] : [itemValue]);\n    } else {\n      setCurrent(pressed ? current.filter((v) => v !== itemValue) : [...current, itemValue]);\n    }\n  };\n\n  const onKeyDown = (event: KeyboardEvent<HTMLButtonElement>, index: number) => {\n    let next: number | null = null;\n    if (event.key === \"ArrowRight\" || event.key === \"ArrowDown\") next = nextEnabled(list, index, 1);\n    else if (event.key === \"ArrowLeft\" || event.key === \"ArrowUp\") next = nextEnabled(list, index, -1);\n    else if (event.key === \"Home\") next = nextEnabled(list, -1, 1);\n    else if (event.key === \"End\") next = nextEnabled(list, list.length, -1);\n    if (next === null) return;\n    event.preventDefault();\n    // Focus moves; pressing stays on Space/Enter (toggle buttons, not radios).\n    setFocusIndex(next);\n    refs.current[next]?.focus();\n  };\n\n  return (\n    <motion.div\n      role=\"group\"\n      aria-label={props[\"aria-label\"]}\n      aria-labelledby={props[\"aria-labelledby\"]}\n      className={cn(\n        \"inline-flex items-center gap-0.5 rounded-xl border border-border bg-card p-1\",\n        disabled && \"opacity-60\",\n        className,\n      )}\n    >\n      {list.map((item, index) => {\n        const pressed = current.includes(item.value);\n        return (\n          <motion.button\n            key={item.value}\n            ref={(el) => {\n              refs.current[index] = el;\n            }}\n            type=\"button\"\n            aria-pressed={pressed}\n            aria-label={item[\"aria-label\"]}\n            disabled={item.disabled}\n            tabIndex={index === tabStop ? 0 : -1}\n            data-state={pressed ? \"on\" : \"off\"}\n            onClick={() => {\n              setFocusIndex(index);\n              toggle(item.value);\n            }}\n            onFocus={() => setFocusIndex(index)}\n            onKeyDown={(e) => onKeyDown(e, index)}\n            whileTap={reduce || item.disabled ? undefined : { scale: 0.88 }}\n            transition={SPRING_PRESS}\n            className={cn(\n              \"relative isolate inline-flex select-none items-center justify-center rounded-lg font-medium outline-none transition-colors duration-200\",\n              \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-card\",\n              \"disabled:cursor-not-allowed disabled:opacity-50\",\n              toggleSize[size],\n              pressed\n                ? \"text-foreground\"\n                : \"text-muted-foreground enabled:hover:bg-muted/60 enabled:hover:text-foreground\",\n            )}\n          >\n            <AnimatePresence initial={false}>\n              {pressed ? (\n                <motion.span\n                  key=\"on\"\n                  aria-hidden\n                  initial={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.6 }}\n                  animate={{ opacity: 1, scale: 1 }}\n                  exit={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.75, transition: { duration: 0.12 } }}\n                  transition={SPRING_SWAP}\n                  className=\"absolute inset-0 -z-10 rounded-lg bg-muted ring-1 ring-border-strong\"\n                />\n              ) : null}\n            </AnimatePresence>\n            {item.icon ? (\n              <span aria-hidden className=\"inline-flex shrink-0\">\n                {item.icon}\n              </span>\n            ) : null}\n            {item.label ? <span>{item.label}</span> : null}\n          </motion.button>\n        );\n      })}\n    </motion.div>\n  );\n}\n"},{"path":"lib/ease.ts","type":"registry:lib","target":"@lib/ease.ts","content":"// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\n\n/** Dragged handles and fills (sliders) — critically damped `useSpring` config,\n * so the value follows the pointer butterily and never rebounds off an end. */\nexport const SPRING_GLIDE = {\n  stiffness: 700,\n  damping: 50,\n  mass: 0.5,\n} as const;\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"}]}