{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "usage-meter",
  "title": "Usage Meter",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A plan-limit meter as a bar or ring, with normal, near-limit and over-limit states shown by icon and words, optional stacked segments and a meter role.",
  "dependencies": [
    "motion@^12",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/motion-presets.json",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/usage-meter.tsx",
      "content": "// Ballmac UI: Usage Meter. https://ui.ballmac.com/components/usage-meter\n\"use client\";\n\nimport * as React from \"react\";\nimport { AlertTriangle, CircleCheck, OctagonAlert } from \"lucide-react\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { ease } from \"@/lib/ballmac/motion\";\nimport { cn } from \"@/lib/utils\";\nimport { useLocale } from \"@/lib/ballmac/i18n\";\n\ntype UsageSegment = {\n  /** Name of this part of the total, for example \"Images\". */\n  label: string;\n  /** Amount used by this part. */\n  value: number;\n};\n\ntype UsageMeterProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  /** What is being measured, for example \"Storage\". */\n  label: string;\n  /** Amount used. Ignored when `segments` is given (then it is their sum). */\n  used?: number;\n  /** The plan limit. */\n  limit: number;\n  /** Unit shown after numbers, for example \"GB\" or \"requests\". */\n  unit?: string;\n  /** Break the total into parts, shown as a stacked bar and a legend. */\n  segments?: UsageSegment[];\n  /** Percent of the limit at which the meter turns to a warning. */\n  warnAt?: number;\n  /** Shape of the meter. */\n  variant?: \"bar\" | \"ring\";\n  /** Text under the numbers, for example \"Resets on Oct 1\". */\n  note?: React.ReactNode;\n  /** Content on the right of the header, such as an Upgrade button. */\n  action?: React.ReactNode;\n  /** Locale for number formatting. Fixed by default so server and browser match. */\n  locale?: string;\n};\n\nconst SEGMENT_COLORS = [\"bg-chart-1\", \"bg-chart-2\", \"bg-chart-3\", \"bg-chart-4\", \"bg-chart-5\"];\n\n/**\n * A plan-limit meter with three clear states: normal, near the limit, and over it. State is shown with an icon and words\n * as well as color. Exposes `role=\"meter\"` with a readable value, and can split the total into labelled segments.\n */\nfunction UsageMeter({\n  label,\n  used: usedProp,\n  limit,\n  unit,\n  segments,\n  warnAt = 80,\n  variant = \"bar\",\n  note,\n  action,\n  locale,\n  className,\n  ...props\n}: UsageMeterProps) {\n  const defaultLocale = useLocale()\n  locale ??= defaultLocale\n  const reduce = useReducedMotion();\n  const labelId = React.useId();\n  const used = segments ? segments.reduce((sum, s) => sum + s.value, 0) : (usedProp ?? 0);\n  const percent = limit > 0 ? (used / limit) * 100 : 0;\n  const state = percent >= 100 ? \"over\" : percent >= warnAt ? \"warn\" : \"ok\";\n  const fmt = new Intl.NumberFormat(locale, { maximumFractionDigits: 1 });\n  const text = `${fmt.format(used)} of ${fmt.format(limit)}${unit ? ` ${unit}` : \"\"}`;\n  const StateIcon = state === \"over\" ? OctagonAlert : state === \"warn\" ? AlertTriangle : CircleCheck;\n  const stateText =\n    state === \"over\" ? \"Over limit\" : state === \"warn\" ? `${Math.round(percent)}% used, nearing the limit` : `${Math.round(percent)}% used`;\n  const iconTone = state === \"over\" ? \"text-destructive\" : state === \"warn\" ? \"text-chart-3\" : \"text-chart-2\";\n  const fill = state === \"over\" ? \"bg-destructive\" : state === \"warn\" ? \"bg-chart-3\" : \"bg-primary\";\n  const clamp = Math.min(100, Math.max(0, percent));\n\n  const visual =\n    variant === \"ring\" ? (\n      <RingVisual percent={clamp} state={state} reduce={!!reduce} />\n    ) : (\n      <div\n        className=\"relative h-2.5 w-full overflow-hidden rounded-full bg-muted\"\n        aria-hidden=\"true\"\n      >\n        {segments ? (\n          <div className=\"flex h-full\" style={{ width: `${clamp}%` }}>\n            {segments.map((s, i) => (\n              <motion.span\n                key={s.label}\n                className={cn(\"h-full first:rounded-s-full last:rounded-e-full\", SEGMENT_COLORS[i % SEGMENT_COLORS.length])}\n                initial={reduce ? false : { width: 0 }}\n                animate={{ width: `${used ? (s.value / used) * 100 : 0}%` }}\n                transition={{ duration: 0.7, ease: ease.out, delay: reduce ? 0 : i * 0.06 }}\n                style={{ marginInlineEnd: i < segments.length - 1 ? 1 : 0 }}\n              />\n            ))}\n          </div>\n        ) : (\n          <motion.div\n            className={cn(\"h-full rounded-full\", fill)}\n            initial={reduce ? false : { width: 0 }}\n            animate={{ width: `${clamp}%` }}\n            transition={{ duration: 0.8, ease: ease.out }}\n          />\n        )}\n        {/* the warning mark */}\n        <span\n          className=\"absolute inset-y-0 w-px bg-background/80\"\n          style={{ left: `${warnAt}%` }}\n        />\n      </div>\n    );\n\n  return (\n    <div\n      data-slot=\"usage-meter\"\n      data-state={state}\n      className={cn(\n        \"grid gap-3 rounded-2xl border bg-card p-5 text-card-foreground shadow-[0_1px_2px_rgb(0_0_0/0.04)]\",\n        variant === \"ring\" && \"sm:grid-cols-[auto_1fr] sm:items-center sm:gap-6\",\n        className,\n      )}\n      {...props}\n    >\n      {variant === \"ring\" && <div className=\"mx-auto sm:mx-0\">{visual}</div>}\n      <div className=\"grid min-w-0 gap-3\">\n        <div className=\"flex items-start justify-between gap-3\">\n          <div className=\"min-w-0\">\n            <p id={labelId} className=\"text-sm font-semibold tracking-tight\">\n              {label}\n            </p>\n            <p className=\"mt-0.5 text-2xl font-semibold tracking-tight tabular-nums\">\n              {fmt.format(used)}\n              <span className=\"ms-1 text-sm font-normal text-muted-foreground\">\n                / {fmt.format(limit)}\n                {unit ? ` ${unit}` : \"\"}\n              </span>\n            </p>\n          </div>\n          {action}\n        </div>\n        {variant === \"bar\" && visual}\n        <div\n          role=\"meter\"\n          aria-labelledby={labelId}\n          aria-valuemin={0}\n          aria-valuemax={limit}\n          aria-valuenow={Math.min(used, limit)}\n          aria-valuetext={`${text}. ${stateText}.`}\n          className=\"flex flex-wrap items-center gap-x-2 gap-y-1 text-[13px]\"\n        >\n          <span className=\"flex items-center gap-1.5 font-medium\">\n            <StateIcon aria-hidden=\"true\" className={cn(\"size-4\", iconTone)} />\n            {stateText}\n          </span>\n          {note && <span className=\"text-muted-foreground\">· {note}</span>}\n        </div>\n        {segments && (\n          <ul className=\"grid grid-cols-2 gap-x-4 gap-y-1.5 border-t pt-3 text-[13px]\">\n            {segments.map((s, i) => (\n              <li key={s.label} className=\"flex items-center gap-2\">\n                <span aria-hidden=\"true\" className={cn(\"size-2 shrink-0 rounded-[3px]\", SEGMENT_COLORS[i % SEGMENT_COLORS.length])} />\n                <span className=\"truncate text-muted-foreground\">{s.label}</span>\n                <span className=\"ms-auto font-medium tabular-nums\">\n                  {fmt.format(s.value)}\n                  {unit ? ` ${unit}` : \"\"}\n                </span>\n              </li>\n            ))}\n          </ul>\n        )}\n      </div>\n    </div>\n  );\n}\n\nfunction RingVisual({ percent, state, reduce }: { percent: number; state: \"ok\" | \"warn\" | \"over\"; reduce: boolean }) {\n  const r = 42;\n  const c = 2 * Math.PI * r;\n  return (\n    <div className=\"relative size-28\" aria-hidden=\"true\">\n      <svg viewBox=\"0 0 100 100\" className=\"size-full -rotate-90\">\n        <circle cx=\"50\" cy=\"50\" r={r} fill=\"none\" strokeWidth=\"9\" className=\"stroke-muted\" />\n        <motion.circle\n          cx=\"50\"\n          cy=\"50\"\n          r={r}\n          fill=\"none\"\n          strokeWidth=\"9\"\n          strokeLinecap=\"round\"\n          className={state === \"over\" ? \"stroke-destructive\" : state === \"warn\" ? \"stroke-chart-3\" : \"stroke-primary\"}\n          strokeDasharray={c}\n          initial={reduce ? false : { strokeDashoffset: c }}\n          animate={{ strokeDashoffset: c * (1 - percent / 100) }}\n          transition={{ duration: 0.9, ease: ease.out }}\n        />\n      </svg>\n      <span className=\"absolute inset-0 flex items-center justify-center text-xl font-semibold tabular-nums\">\n        {Math.round(percent)}%\n      </span>\n    </div>\n  );\n}\n\nexport { UsageMeter, type UsageMeterProps, type UsageSegment };\n",
      "type": "registry:ui",
      "target": "@components/ballmac/usage-meter.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "usage",
      "limit",
      "quota",
      "meter"
    ],
    "version": "1.0.0",
    "updated": "2026-09-30",
    "ai": {
      "summary": "label, used (or segments), limit, unit. warnAt sets the warning percent. variant bar | ring. Exposes role=meter with a readable value.",
      "whenToUse": [
        "Storage, seats, API calls and other quotas",
        "Plan pages that nudge upgrades"
      ],
      "whenNotToUse": [
        "Task progress; use progress",
        "Multiple steps; use progress-steps"
      ],
      "composesWith": [
        "billing-card",
        "plan-selector",
        "progress"
      ],
      "a11y": [
        {
          "keys": "Screen readers",
          "action": "role=meter with aria-valuetext such as '86 of 100 k. 86% used, nearing the limit.'"
        },
        {
          "keys": "Color",
          "action": "State is conveyed by icon and text as well as color"
        },
        {
          "keys": "Reduced motion",
          "action": "Fills appear at their final size"
        }
      ],
      "customization": [
        "segments",
        "warnAt",
        "variant",
        "note and action slots"
      ]
    },
    "examples": [
      "usage-meter-demo",
      "usage-meter-states"
    ],
    "url": "https://ui.ballmac.com/components/usage-meter"
  },
  "categories": [
    "saas"
  ],
  "type": "registry:ui"
}