{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "meter-latency-capillary",
  "title": "Meter Latency Capillary",
  "description": "Time-to-first-token rendered as calibrated capillary rise: a narrow tube climbs to a scribed p50 line, holds and trembles if it's running slow, and only surfaces retry/switch-model once it's genuinely past p95. Never fakes progress.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/meter-latency-capillary/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\n\n// ---------------------------------------------------------------------------\n// MeniscusHold — time-to-first-token rendered as calibrated capillary rise.\n// A narrow tube fills toward a scribed line marking the historical p50\n// first-token latency for this model/tool combo; the rise is eased so the\n// meniscus reaches that line exactly when a response typically lands. It\n// never fakes progress past what it can justify: reaching the p50 line with\n// nothing having arrived doesn't keep climbing toward \"done\" — the liquid\n// HOLDS there and trembles (an honest \"still waiting, later than usual\"\n// signal) while a second scribe line for p95 fades in above it. Past p95 the\n// tremble stops — trembling forever would itself be a lie about how confident\n// the wait still is — and a stall affordance (retry / switch model) surfaces.\n// First token landing (`arrivedAt` set) drains the tube fast, ease-out-expo,\n// from wherever the level currently sits.\n//\n// Three states, three honest phrases, announced only on the transition\n// between them (role=status, not a running commentary):\n//   waiting -> \"Waiting, typically N seconds.\"\n//   slow    -> \"Taking longer than usual.\"\n//   stalled -> \"May be stalled, retry available.\"\n// Distinct from status-glyph-cadence (liveness — the connection is alive, no\n// notion of \"how long is normal\") and password-strength-tide (liquid level\n// encodes input strength, not elapsed-time-vs-percentile). This is the only\n// one of the three built from a calibrated latency distribution.\n//\n// Timing is derived from `startedAt`/`p50Ms`/`p95Ms` via two scheduled\n// setTimeouts (not a rAF poll — there's nothing to simulate physically here,\n// just two thresholds), so a late mount (component created a few hundred ms\n// after the real request began) still lands on the correct phase and eases\n// in for whatever time remains, rather than replaying the whole rise from\n// zero. Pure DOM + CSS + SVG: the fill is a div, the meniscus is a 1-path SVG\n// riding its top edge, both colored from --foreground/--ns-muted, no canvas.\n// prefers-reduced-motion drops the rise/tremor/fade entirely and swaps in a\n// static three-segment strip (within-normal / slow / stalled) — the same\n// phase machine, same announcements, nothing depends on perceiving motion.\n// ---------------------------------------------------------------------------\n\nexport type MeniscusPhase = \"waiting\" | \"slow\" | \"stalled\" | \"arrived\";\n\nconst TUBE_H = 48; // px, per spec\nconst TUBE_W = 6; // px, per spec\nconst DRAIN_MS = 380;\nconst DRAIN_EASE = \"cubic-bezier(0.16,1,0.3,1)\"; // ease-out-expo\nconst RISE_EASE = \"cubic-bezier(0.22,1,0.36,1)\"; // gentle decelerate, distinct from drain\nconst P95_FADE_MS_FLOOR = 250;\n\nfunction useReducedMotion() {\n  const [reduced, setReduced] = useState(false);\n  useEffect(() => {\n    const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    setReduced(mq.matches);\n    const onChange = () => setReduced(mq.matches);\n    mq.addEventListener(\"change\", onChange);\n    return () => mq.removeEventListener(\"change\", onChange);\n  }, []);\n  return reduced;\n}\n\nfunction fmtSeconds(ms: number): string {\n  const safe = Number.isFinite(ms) && ms > 0 ? ms : 0;\n  return (safe / 1000).toFixed(1);\n}\n\n// A fraction as a percentage string, fixed to 4 decimals. The raw value\n// serialized as \"43.3333%\" on the server and \"43.333333333333336%\" on the\n// client, and React discards the subtree over a string that long. Four\n// decimals is far below one device pixel on a tube this size.\nfunction pct(frac: number): string {\n  return (frac * 100).toFixed(4);\n}\n\n// which of the two timed phases (ignoring `arrivedAt`) a given elapsed-ms\n// falls in, for a p50/p95 pair\nfunction timedPhase(elapsedMs: number, p50Ms: number, p95Ms: number): \"waiting\" | \"slow\" | \"stalled\" {\n  if (elapsedMs < p50Ms) return \"waiting\";\n  if (elapsedMs < p95Ms) return \"slow\";\n  return \"stalled\";\n}\n\nconst CSS = `\n.ns-mh-tremor { animation: ns-mh-tremor 500ms ease-in-out infinite; }\n@keyframes ns-mh-tremor {\n  0%, 100% { transform: translateY(0); }\n  50% { transform: translateY(0.5px); }\n}\n@media (prefers-reduced-motion: reduce) {\n  .ns-mh-tremor { animation: none; }\n}\n`;\n\nexport interface MeniscusHoldProps {\n  /** ms epoch timestamp this request/turn began. Defaults to Date.now() at mount. */\n  startedAt?: number;\n  /** historical p50 (typical) first-token latency in ms, e.g. from a rolling latency store */\n  p50Ms: number;\n  /** historical p95 first-token latency in ms — should be greater than p50Ms */\n  p95Ms: number;\n  /** ms epoch timestamp the first token actually arrived; null/undefined while still waiting */\n  arrivedAt?: number | null;\n  /** fired when the user presses Retry after a stall */\n  onRetry?: () => void;\n  /** fired when the user presses \"Switch model\" after a stall */\n  onSwitchModel?: () => void;\n  /** accessible name for the root status region. Default \"Time to first token\" */\n  label?: string;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}\n\nexport function MeniscusHold({\n  startedAt,\n  p50Ms,\n  p95Ms,\n  arrivedAt,\n  onRetry,\n  onSwitchModel,\n  label = \"Time to first token\",\n  className = \"\",\n}: MeniscusHoldProps) {\n  const reduced = useReducedMotion();\n  const [autoStart] = useState(() => Date.now());\n  const start = startedAt ?? autoStart;\n  const safeP50 = p50Ms > 0 ? p50Ms : 1;\n  const safeP95 = p95Ms > safeP50 ? p95Ms : safeP50 + 1;\n\n  const [timed, setTimed] = useState<\"waiting\" | \"slow\" | \"stalled\">(() =>\n    timedPhase(Date.now() - start, safeP50, safeP95)\n  );\n\n  // re-arm the two threshold timers whenever a new request starts. A late\n  // mount schedules only whatever time remains, so a component created\n  // 300ms into a request still crosses p50 300ms early, not late.\n  useEffect(() => {\n    const elapsed = Date.now() - start;\n    setTimed(timedPhase(elapsed, safeP50, safeP95));\n    const toSlow = safeP50 - elapsed;\n    const toStalled = safeP95 - elapsed;\n    const timers: number[] = [];\n    if (toSlow > 0) {\n      timers.push(window.setTimeout(() => setTimed(\"slow\"), toSlow));\n    }\n    if (toStalled > 0) {\n      timers.push(window.setTimeout(() => setTimed(\"stalled\"), toStalled));\n    }\n    return () => timers.forEach((t) => window.clearTimeout(t));\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [start, safeP50, safeP95]);\n\n  const phase: MeniscusPhase = arrivedAt ? \"arrived\" : timed;\n\n  // fill height, as a fraction of the tube, held at the p50 line once\n  // reached — it never keeps climbing toward p95, that would be fake\n  // progress. p95 anchors the very top of the tube.\n  const p50Frac = Math.max(0, Math.min(1, safeP50 / safeP95));\n  // The first client render must serialize EXACTLY what the server sent, and\n  // the server cannot know how many ms elapsed before the browser hydrated —\n  // any clock read here produced transition-duration 650ms against the 649ms\n  // already committed to the DOM. So the elapsed clock is not read during\n  // render at all: both sides render the full p50 duration, and the real\n  // remaining time is installed in an effect, which runs only after hydration\n  // has matched. Behaviour is unchanged — the threshold timers above still\n  // decide when each phase begins.\n  const [elapsedAtMount, setElapsedAtMount] = useState(0);\n  useEffect(() => {\n    setElapsedAtMount(Date.now() - start);\n  }, [start]);\n  const riseRemainMs = Math.max(0, safeP50 - elapsedAtMount);\n\n  let fillFrac: number;\n  let fillMs: number;\n  let fillEase: string;\n  if (phase === \"arrived\") {\n    fillFrac = 0;\n    fillMs = reduced ? 0 : DRAIN_MS;\n    fillEase = DRAIN_EASE;\n  } else if (phase === \"waiting\") {\n    fillFrac = p50Frac;\n    fillMs = reduced ? 0 : riseRemainMs;\n    fillEase = RISE_EASE;\n  } else {\n    // slow or stalled: held at the p50 line, no further rise\n    fillFrac = p50Frac;\n    fillMs = 0;\n    fillEase = RISE_EASE;\n  }\n\n  // p95 tick fades in across the \"slow\" window, complete by the time\n  // \"stalled\" arrives; instant under reduced motion (still legible, just\n  // not animated).\n  const p95FadeMs = Math.max(P95_FADE_MS_FLOOR, safeP95 - safeP50);\n  // driven by the underlying timed phase, not the arrived overlay — a token\n  // that lands before p50 was ever reached should never suddenly reveal a\n  // mark it hadn't earned yet.\n  const p95Visible = timed !== \"waiting\";\n\n  // announce only on transition, never a running commentary on the level\n  const [liveText, setLiveText] = useState(\"\");\n  const lastAnnouncedRef = useRef<MeniscusPhase | null>(null);\n  useEffect(() => {\n    if (lastAnnouncedRef.current === phase) return;\n    lastAnnouncedRef.current = phase;\n    if (phase === \"waiting\") {\n      setLiveText(`Waiting, typically ${fmtSeconds(safeP50)} seconds.`);\n    } else if (phase === \"slow\") {\n      setLiveText(\"Taking longer than usual.\");\n    } else if (phase === \"stalled\") {\n      setLiveText(\"May be stalled, retry available.\");\n    } else {\n      setLiveText(\"Response arrived.\");\n    }\n  }, [phase, safeP50]);\n\n  const p50Label = `${fmtSeconds(safeP50)}s`;\n  const p95Label = `${fmtSeconds(safeP95)}s`;\n\n  const captionText =\n    phase === \"waiting\"\n      ? \"waiting\"\n      : phase === \"slow\"\n        ? \"slower than usual\"\n        : phase === \"stalled\"\n          ? \"may be stalled\"\n          : \"arrived\";\n\n  return (\n    <div\n      data-meniscus-root\n      role=\"group\"\n      aria-label={label}\n      className={`inline-flex items-start gap-4 ${className}`}\n    >\n      <style>{CSS}</style>\n\n      {/* the visual tube/tick apparatus (or its reduced-motion stand-in) —\n          a dedicated, spatially disjoint hook for tooling: it never overlaps\n          the retry/switch buttons rendered in the sibling column below, so\n          clicking it (inert — there is no handler here) can never\n          accidentally land on a button that only exists once stalled. */}\n      <div data-meniscus-display>\n        {reduced ? (\n          <ReducedIndicator phase={phase} />\n        ) : (\n          <div className=\"flex items-end\" style={{ height: TUBE_H }}>\n            {/* the tube */}\n            <div\n              className=\"relative rounded-sm border border-border\"\n              style={{ width: TUBE_W, height: TUBE_H }}\n            >\n              <div className=\"absolute inset-0 overflow-hidden rounded-sm\">\n                <div\n                  aria-hidden\n                  className=\"absolute inset-x-0 bottom-0\"\n                  style={{\n                    height: `${pct(fillFrac)}%`,\n                    backgroundColor: \"var(--ns-muted)\",\n                    opacity: 0.25,\n                    transition: `height ${fillMs}ms ${fillEase}`,\n                  }}\n                />\n              </div>\n\n              {/* meniscus — a small concave curve riding the fill's top edge,\n                  a sibling of the clipped layer so it's never cut off */}\n              <div\n                aria-hidden\n                className={phase === \"slow\" ? \"ns-mh-tremor\" : \"\"}\n                style={{\n                  position: \"absolute\",\n                  left: -1,\n                  right: -1,\n                  bottom: `calc(${pct(fillFrac)}% - 2px)`,\n                  height: 4,\n                  transition: `bottom ${fillMs}ms ${fillEase}`,\n                }}\n              >\n                <svg width={TUBE_W + 2} height={4} viewBox=\"0 0 8 4\" style={{ display: \"block\" }}>\n                  <path\n                    d=\"M0,0.5 Q4,3.2 8,0.5\"\n                    fill=\"none\"\n                    stroke=\"var(--foreground)\"\n                    strokeWidth=\"1\"\n                    strokeLinecap=\"round\"\n                  />\n                </svg>\n              </div>\n            </div>\n\n            {/* scribe marks + labels */}\n            <div className=\"relative ml-2\" style={{ height: TUBE_H, width: 34 }}>\n              <Tick bottomFrac={p50Frac} label={p50Label} opacity={1} />\n              <Tick\n                bottomFrac={1}\n                label={p95Label}\n                opacity={p95Visible ? 1 : 0}\n                transitionMs={reduced ? 0 : p95FadeMs}\n              />\n            </div>\n          </div>\n        )}\n      </div>\n\n      <div className=\"flex flex-col gap-2\">\n        <span className=\"font-mono text-[10px] uppercase tracking-[0.14em] text-ns-muted\">\n          {captionText}\n        </span>\n\n        {phase === \"stalled\" && (\n          <div className=\"flex items-center gap-2\">\n            <button\n              type=\"button\"\n              data-meniscus-retry\n              onClick={onRetry}\n              className=\"rounded-sm border border-border px-2 py-1 font-mono text-[11px] text-foreground transition-colors hover:bg-foreground/10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ns-accent\"\n            >\n              Retry\n            </button>\n            <button\n              type=\"button\"\n              data-meniscus-switch\n              onClick={onSwitchModel}\n              className=\"rounded-sm border border-border px-2 py-1 font-mono text-[11px] text-ns-muted transition-colors hover:bg-foreground/10 hover:text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ns-accent\"\n            >\n              Switch model\n            </button>\n          </div>\n        )}\n      </div>\n\n      <p role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n        {liveText}\n      </p>\n    </div>\n  );\n}\n\nfunction Tick({\n  bottomFrac,\n  label,\n  opacity,\n  transitionMs = 0,\n}: {\n  bottomFrac: number;\n  label: string;\n  opacity: number;\n  transitionMs?: number;\n}) {\n  return (\n    <div\n      aria-hidden\n      className=\"absolute left-0 flex items-center gap-1\"\n      style={{\n        bottom: `calc(${pct(bottomFrac)}% - 0.5px)`,\n        opacity,\n        transition: `opacity ${transitionMs}ms linear`,\n      }}\n    >\n      <span className=\"block\" style={{ width: 5, height: 1, backgroundColor: \"var(--foreground)\" }} />\n      <span className=\"whitespace-nowrap font-mono text-[9px] text-ns-muted\">{label}</span>\n    </div>\n  );\n}\n\n// Static, motion-free fallback: three fixed segments, the current phase lit\n// from --ns-muted to --foreground. Same phase machine drives this branch —\n// nothing here depends on perceiving a rise or a tremble.\nfunction ReducedIndicator({ phase }: { phase: MeniscusPhase }) {\n  const segments: { key: \"waiting\" | \"slow\" | \"stalled\"; text: string }[] = [\n    { key: \"waiting\", text: \"normal\" },\n    { key: \"slow\", text: \"slow\" },\n    { key: \"stalled\", text: \"stalled\" },\n  ];\n  const activeKey = phase === \"arrived\" ? null : phase;\n  return (\n    <div className=\"flex items-end gap-1\" style={{ height: TUBE_H }}>\n      {segments.map((seg) => {\n        const active = seg.key === activeKey;\n        return (\n          <div key={seg.key} className=\"flex flex-col items-center gap-1\">\n            <div\n              className=\"rounded-sm border border-border\"\n              style={{\n                width: TUBE_W,\n                height: TUBE_H - 10,\n                backgroundColor: active ? \"var(--foreground)\" : \"var(--ns-muted)\",\n                opacity: active ? 0.55 : 0.15,\n              }}\n            />\n            <span className=\"font-mono text-[8px] uppercase tracking-[0.1em] text-ns-muted\">\n              {seg.text}\n            </span>\n          </div>\n        );\n      })}\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/meter-latency-capillary.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ns-muted": "var(--ns-muted)",
      "color-ns-accent": "var(--ns-accent)"
    },
    "light": {
      "ns-muted": "#4d4d4d",
      "ns-accent": "#006bff"
    },
    "dark": {
      "ns-muted": "#8f8f8f"
    }
  },
  "meta": {
    "collection": "core",
    "tags": [
      "loading",
      "status",
      "latency",
      "agent",
      "aria-live",
      "svg",
      "meter",
      "timing"
    ],
    "instruction": "Renders time-to-first-token as a calibrated capillary tube: a narrow column, exactly 6px wide and 48px tall, 1px --border walls, whose fill (--ns-muted at 25% opacity) rises from the bottom toward a scribed p50 line the instant the request starts. The rise duration is set so the fill's top — a small concave SVG meniscus curve riding it, stroked in --foreground, animated separately from the fill itself — reaches that p50 line exactly when a response typically lands for this model/tool combo, using an eased (not linear) decelerating curve so the arrival reads as a settling motion, not a race. If nothing has arrived by the time the p50 line is reached, the component does the one honest thing a spinner never does: it stops rising. The fill HOLDS at the p50 level — it never keeps climbing toward p95, since that would misrepresent unearned progress — while the meniscus curve alone (not the fill, not the tube) gets a barely-there 0.5px vertical tremble at 2Hz, and a second scribed line for p95 fades in above it over the p50-to-p95 window. The moment elapsed time actually passes p95, the tremble stops outright (trembling forever would itself be dishonest about how much longer 'unusually slow' is expected to last) and a stall affordance — real Retry and Switch model buttons, not disabled decoys — mounts into the layout, entering tab order exactly when they appear. If the first token arrives at any point (an `arrivedAt` timestamp appears), the tube drains to empty fast, ease-out-expo, from wherever the fill currently sits, as the real streaming response takes over. The whole thing is driven by three props — `startedAt` (ms epoch, defaults to mount time), `p50Ms`, `p95Ms` (defaults are the caller's, drawn from a rolling per-model latency store) — via two scheduled setTimeouts rather than a rAF poll, and correctly handles a late mount: if the component is created e.g. 300ms into an already-running request, it computes remaining time to each threshold and eases in for only what's left, rather than restarting the rise from zero. Accessibility is phase-transition-only: a role=status aria-live=polite region announces exactly once per transition — 'Waiting, typically N seconds.' entering the wait, 'Taking longer than usual.' entering the slow hold, 'May be stalled, retry available.' entering the stalled state — and never narrates the continuously-changing fill level itself, which would be noise. Retry and Switch model are ordinary <button>s with visible text labels (real accessible names, not icon-only), rendered conditionally so they simply aren't in the DOM (and not in tab order) until the stalled phase actually begins. Under prefers-reduced-motion, the entire tube/meniscus/tick apparatus is replaced by a static three-segment strip (within-normal / slow / stalled) with the current segment lit from --ns-muted to --foreground and the rest dim — driven by the exact same phase state machine and the exact same three announcements, so nothing about the component's honesty depends on a viewer being able to perceive motion. Distinct from status-glyph-cadence, which signals raw connection liveness with no concept of 'how long is normal' — no percentiles, no stall detection, just 'is it still alive'; and distinct from password-strength-tide, whose liquid level encodes input strength typed so far, not elapsed wall-clock time measured against a calibrated latency distribution. Props: startedAt (optional, ms epoch), p50Ms and p95Ms (required, ms), arrivedAt (optional ms epoch or null/undefined while waiting), onRetry, onSwitchModel (both optional callbacks fired by the two stall-state buttons), label (accessible name for the root group, default 'Time to first token'), className. Pure DOM + CSS + one small inline SVG path for the meniscus curve — no canvas, all ink and fill from --foreground/--ns-muted/--border tokens."
  },
  "type": "registry:ui"
}