{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "usage-meter",
  "type": "registry:ui",
  "title": "Usage Meter",
  "description": "Quota bar split by category that warns near the limit and shows overage honestly.",
  "categories": [
    "data"
  ],
  "dependencies": [
    "@base-ui/react",
    "@number-flow/react",
    "motion"
  ],
  "registryDependencies": [
    "https://www.stealth.pm/r/tokens.json",
    "https://www.stealth.pm/r/cn.json",
    "https://www.stealth.pm/r/icons.json",
    "https://www.stealth.pm/r/motion.json"
  ],
  "files": [
    {
      "path": "components/ui/usage-meter.tsx",
      "content": "\"use client\";\nimport { Meter } from \"@base-ui/react/meter\";\nimport NumberFlow from \"@number-flow/react\";\nimport { useReducedMotion } from \"motion/react\";\nimport { useState, useSyncExternalStore } from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { Alert, Warning } from \"@/lib/icons\";\nimport { ease } from \"@/lib/motion\";\n\nconst noop = () => () => {};\nfunction useLocale(locale?: string) {\n  const detected = useSyncExternalStore(\n    noop,\n    () => Intl.NumberFormat().resolvedOptions().locale,\n    () => \"en-US\",\n  );\n  return locale ?? detected;\n}\n\nexport type UsageSegment = { label: string; value: number };\nexport type UsageState = \"ok\" | \"warning\" | \"over\";\n\n/** Totals and state for a quota, for when the numbers drive something other than the bar. */\nexport function getUsage({ value, segments, max, warnAt = 0.8 }: { value?: number; segments?: UsageSegment[]; max: number; warnAt?: number }) {\n  const used = segments ? segments.reduce((sum, s) => sum + Math.max(0, s.value), 0) : Math.max(0, value ?? 0);\n  const ratio = max > 0 ? used / max : 0;\n  const state: UsageState = ratio > 1 ? \"over\" : ratio >= warnAt ? \"warning\" : \"ok\";\n  return { used, ratio, state, remaining: Math.max(0, max - used), over: Math.max(0, used - max) };\n}\n\n// Categories step down the foreground ramp: nearest first, fading back.\nconst ramp = [\"bg-fg\", \"bg-fg-2\", \"bg-fg-3\", \"bg-fg-4\"];\nconst dots = [\"bg-fg\", \"bg-fg-2\", \"bg-fg-3\", \"bg-fg-4\"];\n\nconst expo = `cubic-bezier(${ease.out.join(\",\")})`;\nconst timing = {\n  transformTiming: { duration: 600, easing: expo },\n  spinTiming: { duration: 600, easing: expo },\n  opacityTiming: { duration: 200, easing: \"ease-out\" },\n};\n\nexport type UsageMeterProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  /** What is being used: \"Storage\", \"Seats\", \"API requests\". */\n  label: string;\n  /** The quota. */\n  max: number;\n  /** Total used, when there are no categories. */\n  value?: number;\n  /** Used, split by category. Drawn in order; the total is their sum. */\n  segments?: UsageSegment[];\n  /** Short unit after the numbers: \"GB\", \"seats\". */\n  unit?: string;\n  /** Maximum fraction digits in the numbers. */\n  decimals?: number;\n  /** Share of the quota at which it turns to the warning state. */\n  warnAt?: number;\n  /** sm is a thin bar with the header only, for sidebars and table cells. */\n  size?: \"sm\" | \"md\";\n  /** Show the category legend. Defaults to on when there is more than one segment (md only). */\n  legend?: boolean;\n  /** First load: skeleton in the same shape. */\n  loading?: boolean;\n  /** Shown beside the status once it warns or goes over, e.g. an Upgrade button. */\n  action?: React.ReactNode;\n  locale?: string;\n};\n\nexport function UsageMeter({\n  label,\n  max,\n  value,\n  segments: segmentsProp,\n  unit,\n  decimals = 1,\n  warnAt = 0.8,\n  size = \"md\",\n  legend,\n  loading = false,\n  action,\n  locale: localeProp,\n  className,\n  ...rest\n}: UsageMeterProps) {\n  const locale = useLocale(localeProp);\n  const reduce = useReducedMotion();\n  const [hot, setHot] = useState<number | null>(null);\n  const segments = segmentsProp ?? [{ label, value: value ?? 0 }];\n  const single = !segmentsProp || segmentsProp.length === 1;\n  const { used, ratio, state, remaining, over } = getUsage({ value, segments: segmentsProp, max, warnAt });\n  const showLegend = size === \"md\" && (legend ?? !single);\n\n  // Over the limit, the scale stretches to the total and a marker shows where the quota ends.\n  const scale = Math.max(max, used) || 1;\n  const limitAt = (max / scale) * 100;\n\n  const fmt = new Intl.NumberFormat(locale, { maximumFractionDigits: decimals });\n  const withUnit = (n: number) => (unit ? `${fmt.format(n)} ${unit}` : fmt.format(n));\n  const valueText =\n    `${fmt.format(used)} of ${withUnit(max)} used, ${Math.round(ratio * 100)}%` +\n    (state === \"over\" ? `, ${withUnit(over)} over the limit` : state === \"warning\" ? \", almost full\" : \"\");\n\n  const widths = segments.map((s) => (Math.max(0, s.value) / scale) * 100);\n  const bars = segments.map((s, i) => ({ ...s, i, width: widths[i], start: widths.slice(0, i).reduce((a, b) => a + b, 0) }));\n  const last = bars.findLastIndex((b) => b.width > 0);\n\n  const status =\n    state === \"over\" ? (\n      <span className=\"flex items-center gap-1.5 text-danger\">\n        <Alert size={14} className=\"shrink-0\" />\n        {withUnit(over)} over the limit\n      </span>\n    ) : state === \"warning\" ? (\n      <span className=\"flex items-center gap-1.5 text-warning\">\n        <Warning size={14} className=\"shrink-0\" />\n        Almost full · {withUnit(remaining)} left\n      </span>\n    ) : (\n      <span className=\"text-fg-3\">{withUnit(remaining)} left</span>\n    );\n\n  return (\n    <Meter.Root\n      value={Math.min(used, max)}\n      max={max}\n      aria-valuetext={loading ? \"Loading\" : valueText}\n      aria-busy={loading || undefined}\n      data-state={loading ? \"loading\" : state}\n      data-size={size}\n      className={cn(\"flex w-full min-w-0 flex-col\", size === \"sm\" ? \"gap-1.5\" : \"gap-2.5\", className)}\n      {...rest}\n    >\n      <div className=\"flex min-w-0 items-baseline justify-between gap-3\">\n        <Meter.Label className={cn(\"min-w-0 truncate text-fg\", size === \"sm\" ? \"text-[12px]\" : \"text-[13px] font-medium tracking-[-0.01em]\")}>\n          {label}\n        </Meter.Label>\n        {loading ? (\n          <span aria-hidden className=\"h-3 w-20 self-center rounded-sm bg-hover motion-safe:animate-pulse-soft\" />\n        ) : (\n          <span aria-hidden className={cn(\"flex shrink-0 items-baseline gap-1 whitespace-nowrap tabular\", size === \"sm\" ? \"text-[11.5px]\" : \"text-[12.5px]\")}>\n            <span\n              className={cn(\n                \"inline-flex h-[1lh] items-center font-medium transition-colors duration-300\",\n                state === \"over\" ? \"text-danger\" : state === \"warning\" ? \"text-warning\" : \"text-fg\",\n              )}\n            >\n              <NumberFlow value={used} locales={locale} format={{ maximumFractionDigits: decimals }} animated={!reduce} {...timing} />\n            </span>\n            <span className=\"text-fg-3\">of {withUnit(max)}</span>\n          </span>\n        )}\n      </div>\n\n      <Meter.Track\n        className={cn(\"relative w-full overflow-hidden rounded-full bg-fg/[0.08]\", size === \"sm\" ? \"h-1\" : \"h-2\")}\n        onPointerLeave={() => setHot(null)}\n      >\n        {!loading && (\n          // The fill wipes in from the left on first paint, then each segment glides to its new place.\n          <div className=\"absolute inset-0 transition-[clip-path] duration-[900ms] ease-out-expo [clip-path:inset(0_0_0_0)] starting:[clip-path:inset(0_100%_0_0)] motion-reduce:transition-none\">\n            {bars.map((b) => (\n              <div\n                key={b.label}\n                onPointerEnter={() => setHot(b.i)}\n                className={cn(\n                  \"absolute inset-y-0 transition-[left,width,opacity,background-color] duration-[600ms] ease-in-out-quart motion-reduce:transition-none\",\n                  single ? (state === \"over\" ? \"bg-danger\" : state === \"warning\" ? \"bg-warning\" : \"bg-fg\") : ramp[b.i % ramp.length],\n                  hot != null && hot !== b.i && \"opacity-30\",\n                )}\n                style={{\n                  left: `${b.start}%`,\n                  // A 2px seam between categories, none after the last.\n                  width: b.i === last || single ? `${b.width}%` : `max(0px, calc(${b.width}% - 2px))`,\n                }}\n              />\n            ))}\n            {state === \"over\" && (\n              <div\n                className=\"absolute inset-y-0 right-0 bg-[repeating-linear-gradient(-45deg,var(--danger)_0_2px,transparent_2px_5px)] opacity-90 transition-[left] duration-[600ms] ease-in-out-quart\"\n                style={{ left: `${limitAt}%` }}\n              />\n            )}\n          </div>\n        )}\n        {loading && <div className=\"absolute inset-0 bg-hover motion-safe:animate-pulse-soft\" />}\n        {!loading && state === \"over\" && (\n          <div aria-hidden className=\"absolute inset-y-0 w-0.5 -translate-x-1/2 bg-raised\" style={{ left: `${limitAt}%` }} />\n        )}\n      </Meter.Track>\n\n      {showLegend && (\n        <ul aria-label={`${label} by category`} className=\"flex flex-wrap gap-x-4 gap-y-1.5\" onPointerLeave={() => setHot(null)}>\n          {bars.map((b) => (\n            <li\n              key={b.label}\n              onPointerEnter={() => setHot(b.i)}\n              className={cn(\"flex items-center gap-1.5 text-[12px] transition-opacity duration-150\", hot != null && hot !== b.i && \"opacity-45\")}\n            >\n              <span aria-hidden className={cn(\"size-2 shrink-0 rounded-[2px]\", dots[b.i % dots.length])} />\n              <span className=\"text-fg-2\">{b.label}</span>\n              <span className=\"text-fg-3 tabular\">{loading ? \"–\" : withUnit(b.value)}</span>\n            </li>\n          ))}\n        </ul>\n      )}\n\n      {size === \"md\" && !loading && (\n        <div className=\"flex min-h-7 items-center justify-between gap-3 text-[12px]\">\n          <span role=\"status\" className=\"min-w-0\">\n            {status}\n          </span>\n          {state !== \"ok\" && action}\n        </div>\n      )}\n    </Meter.Root>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "https://www.stealth.pm/ui/usage-meter"
}
