{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "token-meter",
  "title": "Token Meter",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A context-window meter that stacks system, history and attachments against the limit with a reserved reply area, plus a composer pill that opens the same view in a popover.",
  "dependencies": [
    "motion@^12",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/popover.json",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/token-meter.tsx",
      "content": "// Ballmac UI: Token Meter. https://ui.ballmac.com/components/token-meter\n\"use client\"\n\nimport * as React from \"react\"\nimport { AlertTriangle, Info } from \"lucide-react\"\nimport { motion, useReducedMotion } from \"motion/react\"\n\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ballmac/popover\"\nimport { cn } from \"@/lib/utils\"\nimport { useMessages, type Msg } from \"@/lib/ballmac/i18n\"\n\ntype TokenSegment = {\n  /** What these tokens are, for example \"Conversation\". */\n  label: string\n  /** Token count. */\n  tokens: number\n  /** Reserved space, like room kept for the reply. Drawn hatched and not counted as used. */\n  reserved?: boolean\n}\n\n/** 12400 → \"12.4k\", 1_250_000 → \"1.25M\". */\nfunction formatTokens(tokens: number) {\n  if (!Number.isFinite(tokens)) return \"0\"\n  const abs = Math.abs(tokens)\n  if (abs >= 1_000_000) return `${+(tokens / 1_000_000).toFixed(2)}M`\n  if (abs >= 10_000) return `${+(tokens / 1000).toFixed(1)}k`\n  if (abs >= 1000) return `${+(tokens / 1000).toFixed(2)}k`\n  return String(Math.round(tokens))\n}\n\n/** A fast rough estimate: about one token per four characters of English text. */\nfunction estimateTokens(text: string) {\n  return Math.ceil(text.length / 4)\n}\n\nconst FILLS = [\"bg-chart-1\", \"bg-chart-2\", \"bg-chart-4\", \"bg-chart-5\", \"bg-chart-3\"]\n\ntype TokenMeterProps = Omit<React.ComponentProps<\"div\">, \"title\"> & {\n  /** Where the tokens go. Order sets the stacking order and the legend. */\n  segments: TokenSegment[]\n  /** Size of the context window in tokens. */\n  limit: number\n  /** Heading. */\n  title?: string\n  /** Percent used at which the meter turns to a warning. */\n  warnAt?: number\n  /** Cost of the current context, already formatted, such as \"$0.21\". */\n  cost?: string\n  /** Adds a button in the warning and full states, such as \"Compact conversation\". */\n  action?: React.ReactNode\n}\n\nfunction summarize(segments: TokenSegment[], limit: number, warnAt: number) {\n  const used = segments.filter((s) => !s.reserved).reduce((n, s) => n + Math.max(0, s.tokens), 0)\n  const reserved = segments.filter((s) => s.reserved).reduce((n, s) => n + Math.max(0, s.tokens), 0)\n  const percent = limit > 0 ? (used / limit) * 100 : 0\n  const state = percent >= 100 ? \"full\" : percent >= warnAt ? \"warning\" : \"ok\"\n  return { used, reserved, percent, state } as const\n}\n\nfunction stateText(msg: Msg, state: \"ok\" | \"warning\" | \"full\", left: number) {\n  if (state === \"full\") return msg(\"token-meter.full\", \"Context is full\")\n  if (state === \"warning\") return msg(\"token-meter.nearingLimit\", \"Nearing the limit, {left} left\", { left: formatTokens(left) })\n  return msg(\"token-meter.left\", \"{left} left\", { left: formatTokens(left) })\n}\n\nfunction Track({\n  segments,\n  limit,\n  label,\n  valueText,\n  className,\n}: {\n  segments: TokenSegment[]\n  limit: number\n  label: string\n  valueText: string\n  className?: string\n}) {\n  const reduce = useReducedMotion()\n  const total = Math.max(limit, segments.reduce((n, s) => n + Math.max(0, s.tokens), 0), 1)\n  return (\n    <div\n      role=\"meter\"\n      aria-label={label}\n      aria-valuemin={0}\n      aria-valuemax={limit}\n      aria-valuenow={Math.min(limit, segments.filter((s) => !s.reserved).reduce((n, s) => n + s.tokens, 0))}\n      aria-valuetext={valueText}\n      className={cn(\"flex h-2.5 w-full gap-px overflow-hidden rounded-full bg-muted\", className)}\n    >\n      {segments.map((s, i) => {\n        const fillIndex = segments.slice(0, i).filter((x) => !x.reserved).length\n        const width = (Math.max(0, s.tokens) / total) * 100\n        return (\n          <motion.span\n            key={s.label}\n            aria-hidden=\"true\"\n            className={cn(\n              \"block h-full first:rounded-s-full last:rounded-e-full\",\n              s.reserved\n                ? \"bg-[repeating-linear-gradient(135deg,var(--border)_0_3px,transparent_3px_6px)]\"\n                : FILLS[fillIndex % FILLS.length]\n            )}\n            initial={reduce ? false : { width: 0 }}\n            animate={{ width: `${width}%` }}\n            transition={{ type: \"spring\", stiffness: 140, damping: 22 }}\n          />\n        )\n      })}\n    </div>\n  )\n}\n\nfunction Legend({ segments }: { segments: TokenSegment[] }) {\n  return (\n    <ul className=\"grid gap-1.5\">\n      {segments.map((s, i) => {\n        const fillIndex = segments.slice(0, i).filter((x) => !x.reserved).length\n        return (\n          <li key={s.label} className=\"flex items-center gap-2 text-[13px]\">\n            <span\n              aria-hidden=\"true\"\n              className={cn(\n                \"size-2.5 shrink-0 rounded-[3px]\",\n                s.reserved\n                  ? \"border bg-[repeating-linear-gradient(135deg,var(--border)_0_2px,transparent_2px_4px)]\"\n                  : FILLS[fillIndex % FILLS.length]\n              )}\n            />\n            <span className=\"text-foreground\">{s.label}</span>\n            {s.reserved && <span className=\"text-xs text-muted-foreground\">reserved</span>}\n            <span className=\"ms-auto font-mono text-xs text-muted-foreground tabular-nums\">{formatTokens(s.tokens)}</span>\n          </li>\n        )\n      })}\n    </ul>\n  )\n}\n\nfunction TokenMeter({ segments, limit, title, warnAt = 80, cost, action, className, ...props }: TokenMeterProps) {\n  const msg = useMessages()\n  title ??= msg(\"token-meter.title\", \"Context window\")\n  const { used, percent, state } = summarize(segments, limit, warnAt)\n  const left = Math.max(0, limit - used)\n  const text = msg(\"token-meter.summary\", \"{used} of {limit} tokens used, {percent}%. {state}.\", { used: formatTokens(used), limit: formatTokens(limit), percent: Math.round(percent), state: stateText(msg, state, left) })\n  return (\n    <div\n      data-slot=\"token-meter\"\n      data-state={state}\n      className={cn(\"grid w-full gap-3 rounded-xl border bg-card p-4 text-card-foreground\", className)}\n      {...props}\n    >\n      <div className=\"flex items-baseline justify-between gap-3\">\n        <p className=\"text-sm font-medium text-foreground\">{title}</p>\n        <p className=\"font-mono text-xs text-muted-foreground tabular-nums\">\n          <span className=\"text-foreground\">{formatTokens(used)}</span> / {formatTokens(limit)}\n        </p>\n      </div>\n      <Track segments={segments} limit={limit} label={title} valueText={text} />\n      <Legend segments={segments} />\n      <div className=\"flex flex-wrap items-center justify-between gap-2 border-t pt-3 text-[13px]\">\n        <p\n          className=\"flex items-center gap-1.5 text-foreground\"\n          role={state === \"ok\" ? undefined : \"status\"}\n        >\n          {state === \"ok\" ? (\n            <Info aria-hidden=\"true\" className=\"size-3.5 text-muted-foreground\" />\n          ) : (\n            <AlertTriangle aria-hidden=\"true\" className={cn(\"size-3.5\", state === \"full\" ? \"text-destructive\" : \"text-chart-3\")} />\n          )}\n          {stateText(msg, state, left)}\n        </p>\n        {cost && <p className=\"text-muted-foreground tabular-nums\">≈ {cost}</p>}\n        {state !== \"ok\" && action}\n      </div>\n    </div>\n  )\n}\n\ntype TokenMeterPillProps = Omit<React.ComponentProps<\"button\">, \"title\"> &\n  Pick<TokenMeterProps, \"segments\" | \"limit\" | \"warnAt\" | \"cost\" | \"action\" | \"title\"> & {\n    /** Which side of the pill the details open on. */\n    side?: \"top\" | \"bottom\"\n  }\n\n/** A compact ring and percentage for the composer toolbar. Opens the full meter in a popover. */\nfunction TokenMeterPill({\n  segments,\n  limit,\n  warnAt = 80,\n  cost,\n  action,\n  title,\n  side = \"top\",\n  className,\n  ...props\n}: TokenMeterPillProps) {\n  const msg = useMessages()\n  title ??= msg(\"token-meter.title\", \"Context window\")\n  const { used, percent, state } = summarize(segments, limit, warnAt)\n  const shown = Math.min(100, Math.round(percent))\n  const r = 7\n  const c = 2 * Math.PI * r\n  return (\n    <Popover>\n      <PopoverTrigger\n        data-slot=\"token-meter-pill\"\n        data-state-meter={state}\n        aria-label={msg(\"token-meter.pill\", \"{title}: {percent}% used. {state}.\", { title, percent: shown, state: stateText(msg, state, Math.max(0, limit - used)) })}\n        className={cn(\n          \"inline-flex h-8 items-center gap-1.5 rounded-full border bg-background px-2.5 text-xs font-medium text-foreground tabular-nums outline-none transition-colors hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/50 data-[state=open]:bg-accent\",\n          className\n        )}\n        {...props}\n      >\n        <svg aria-hidden=\"true\" viewBox=\"0 0 20 20\" className=\"size-4 -rotate-90\">\n          <circle cx=\"10\" cy=\"10\" r={r} fill=\"none\" strokeWidth=\"3\" className=\"stroke-muted\" />\n          <circle\n            cx=\"10\"\n            cy=\"10\"\n            r={r}\n            fill=\"none\"\n            strokeWidth=\"3\"\n            strokeLinecap=\"round\"\n            strokeDasharray={c}\n            strokeDashoffset={c * (1 - Math.min(1, percent / 100))}\n            className={cn(\n              \"transition-[stroke-dashoffset] duration-500 motion-reduce:transition-none\",\n              state === \"full\" ? \"stroke-destructive\" : state === \"warning\" ? \"stroke-chart-3\" : \"stroke-foreground\"\n            )}\n          />\n        </svg>\n        {shown}%\n        {state !== \"ok\" && <AlertTriangle aria-hidden=\"true\" className=\"size-3 text-muted-foreground\" />}\n      </PopoverTrigger>\n      <PopoverContent label={title} side={side} align=\"end\" className=\"w-[min(22rem,calc(100vw-1.5rem))] p-0\">\n        <TokenMeter segments={segments} limit={limit} title={title} warnAt={warnAt} cost={cost} action={action} className=\"border-0 bg-transparent\" />\n      </PopoverContent>\n    </Popover>\n  )\n}\n\nexport {\n  TokenMeter,\n  TokenMeterPill,\n  formatTokens,\n  estimateTokens,\n  type TokenMeterProps,\n  type TokenMeterPillProps,\n  type TokenSegment,\n}\n",
      "type": "registry:ui",
      "target": "@components/ballmac/token-meter.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "ai",
      "tokens",
      "context",
      "meter",
      "usage",
      "llm"
    ],
    "version": "1.0.0",
    "updated": "2026-09-30",
    "ai": {
      "summary": "segments is [{ label, tokens, reserved? }], limit is the window. The meter warns at warnAt percent (80) and again when full, with words and an icon. <TokenMeterPill> shows a ring and percentage. Exports formatTokens and estimateTokens.",
      "whenToUse": [
        "Showing how full the model's context is",
        "Nudging people to start a new chat or compact"
      ],
      "whenNotToUse": [
        "Plan quotas and billing limits; use usage-meter",
        "Generic progress; use progress"
      ],
      "composesWith": [
        "prompt-input",
        "model-picker",
        "usage-meter"
      ],
      "a11y": [
        {
          "keys": "Screen readers",
          "action": "role=meter with a value such as '142k of 200k tokens used, 71%. 58k left.'"
        },
        {
          "keys": "Enter / Space",
          "action": "Opens the pill's detail popover"
        },
        {
          "keys": "Color",
          "action": "Warning and full states add an icon and words"
        }
      ],
      "customization": [
        "segments and limit",
        "warnAt",
        "cost text",
        "action slot for a Compact button"
      ]
    },
    "examples": [
      "token-meter-demo",
      "token-meter-pill"
    ],
    "url": "https://ui.ballmac.com/components/token-meter"
  },
  "categories": [
    "ai"
  ],
  "type": "registry:ui"
}