{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "meter-context-window",
  "title": "Meter Context Window",
  "description": "A context-window budget meter: one hairline stacked bar segmenting the window into system prompt / tools / history / current turn, told apart by fill pattern instead of color, that visibly resettles rather than jumps when the window compacts.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/meter-context-window/component.tsx",
      "content": "\"use client\";\n\n// A context-window budget meter: one hairline stacked bar segmenting the\n// window into system prompt / tools / conversation history / current turn,\n// plus the unclaimed remainder, with a Geist Mono token readout underneath.\n// It reads at rest because the bar IS the thing it describes — no legend\n// needed to decode \"what does 40% mean,\" the segment widths are the answer.\n//\n// Segments carry no color of their own (the palette is monochrome by house\n// rule) — they're told apart by a stepped weight ramp (four flat, clearly\n// separated color-mix() tones, lightest for system up to heaviest for the\n// live turn) plus a bonus pattern once a segment is wide enough to resolve\n// it: tools a diagonal hatch, history a stipple, current turn a fine\n// vertical hatch that breathes gently while it's live. A 1px hairline\n// divider sits between every pair so the boundary reads even when two\n// tones land close together. Pure CSS, built from color-mix() against\n// --foreground and --background, never a hex literal and never --ns-accent\n// (accent is reserved for interaction, not decoration).\n//\n// Layout is plain flexbox: each segment's flex-basis is its share of\n// max(capacity, used) as a percentage, with a min-width floor on any segment\n// that has real tokens but would round below a legible sliver. The floor is\n// enforced by the browser's own flex-shrink algorithm (larger segments give\n// up the room), not by a hand-rolled pixel solver — a segment with tokens\n// never disappears, one with none never fakes a sliver it didn't earn.\n//\n// Approaching capacity is signalled through form: the wrapper gains a\n// pulsing inset ring, the readout goes bold and grows a triangle glyph, and\n// the free segment (if any width remains) grows a hazard hatch — never a red\n// fill, this palette doesn't have one. Compaction is just a prop change:\n// react re-renders new percentages and the segments ease into them on a\n// shared transition, so the resettle itself carries the news.\n//\n// All the numbers are real DOM text (a <p> readout plus a legend list), so\n// assistive tech gets the actual figures regardless of what the bar looks\n// like; the bar itself is aria-hidden as a redundant visual. The readout is\n// a polite live region since it changes asynchronously as tokens accrue.\n// prefers-reduced-motion drops the transition, the breathing, and the\n// hazard pulse — every state stays fully legible, just static.\n\nexport interface BallastContextProps {\n  /** total size of the context window, in tokens */\n  capacity: number;\n  /** tokens spent on the system prompt */\n  system: number;\n  /** tokens spent on tool/function definitions and results */\n  tools: number;\n  /** tokens spent on prior conversation turns */\n  history: number;\n  /** tokens spent on the turn currently being read or generated */\n  turn: number;\n  /**\n   * fraction of capacity (0-1) below which the free segment is flagged as\n   * low headroom. default 0.08 (8%)\n   */\n  lowFreeThreshold?: number;\n  /** accessible name for the meter. default \"Context window budget\" */\n  ariaLabel?: string;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}\n\nconst ROLES = [\n  { role: \"system\", label: \"System\" },\n  { role: \"tools\", label: \"Tools\" },\n  { role: \"history\", label: \"History\" },\n  { role: \"turn\", label: \"Turn\" },\n  { role: \"free\", label: \"Free\" },\n] as const;\n\nfunction formatTokens(n: number): string {\n  return Math.round(Math.max(0, n)).toLocaleString(\"en-US\");\n}\n\nfunction formatPct(tokens: number, denom: number): string {\n  if (tokens <= 0 || denom <= 0) return \"0%\";\n  const p = (tokens / denom) * 100;\n  if (p < 1) return \"<1%\";\n  return `${Math.round(p)}%`;\n}\n\nexport function BallastContext({\n  capacity,\n  system,\n  tools,\n  history,\n  turn,\n  lowFreeThreshold = 0.08,\n  ariaLabel = \"Context window budget\",\n  className = \"\",\n}: BallastContextProps) {\n  const safeSystem = Math.max(0, system);\n  const safeTools = Math.max(0, tools);\n  const safeHistory = Math.max(0, history);\n  const safeTurn = Math.max(0, turn);\n  const safeCapacity = Math.max(0, capacity);\n\n  const used = safeSystem + safeTools + safeHistory + safeTurn;\n  const overBudget = safeCapacity > 0 && used > safeCapacity;\n  const free = Math.max(0, safeCapacity - used);\n  // denominator the bar fills to 100% against: capacity normally, or the\n  // used total itself once over budget, so the four segments still sum to\n  // a full bar instead of visually shrinking below their real weight\n  const denom = Math.max(safeCapacity, used, 1);\n\n  const freeFraction = safeCapacity > 0 ? free / safeCapacity : 0;\n  const warn = safeCapacity > 0 && (overBudget || freeFraction <= lowFreeThreshold);\n\n  const tokensByRole: Record<(typeof ROLES)[number][\"role\"], number> = {\n    system: safeSystem,\n    tools: safeTools,\n    history: safeHistory,\n    turn: safeTurn,\n    free,\n  };\n\n  const pct = safeCapacity > 0 ? Math.round((used / safeCapacity) * 100) : 0;\n  const statusSuffix = overBudget ? \" · over budget\" : warn ? \" · low headroom\" : \"\";\n\n  return (\n    <div\n      role=\"group\"\n      aria-label={ariaLabel}\n      data-warn={warn}\n      className={[\"ns-ballast\", className].join(\" \")}\n    >\n      <style>{`\n.ns-ballast-track{transition:box-shadow 300ms ease-out}\n.ns-ballast[data-warn=\"true\"] .ns-ballast-track{animation:ns-ballast-ring 1.8s ease-in-out infinite}\n.ns-ballast-seg{transition:flex-basis 520ms cubic-bezier(.22,1,.36,1)}\n.ns-ballast-track .ns-ballast-seg:not([data-role=\"free\"]){border-right:1px solid var(--background)}\n.ns-ballast-seg[data-role=\"system\"]{background:color-mix(in srgb, var(--foreground) 22%, var(--background))}\n.ns-ballast-seg[data-role=\"tools\"]{background-color:color-mix(in srgb, var(--foreground) 30%, var(--background));background-image:repeating-linear-gradient(45deg, color-mix(in srgb, var(--foreground) 46%, var(--background)) 0 2px, transparent 2px 5px)}\n.ns-ballast-seg[data-role=\"history\"]{background-color:color-mix(in srgb, var(--foreground) 40%, var(--background));background-image:radial-gradient(color-mix(in srgb, var(--foreground) 60%, var(--background)) 1.1px, transparent 1.3px);background-size:5px 5px}\n.ns-ballast-seg[data-role=\"turn\"]{background-color:color-mix(in srgb, var(--foreground) 52%, var(--background));background-image:repeating-linear-gradient(90deg, color-mix(in srgb, var(--foreground) 72%, var(--background)) 0 1.5px, transparent 1.5px 4px);animation:ns-ballast-breathe 2.6s ease-in-out infinite}\n.ns-ballast-seg[data-role=\"free\"]{background:transparent}\n.ns-ballast[data-warn=\"true\"] .ns-ballast-seg[data-role=\"free\"]{background-image:repeating-linear-gradient(135deg, color-mix(in srgb, var(--foreground) 26%, transparent) 0 1px, transparent 1px 6px);animation:ns-ballast-hazard 1.6s ease-in-out infinite}\n@keyframes ns-ballast-breathe{0%,100%{opacity:.82}50%{opacity:1}}\n@keyframes ns-ballast-hazard{0%,100%{opacity:.5}50%{opacity:1}}\n@keyframes ns-ballast-ring{0%,100%{box-shadow:inset 0 0 0 1px var(--border)}50%{box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--foreground) 45%, transparent)}}\n@media (prefers-reduced-motion: reduce){\n  .ns-ballast-seg{transition:none;animation:none !important;opacity:1 !important}\n  .ns-ballast-track{animation:none !important;box-shadow:inset 0 0 0 1px var(--border) !important}\n}\n`}</style>\n\n      <div\n        aria-hidden=\"true\"\n        className=\"ns-ballast-track flex h-2.5 w-full overflow-hidden rounded-full border border-border bg-background\"\n      >\n        {ROLES.map(({ role }) => {\n          const t = tokensByRole[role];\n          return (\n            <div\n              key={role}\n              data-role={role}\n              className=\"ns-ballast-seg h-full shrink\"\n              style={{\n                flexBasis: `${(t / denom) * 100}%`,\n                flexGrow: 0,\n                minWidth: t > 0 ? \"3px\" : \"0px\",\n              }}\n            />\n          );\n        })}\n      </div>\n\n      <p\n        aria-live=\"polite\"\n        className={[\n          \"mt-3 flex items-center gap-1.5 font-mono text-xs tabular-nums text-ns-muted\",\n          warn ? \"font-semibold text-foreground\" : \"\",\n        ].join(\" \")}\n      >\n        {warn ? (\n          <svg\n            aria-hidden=\"true\"\n            width=\"12\"\n            height=\"12\"\n            viewBox=\"0 0 16 16\"\n            fill=\"none\"\n            className=\"shrink-0 text-foreground\"\n          >\n            <path\n              d=\"M8 2.4 14.6 13.6H1.4L8 2.4Z\"\n              stroke=\"currentColor\"\n              strokeWidth=\"1.3\"\n              strokeLinejoin=\"round\"\n            />\n            <path d=\"M8 6.6v3.4\" stroke=\"currentColor\" strokeWidth=\"1.3\" strokeLinecap=\"round\" />\n            <circle cx=\"8\" cy=\"11.9\" r=\"0.85\" fill=\"currentColor\" />\n          </svg>\n        ) : null}\n        <span>\n          {formatTokens(used)} / {formatTokens(safeCapacity)} tokens · {pct}%{statusSuffix}\n        </span>\n      </p>\n\n      <ul className=\"mt-2.5 flex flex-wrap gap-x-4 gap-y-1.5 font-mono text-[11px] tabular-nums text-ns-muted\">\n        {ROLES.map(({ role, label }) => {\n          const t = tokensByRole[role];\n          return (\n            <li key={role} className=\"flex items-center gap-1.5\">\n              <span\n                aria-hidden=\"true\"\n                data-role={role}\n                className=\"ns-ballast-seg h-2.5 w-2.5 shrink-0 rounded-[2px] border border-border\"\n              />\n              <span className=\"tracking-wide text-ns-muted\">{label}</span>\n              <span className=\"text-foreground\">{formatTokens(t)}</span>\n              <span>{formatPct(t, denom)}</span>\n            </li>\n          );\n        })}\n      </ul>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/meter-context-window.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": [
      "meter",
      "progress",
      "context-window",
      "tokens",
      "llm",
      "agent",
      "data-viz",
      "mono",
      "dashboard",
      "budget",
      "accessibility"
    ],
    "instruction": "A context-window budget meter for agent UIs: one hairline stacked bar (rounded-full, 10px tall, 1px --border ring, bg-background bed) segmenting the window into five shares — system prompt, tools, conversation history, current turn, and the unclaimed remainder — plus a font-mono tabular-nums readout and a text legend underneath. Pure DOM/CSS, no canvas, no SVG paint loop, no rAF: each segment is a flex child whose flex-basis is its percentage of max(capacity, used) so the five always sum to a full bar even when usage exceeds capacity, and a min-width floor (3px) on any segment that has real tokens keeps it from vanishing below a legible sliver — the browser's own flex-shrink gives it room by shrinking the larger segments, not a hand-rolled pixel solver, so a segment with zero tokens renders zero width honestly and one with tokens never fakes a mark it didn't earn. The palette is monochrome by house rule, so segments are told apart primarily by a stepped weight ramp (four flat, unmistakably different color-mix() tones against --foreground, never --ns-accent — 22/30/40/52% — so even a 3px sliver reads as a distinct step, with no fill ever falling through to a bare or transparent look that would misread as free space) plus a bonus pattern layered on top of each base tone once a segment is wide enough to resolve it (system stays flat, tools gets a 45deg diagonal hatch, history a stipple, current turn a close vertical hatch that breathes gently — a 2.6s opacity pulse — while it's the live, growing share); a hairline 1px --background divider sits between every adjacent pair so boundaries stay crisp even when two neighboring tones land close together, and the free remainder is bare track. Compaction is just a prop change — react re-renders new percentages and every segment eases into them on a shared 520ms cubic-bezier(0.22,1,0.36,1) flex-basis transition, so the resettle itself is what communicates that compaction happened, never an instant jump. Approaching capacity reads as a warning through form, never a red fill: once free space drops to or below `lowFreeThreshold` (prop, fraction of capacity, default 0.08) or usage exceeds capacity outright, the track gains a slow pulsing inset ring, the readout goes bold and grows a small triangle glyph, and the free segment (if any width remains) grows a diagonal hazard hatch. Props: capacity, system, tools, history, turn (all plain numbers, tokens, fully controlled — the component holds no state of its own), lowFreeThreshold, ariaLabel, className. Accessibility: the bar itself is aria-hidden (a redundant visual against the text that follows); a role=group wraps the whole meter with an accessible name; the token readout is a real aria-live=polite paragraph so async updates get announced; the legend below the bar is plain DOM text — label, exact token count, and a percentage (values under 1% but above zero read '<1%' rather than rounding to a misleading 0%) — for every one of the five shares, so assistive tech gets the real numbers regardless of what the bar renders. The component exposes no interactive controls of its own (display-only, correctly skipped by the tab-reachability check) and renders correctly in both themes since every ink is a CSS custom property, never a literal. prefers-reduced-motion drops the flex-basis transition, the current-turn breathing, and the hazard pulse entirely — every state (empty, mid-fill, near-capacity, over-budget) stays fully legible, just static. Demo: an agent-session card (claude-4.5, 200K-token window) seeded with a realistic mid-fill baseline so the resting screenshot already shows the meter doing its job, plus a live status line, a CALL TOOL button that grows the tools share, a COMPACT CONTEXT button that shrinks history to roughly a ninth of itself and lets the resettle play, a RESET SESSION button, a FILL WINDOW button that jumps usage straight to ~96% so the near-capacity warning state (pulsing ring, bold readout + triangle glyph, hazard hatch on the free sliver) is reachable in one click rather than only after minutes of ambient growth, and a background interval that streams small turns into the current-turn share and folds each one into history after it settles — pausing near 94% usage the way a real agent would compact before running out, so the near-capacity warning state is also reachable just by leaving the demo running."
  },
  "type": "registry:ui"
}