{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "usage-meter",
  "title": "Usage meter",
  "description": "Shows how much AI credit is left — the number, a bar in the AI gradient, when it renews, what the rest will buy, and a breakdown — with a compact ring for a composer footer.",
  "registryDependencies": [
    "progress"
  ],
  "files": [
    {
      "path": "registry/lib/usage-meter.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport { Progress } from '@/components/ui/progress'\nimport { cn } from '@/lib/utils'\n\nexport type UsageBreakdownRow = { label: string; value: number }\n\nexport type UsageMeterProps = {\n  /** What is left to spend. */\n  remaining: number\n  /** The whole allowance for the period. */\n  total: number\n  /** Names the allowance: \"AI credits\". */\n  label?: string\n  /** The plural the numbers count: \"credits\", \"messages\". */\n  unit?: string\n  /** The same word for exactly one: \"credit\". Defaults to `unit` without its final \"s\"; pass it for a plural that is not just +s. */\n  unitOne?: string\n  /** When it refills, as a finished phrase: \"Renews 1 November\". */\n  renews?: string\n  /** What the rest will buy, in plain words: \"About 150 more long answers.\" */\n  estimate?: string\n  /** Where it went so far, one row each. */\n  breakdown?: UsageBreakdownRow[]\n  /** A button for the card's last row: \"Get more credits\". */\n  action?: React.ReactNode\n  /** The share of the total at or under which the meter reads low. 0.1 is a tenth. */\n  lowAt?: number\n  /** The words a low bar or card shows; every variant speaks them to screen readers. */\n  lowLabel?: string\n  /** card: everything, for a settings page. bar: the numbers and the bar. compact: a ring and one line, for a composer footer. */\n  variant?: 'card' | 'bar' | 'compact'\n  /** How the numbers are written. Fixed by default so the server and the browser agree. */\n  locale?: string\n  className?: string\n}\n\n// An amount the app did not clean (NaN, Infinity) counts as nothing rather than printing \"NaN\".\nconst finite = (value: number) => (Number.isFinite(value) ? value : 0)\n\n// The stock Progress draws its own track and fill; these reach them by the data-slot it gives each.\n// The track has to be seen against the card, whose colour the muted surface nearly matches (1.06:1 at 80%, 1.08:1 at full\n// strength, measured on the card in Dawn). So in the light themes it takes a little of the control line's colour. That\n// brings the fill's brightness closer to the track than 3:1; it still stands apart by colour, and the amount is always\n// written in numbers above the bar (Ryan's call, 2026-10-06). The dark themes keep the full muted surface.\nconst TRACK = '[&_[data-slot=progress-track]]:h-1.5 [&_[data-slot=progress-track]]:bg-[color-mix(in_oklab,var(--input)_15%,var(--muted))] dark:[&_[data-slot=progress-track]]:bg-muted'\n// The one place the AI gradient sits here. A low meter keeps it: \"running low\" is said in words, the bar never changes colour.\n// The gradient is laid across the whole track and the fill reveals it from the left, so a nearly empty bar is plain gold\n// and not all three colours squeezed into a dot. The fill is `share` of the track wide, so a background 1/share of the\n// fill's own width is exactly the track's width: no measuring.\n// Only the width eases when the amount changes (the stock indicator eases everything): a gradient size that eased\n// with it would show the wrong colours for the length of the move.\nconst FILL = '[&_[data-slot=progress-indicator]]:rounded-full [&_[data-slot=progress-indicator]]:ai-meter [&_[data-slot=progress-indicator]]:bg-no-repeat [&_[data-slot=progress-indicator]]:transition-[width] [&_[data-slot=progress-indicator]]:[background-size:calc(100%/var(--usage-share,1))_100%]'\n\n/** Shows how much AI credit is left — the number, a bar in the AI gradient, when it renews, what the rest will buy, and a breakdown — with a compact ring for a composer footer. */\nexport function UsageMeter({\n  remaining, total, label = 'AI credits', unit = 'credits', unitOne, renews, estimate, breakdown, action,\n  lowAt = 0.1, lowLabel = 'Running low', variant = 'card', locale = 'en-US', className,\n}: UsageMeterProps) {\n  const titleId = React.useId()\n  // useId keeps the gradient's id unique per meter; a shared id paints every later ring black once the first leaves the page.\n  const gradientId = React.useId().replace(/:/g, '')\n  const max = Math.max(0, finite(total))\n  const left = Math.min(max, Math.max(0, finite(remaining)))\n  const share = max > 0 ? left / max : 0\n  // Nothing left, or no allowance at all, is low too.\n  const low = share <= Math.min(1, Math.max(0, finite(lowAt)))\n  const whole = new Intl.NumberFormat(locale)\n  const short = new Intl.NumberFormat(locale, { notation: 'compact', maximumFractionDigits: 1 })\n  // \"1 credits left\" read wrong; one left takes the singular.\n  const leftUnit = left === 1 ? (unitOne ?? unit.replace(/s$/, '')) : unit\n  const spoken = `${whole.format(left)} of ${whole.format(max)} ${leftUnit} left${low ? `. ${lowLabel}` : ''}`\n  const rows = breakdown ?? []\n\n  if (variant === 'compact') {\n    return (\n      <span data-slot=\"usage-meter\" data-variant=\"compact\" data-low={low || undefined}\n        className={cn('inline-flex items-center gap-2 text-xs tabular-nums text-muted-foreground', className)}>\n        {/* Turned a quarter so the arc starts at the top. A ring that fills around its edge, never a pie wedge. */}\n        <svg aria-hidden width=\"18\" height=\"18\" viewBox=\"0 0 18 18\" className=\"shrink-0 -rotate-90\">\n          <defs>\n            <linearGradient id={gradientId} x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">\n              <stop offset=\"0\" stopColor=\"var(--ai-text-from)\" />\n              <stop offset=\"0.5\" stopColor=\"var(--ai-via)\" />\n              <stop offset=\"1\" stopColor=\"var(--ai-to)\" />\n            </linearGradient>\n          </defs>\n          <circle data-slot=\"usage-ring-track\" cx=\"9\" cy=\"9\" r=\"7\" fill=\"none\" strokeWidth=\"2.5\" className=\"stroke-[color-mix(in_oklab,var(--input)_15%,var(--muted))] dark:stroke-muted\" />\n          {/* No arc at zero: a zero-length stroke with round caps still paints a dot. For the same reason any share above zero draws at least 1 of 100: under half a percent would round to a dash of 0. */}\n          {share > 0 ? (\n            <circle data-slot=\"usage-arc\" cx=\"9\" cy=\"9\" r=\"7\" fill=\"none\" strokeWidth=\"2.5\" strokeLinecap=\"round\" pathLength={100}\n              strokeDasharray={`${Math.max(1, Math.round(share * 100))} 100`} stroke={`url(#${gradientId})`} />\n          ) : null}\n        </svg>\n        {/* The visible line is the same low or not; low is for screen readers only, said before the amount. */}\n        <span>\n          {low ? <span className=\"sr-only\">{lowLabel}: </span> : null}\n          {short.format(left)} {leftUnit} left\n          <span className=\"sr-only\"> of {whole.format(max)}</span>\n        </span>\n      </span>\n    )\n  }\n\n  const meter = (\n    <div data-slot=\"usage-bar\" className=\"grid gap-1.5\">\n      {/* The total always shows and never shrinks; on a narrow bar the left group wraps under itself instead. */}\n      <p className=\"flex items-baseline justify-between gap-x-3 text-sm tabular-nums\">\n        <span className=\"min-w-0 text-ink-soft\">\n          <span className=\"text-base font-semibold text-foreground\">{whole.format(left)}</span> left\n          {/* The dot travels with the words, so a narrow bar wraps to \"· Running low\" and never leaves the dot hanging. */}\n          {low ? <>{' '}<span className=\"inline-block\">{'· '}<span className=\"font-semibold text-destructive\">{lowLabel}</span></span></> : null}\n        </span>\n        <span className=\"shrink-0 whitespace-nowrap text-muted-foreground\">of {whole.format(max)}</span>\n      </p>\n      {/* max must be above zero for the primitive; with no allowance the bar is simply empty. */}\n      {/* An empty bar draws no fill; the tiny floor only keeps the gradient's size from dividing by zero. */}\n      <Progress aria-label={`${label} left`} value={left} max={max > 0 ? max : 1} getAriaValueText={() => spoken} className={cn(TRACK, FILL)}\n        style={{ '--usage-share': Math.max(share, 0.0001) } as React.CSSProperties} />\n    </div>\n  )\n\n  if (variant === 'bar') {\n    return <div data-slot=\"usage-meter\" data-variant=\"bar\" data-low={low || undefined} className={cn('text-sm', className)}>{meter}</div>\n  }\n\n  return (\n    <div data-slot=\"usage-meter\" data-variant=\"card\" data-low={low || undefined} role=\"group\" aria-labelledby={titleId}\n      className={cn('grid grid-cols-[minmax(0,1fr)] gap-3 rounded-xl border border-border bg-card p-4 text-sm shadow-sm', className)}>\n      <div className=\"flex items-baseline justify-between gap-3\">\n        <p id={titleId} className=\"font-semibold text-foreground\">{label}</p>\n        {renews ? <p className=\"text-xs text-muted-foreground\">{renews}</p> : null}\n      </div>\n      {meter}\n      {estimate ? <p className=\"text-xs text-muted-foreground\">{estimate}</p> : null}\n      {rows.length ? (\n        <table className=\"w-full text-xs tabular-nums\">\n          <caption className=\"sr-only\">Used so far</caption>\n          <tbody>\n            {rows.map((row, index) => (\n              // Labels may repeat; the list is never reordered, so position is a stable key.\n              <tr key={index} className=\"border-t border-border\">\n                <th scope=\"row\" className=\"py-1.5 text-left font-normal text-foreground\">{row.label}</th>\n                <td className=\"py-1.5 text-right text-muted-foreground\">{whole.format(finite(row.value))}</td>\n              </tr>\n            ))}\n          </tbody>\n        </table>\n      ) : null}\n      {action ? <div className=\"flex justify-end\">{action}</div> : null}\n    </div>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/usage-meter.tsx"
    }
  ],
  "docs": "Shows how much AI credit is left — the number, a bar in the AI gradient, when it renews, what the rest will buy, and a breakdown — with a compact ring for a composer footer. Do: Let a low bar or card say \"Running low\" in words beside the amount; the bar itself does not change colour. Don't: Rely on the short fill alone to say credit is low. Do: Keep the AI gradient on the fill of the bar and the ring. The gradient is laid across the whole bar and the fill reveals it, so a nearly empty bar is plain gold. Everything else on the card is plain. Don't: Put the gradient on the number, the card or the \"Get more\" button. Do: Add a plain-language line (\"About 150 more long answers\") so the number means something. Don't: Show a bare count of credits or tokens and leave people to work out what it buys. Do: Use the compact ring where space is tight: a composer footer, a sidebar. It looks the same whether or not credit is low. Don't: Redraw the compact meter as a filled pie wedge, or drop its text and leave the ring alone. Use progress when the bar shows how far a task has got, not how much of an allowance is left. Use stat-cards when the number is a business metric on a dashboard, not an allowance being spent. Use ai-notice when you only need one line of small print under the composer, with no amount.",
  "type": "registry:ui"
}