{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "context-meter",
  "type": "registry:ui",
  "title": "Context Meter",
  "description": "Token / context-window usage as a compact ring or bar with warning and danger thresholds, rolling numbers and a segmented breakdown card.",
  "dependencies": [],
  "registryDependencies": [
    "https://kuro.abstergo.fyi/r/kura-theme.json",
    "https://kuro.abstergo.fyi/r/popover.json",
    "https://kuro.abstergo.fyi/r/use-controllable-state.json",
    "https://kuro.abstergo.fyi/r/use-reduced-motion.json",
    "utils"
  ],
  "files": [
    {
      "path": "registry/kura/context-meter/context-meter.tsx",
      "type": "registry:ui",
      "target": "components/kura/context-meter/context-meter.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { useControllableState } from \"@/hooks/kura/use-controllable-state\";\nimport { useReducedMotion } from \"@/hooks/kura/use-reduced-motion\";\nimport { cn } from \"@/lib/utils\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/kura/popover\";\n\nexport interface ContextSegment {\n  /** Stable key, e.g. \"system\". */\n  id: string;\n  /** Legend label, e.g. \"System prompt\". */\n  label: string;\n  /** Tokens used by this part of the context. */\n  tokens: number;\n  /** Optional CSS color for the swatch and bar segment. Default: an ink tint by position. */\n  color?: string;\n}\n\nexport interface ContextMeterProps extends Omit<React.ComponentProps<\"button\">, \"children\"> {\n  /** Context window size in tokens, e.g. 200000. */\n  max: number;\n  /** Tokens used. Default: the sum of `segments`. */\n  used?: number;\n  /** Breakdown shown in the bar and the hover card, e.g. system, history, tools, files. */\n  segments?: readonly ContextSegment[];\n  /** `ring` = compact donut + value; `bar` = a horizontal (segmented) bar with value. Default \"ring\". */\n  variant?: \"ring\" | \"bar\";\n  /** Fraction (0–1) at which the meter turns to warning. Default 0.7. */\n  warningAt?: number;\n  /** Fraction (0–1) at which the meter turns to danger. Default 0.9. */\n  dangerAt?: number;\n  /** What the value shows next to the meter. Default \"percent\" for ring, \"tokens\" for bar. */\n  valueDisplay?: \"percent\" | \"tokens\" | \"none\";\n  /** Short name used in the accessible label and the card title. Default “Context”. */\n  label?: string;\n  /** Extra content at the bottom of the hover card, e.g. “Older turns will be summarized”. */\n  footer?: React.ReactNode;\n  /** Hover card open (controlled). */\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n}\n\nexport function formatTokens(n: number): string {\n  const v = Math.max(0, n);\n  if (v < 1000) return String(Math.round(v));\n  if (v < 1_000_000) {\n    const k = v / 1000;\n    return `${k < 100 ? k.toFixed(1).replace(/\\.0$/, \"\") : Math.round(k)}k`;\n  }\n  const m = v / 1_000_000;\n  return `${m.toFixed(m < 10 ? 2 : 1).replace(/\\.?0+$/, \"\")}M`;\n}\n\n/** Eases a number toward its target so counters roll instead of jumping. */\nfunction useTweened(target: number, duration = 520): number {\n  const reduced = useReducedMotion();\n  const [value, setValue] = React.useState(target);\n  const from = React.useRef(target);\n  const current = React.useRef(target);\n  current.current = value;\n  React.useEffect(() => {\n    if (reduced || typeof requestAnimationFrame !== \"function\") {\n      setValue(target);\n      return;\n    }\n    from.current = current.current;\n    const start = performance.now();\n    let frame = 0;\n    const tick = (t: number) => {\n      const p = Math.min(1, (t - start) / duration);\n      const eased = 1 - (1 - p) ** 4;\n      setValue(from.current + (target - from.current) * eased);\n      if (p < 1) frame = requestAnimationFrame(tick);\n    };\n    frame = requestAnimationFrame(tick);\n    return () => cancelAnimationFrame(frame);\n  }, [target, duration, reduced]);\n  return value;\n}\n\nconst tints = [\"var(--kura-fg)\", \"var(--kura-gray-9)\", \"var(--kura-gray-8)\", \"var(--kura-gray-7)\"];\nconst segmentColor = (s: ContextSegment, i: number) =>\n  s.color ?? `color-mix(in oklab, ${tints[i % tints.length]} ${i < 4 ? 100 : 60}%, transparent)`;\n\n/**\n * Token / context-window usage as a compact ring or bar with warning and danger thresholds,\n * rolling tabular numbers, and a hover card that breaks usage down by segment.\n */\nexport function ContextMeter({\n  max,\n  used: usedProp,\n  segments,\n  variant = \"ring\",\n  warningAt = 0.7,\n  dangerAt = 0.9,\n  valueDisplay,\n  label = \"Context\",\n  footer,\n  open: openProp,\n  onOpenChange,\n  className,\n  onPointerEnter,\n  onPointerLeave,\n  onFocus,\n  onBlur,\n  ...props\n}: ContextMeterProps) {\n  const used = usedProp ?? segments?.reduce((sum, s) => sum + s.tokens, 0) ?? 0;\n  const ratio = max > 0 ? Math.min(1, Math.max(0, used / max)) : 0;\n  const level = ratio >= dangerAt ? \"danger\" : ratio >= warningAt ? \"warning\" : \"ok\";\n  const display = valueDisplay ?? (variant === \"ring\" ? \"percent\" : \"tokens\");\n  const tweenedRatio = useTweened(ratio);\n  const tweenedUsed = useTweened(used);\n\n  const [open, setOpen] = useControllableState({\n    value: openProp,\n    defaultValue: false,\n    onChange: onOpenChange,\n  });\n  const timer = React.useRef<ReturnType<typeof setTimeout>>(undefined);\n  React.useEffect(() => () => clearTimeout(timer.current), []);\n  const schedule = (next: boolean, delay: number) => {\n    clearTimeout(timer.current);\n    timer.current = setTimeout(() => setOpen(next), delay);\n  };\n\n  const pct = Math.round(ratio * 100);\n  const levelText =\n    level === \"danger\" ? \", nearly full\" : level === \"warning\" ? \", filling up\" : \"\";\n  const accessibleName = `${label}: ${used.toLocaleString(\"en-US\")} of ${max.toLocaleString(\"en-US\")} tokens used (${pct}%${levelText})`;\n\n  const tone = cn(\n    level === \"ok\" && \"text-kura-fg-muted\",\n    level === \"warning\" && \"text-kura-warning-text\",\n    level === \"danger\" && \"text-kura-danger-text\",\n  );\n  const stroke =\n    level === \"danger\"\n      ? \"var(--kura-danger)\"\n      : level === \"warning\"\n        ? \"var(--kura-warning)\"\n        : \"var(--kura-fg-muted)\";\n\n  const value =\n    display === \"none\" ? null : (\n      <span\n        data-slot=\"context-meter-value\"\n        className={cn(\n          \"text-left font-kura-mono text-xs tabular-nums\",\n          display === \"percent\" ? \"min-w-[4ch]\" : \"min-w-[13ch]\",\n          tone,\n        )}\n      >\n        {display === \"percent\" ? (\n          `${Math.round(tweenedRatio * 100)}%`\n        ) : (\n          <>\n            {formatTokens(tweenedUsed)}\n            <span className=\"text-kura-fg-subtle\"> / {formatTokens(max)}</span>\n          </>\n        )}\n      </span>\n    );\n\n  const R = 7;\n  const C = 2 * Math.PI * R;\n\n  return (\n    <Popover open={open} onOpenChange={setOpen}>\n      <PopoverTrigger asChild>\n        <button\n          type=\"button\"\n          data-slot=\"context-meter\"\n          data-variant={variant}\n          data-level={level}\n          aria-label={accessibleName}\n          onPointerEnter={(e) => {\n            onPointerEnter?.(e);\n            if (e.pointerType !== \"touch\" && segments?.length) schedule(true, 300);\n          }}\n          onPointerLeave={(e) => {\n            onPointerLeave?.(e);\n            schedule(false, 150);\n          }}\n          onFocus={onFocus}\n          onBlur={(e) => {\n            onBlur?.(e);\n            schedule(false, 100);\n          }}\n          className={cn(\n            \"group/meter inline-flex h-7 cursor-pointer items-center gap-2 rounded-kura-sm px-1.5 font-kura-sans outline-none\",\n            \"transition-[background-color,box-shadow] duration-(--kura-duration-fast) ease-kura-out hover:bg-kura-hover focus-visible:shadow-kura-focus data-[state=open]:bg-kura-hover\",\n            className,\n          )}\n          {...props}\n        >\n          {variant === \"ring\" ? (\n            <svg\n              aria-hidden\n              data-slot=\"context-meter-ring\"\n              viewBox=\"0 0 18 18\"\n              className=\"size-[18px] shrink-0 -rotate-90\"\n            >\n              <circle\n                cx=\"9\"\n                cy=\"9\"\n                r={R}\n                fill=\"none\"\n                strokeWidth=\"2.25\"\n                className=\"stroke-kura-border\"\n              />\n              <circle\n                cx=\"9\"\n                cy=\"9\"\n                r={R}\n                fill=\"none\"\n                strokeWidth=\"2.25\"\n                strokeLinecap=\"round\"\n                strokeDasharray={C}\n                strokeDashoffset={C * (1 - tweenedRatio)}\n                style={{ stroke }}\n                className=\"transition-[stroke] duration-(--kura-duration-slow) ease-kura-out\"\n                opacity={ratio === 0 ? 0 : 1}\n              />\n            </svg>\n          ) : (\n            <SegmentBar\n              segments={segments}\n              used={used}\n              max={max}\n              level={level}\n              ratio={tweenedRatio}\n              className=\"h-1.5 w-28 shrink-0\"\n            />\n          )}\n          {value}\n        </button>\n      </PopoverTrigger>\n      {segments && segments.length > 0 && (\n        <PopoverContent\n          side=\"top\"\n          align={variant === \"bar\" ? \"start\" : \"center\"}\n          data-slot=\"context-meter-card\"\n          className=\"w-72 p-3.5\"\n          onOpenAutoFocus={(e) => e.preventDefault()}\n          onPointerEnter={() => clearTimeout(timer.current)}\n          onPointerLeave={() => schedule(false, 150)}\n        >\n          <div className=\"flex flex-col gap-3\">\n            <div className=\"flex items-baseline justify-between\">\n              <span className=\"font-medium text-kura-fg text-sm\">{label} window</span>\n              <span className={cn(\"font-kura-mono text-xs tabular-nums\", tone)}>\n                {formatTokens(used)}{\" \"}\n                <span className=\"text-kura-fg-subtle\">/ {formatTokens(max)}</span>\n              </span>\n            </div>\n            <SegmentBar\n              segments={segments}\n              used={used}\n              max={max}\n              level={level}\n              ratio={ratio}\n              className=\"h-2\"\n            />\n            <ul className=\"flex flex-col gap-1.5\" aria-label=\"Breakdown\">\n              {segments.map((s, i) => (\n                <li\n                  key={s.id}\n                  data-slot=\"context-meter-segment\"\n                  className=\"flex items-center gap-2 text-[13px]\"\n                >\n                  <span\n                    aria-hidden\n                    className=\"size-2 shrink-0 rounded-[2px]\"\n                    style={{ background: segmentColor(s, i) }}\n                  />\n                  <span className=\"min-w-0 flex-1 truncate text-kura-fg-muted\">{s.label}</span>\n                  <span className=\"font-kura-mono text-kura-fg text-xs tabular-nums\">\n                    {formatTokens(s.tokens)}\n                  </span>\n                  <span className=\"w-9 text-right font-kura-mono text-kura-fg-subtle text-xs tabular-nums\">\n                    {max > 0 ? `${Math.round((s.tokens / max) * 100)}%` : \"–\"}\n                  </span>\n                </li>\n              ))}\n              <li className=\"flex items-center gap-2 text-[13px]\">\n                <span\n                  aria-hidden\n                  className=\"size-2 shrink-0 rounded-[2px] shadow-[inset_0_0_0_1px_var(--kura-border-strong)]\"\n                />\n                <span className=\"flex-1 text-kura-fg-subtle\">Free</span>\n                <span className=\"font-kura-mono text-kura-fg-subtle text-xs tabular-nums\">\n                  {formatTokens(Math.max(0, max - used))}\n                </span>\n                <span className=\"w-9\" />\n              </li>\n            </ul>\n            {(footer != null || level !== \"ok\") && (\n              <p\n                className={cn(\n                  \"text-xs leading-relaxed shadow-[inset_0_1px_0_var(--kura-border-subtle)] pt-2.5\",\n                  level === \"ok\" ? \"text-kura-fg-muted\" : tone,\n                )}\n              >\n                {footer ??\n                  (level === \"danger\"\n                    ? \"Nearly full — the oldest turns will be dropped or summarized.\"\n                    : \"Filling up — consider starting a new thread for unrelated work.\")}\n              </p>\n            )}\n          </div>\n        </PopoverContent>\n      )}\n    </Popover>\n  );\n}\n\nfunction SegmentBar({\n  segments,\n  used,\n  max,\n  level,\n  ratio,\n  className,\n}: {\n  segments: readonly ContextSegment[] | undefined;\n  used: number;\n  max: number;\n  level: \"ok\" | \"warning\" | \"danger\";\n  ratio: number;\n  className?: string;\n}) {\n  const fill =\n    level === \"danger\"\n      ? \"bg-kura-danger\"\n      : level === \"warning\"\n        ? \"bg-kura-warning\"\n        : \"bg-kura-fg-muted\";\n  return (\n    <span\n      aria-hidden\n      data-slot=\"context-meter-bar\"\n      className={cn(\"relative flex overflow-hidden rounded-full bg-kura-active\", className)}\n    >\n      {segments && segments.length > 0 && level === \"ok\" ? (\n        <span\n          className=\"flex h-full origin-left transition-[width] duration-(--kura-duration-slow) ease-kura-out motion-reduce:transition-none\"\n          style={{ width: `${ratio * 100}%` }}\n        >\n          {segments.map((s, i) => (\n            <span\n              key={s.id}\n              className=\"h-full [&+&]:ml-px\"\n              style={{\n                width: `${used > 0 ? (s.tokens / used) * 100 : 0}%`,\n                background: segmentColor(s, i),\n              }}\n            />\n          ))}\n        </span>\n      ) : (\n        <span\n          className={cn(\n            \"h-full rounded-full transition-[width,background-color] duration-(--kura-duration-slow) ease-kura-out motion-reduce:transition-none\",\n            fill,\n          )}\n          style={{ width: `${max > 0 ? ratio * 100 : 0}%` }}\n        />\n      )}\n    </span>\n  );\n}\n"
    },
    {
      "path": "registry/kura/context-meter/index.ts",
      "type": "registry:ui",
      "target": "components/kura/context-meter/index.ts",
      "content": "export {\n  ContextMeter,\n  type ContextMeterProps,\n  type ContextSegment,\n  formatTokens,\n} from \"./context-meter\";\n"
    }
  ],
  "categories": [
    "agent",
    "tokens",
    "context-window",
    "usage",
    "meter"
  ],
  "docs": "Docs: https://kuro.abstergo.fyi/components/context-meter · Markdown for agents: https://kuro.abstergo.fyi/components/context-meter.md",
  "meta": {
    "kura": {
      "status": "beta",
      "whenToUse": [
        "Show how much of a model's context window a conversation uses (e.g. in a composer footer or chat header).",
        "Break usage down by source — system prompt, tool definitions, attached files, history — in a hover card.",
        "Warn before the context fills up (auto-compaction, truncation) with warning/danger thresholds.",
        "Show any other token budget (per-request output limit, monthly quota) as a bar."
      ],
      "whenNotToUse": [
        "Indeterminate progress or “thinking” — use shimmer-text or reasoning.",
        "Upload or task progress — use a progress bar (or agent-timeline for multi-step work).",
        "Billing/cost dashboards with history — use a chart."
      ]
    }
  }
}
