{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "meter",
  "title": "Meter",
  "description": "A labelled bar for a bounded measurement, with normal, warning and danger tones and a usage variant.",
  "dependencies": [
    "@base-ui/react@^1.9.0"
  ],
  "registryDependencies": [
    "https://sproutboat.com/ikada/r/theme.json",
    "https://sproutboat.com/ikada/r/cn.json",
    "https://sproutboat.com/ikada/r/format.json",
    "https://sproutboat.com/ikada/r/locale.json",
    "https://sproutboat.com/ikada/r/status.json",
    "https://sproutboat.com/ikada/r/status-glyph.json"
  ],
  "files": [
    {
      "path": "registry/ikada/ui/meter.tsx",
      "content": "\"use client\";\n\nimport { Meter as BaseMeter } from \"@base-ui/react/meter\";\nimport { useId } from \"react\";\nimport type { ComponentProps, ReactNode } from \"react\";\nimport { cn } from \"@/components/ikada/lib/cn\";\nimport { formatCompact } from \"@/components/ikada/lib/format\";\nimport { useLocale } from \"@/components/ikada/lib/locale\";\nimport { StatusGlyph } from \"@/components/ikada/status-glyph\";\nimport type { Status } from \"@/components/ikada/status\";\n\nexport type MeterTone = \"normal\" | \"warning\" | \"danger\";\n\nconst fill = {\n  normal: \"bg-fg\",\n  warning: \"bg-warn-dot\",\n  danger: \"bg-danger-dot\",\n} satisfies Record<MeterTone, string>;\n\nexport interface MeterProps extends Omit<ComponentProps<typeof BaseMeter.Root>, \"className\" | \"children\" | \"locale\"> {\n  className?: string;\n  /** Number-formatting locale; defaults to the `LocaleProvider` value, else `en`. */\n  locale?: string;\n  /** Visible label above the bar. Without one, pass `aria-label`. */\n  label?: ReactNode;\n  /** Fill colour: normal, warning (amber) or danger (red). Default normal. */\n  tone?: MeterTone;\n  /** Right of the label; defaults to the formatted value. */\n  valueLabel?: ReactNode;\n}\n\n/** A bar for a bounded measurement. The fill grows over 250ms ease-out (a width change, so it also stays under reduced motion). */\nexport function Meter({ label, tone = \"normal\", valueLabel, className, locale, ...props }: MeterProps) {\n  const resolved = useLocale(locale);\n  // Base UI does not always link the label to the meter; do it here so the meter always has a name.\n  const labelId = useId();\n  return (\n    <BaseMeter.Root\n      data-slot=\"meter\"\n      className={cn(\"flex flex-col gap-1.5\", className)}\n      locale={resolved}\n      aria-labelledby={label ? labelId : undefined}\n      {...props}\n    >\n      {label || valueLabel !== null ? (\n        <div className=\"flex flex-wrap items-baseline justify-between gap-x-3 gap-y-0.5 text-sb-base\">\n          {label ? (\n            <BaseMeter.Label id={labelId} className=\"font-medium\">\n              {label}\n            </BaseMeter.Label>\n          ) : (\n            <span />\n          )}\n          {valueLabel === undefined ? <BaseMeter.Value className=\"tabular-nums text-fg-muted\" /> : valueLabel}\n        </div>\n      ) : null}\n      <BaseMeter.Track\n        data-slot=\"meter-track\"\n        className=\"h-2 w-full overflow-hidden rounded-full shadow-[inset_0_0_0_1px_var(--sb-hairline-strong)]\"\n      >\n        <BaseMeter.Indicator\n          data-slot=\"meter-indicator\"\n          data-tone={tone}\n          className={cn(\n            \"h-full rounded-full transition-[width] duration-(--sb-duration-slide) ease-out motion-reduce:transition-none\",\n            fill[tone],\n          )}\n        />\n      </BaseMeter.Track>\n    </BaseMeter.Root>\n  );\n}\n\nexport interface UsageMeterProps extends Omit<MeterProps, \"value\" | \"max\" | \"min\" | \"tone\" | \"valueLabel\" | \"format\"> {\n  /** Amount used so far, in the same unit as the limit. */\n  used: number;\n  /** `null` is unlimited: the bar is replaced by the number alone. */\n  limit: number | null;\n  /** Unit suffix, e.g. \"GB\" or \"requests\". */\n  unit?: string;\n  /** Near-limit threshold as a fraction. Default 0.8. */\n  warnAt?: number;\n  /**\n   * The platform is actually refusing work (writes rejected, deploys paused). Only then\n   * is the meter red. Reaching the limit without being blocked stays amber.\n   */\n  blocked?: boolean;\n  /** Defaults to compact units in the resolved locale: 860K, 1M, 4.4. */\n  formatValue?: (n: number) => string;\n}\n\ntype UsageState = \"under\" | \"near\" | \"at\" | \"blocked\" | \"unlimited\";\n\nexport function usageState(used: number, limit: number | null, warnAt = 0.8, blocked = false): UsageState {\n  if (limit === null) return \"unlimited\";\n  if (blocked) return \"blocked\";\n  if (used >= limit) return \"at\";\n  return used >= limit * warnAt ? \"near\" : \"under\";\n}\n\nconst stateText = {\n  under: null,\n  near: \"Near limit\",\n  at: \"At limit\",\n  blocked: \"Blocked\",\n  unlimited: null,\n} satisfies Record<UsageState, string | null>;\n\nfunction stateStatus(state: UsageState): Status | undefined {\n  if (state === \"near\" || state === \"at\") return \"warning\";\n  return state === \"blocked\" ? \"danger\" : undefined;\n}\n\n/**\n * Usage against a plan limit. Amber from 80% and at 100%; red only when `blocked`. The\n * state is also written out (never color alone) and read by screen readers.\n */\nexport function UsageMeter({\n  used,\n  limit,\n  unit,\n  warnAt = 0.8,\n  blocked = false,\n  formatValue: formatValueProp,\n  locale,\n  label,\n  className,\n  ...props\n}: UsageMeterProps) {\n  const resolved = useLocale(locale);\n  const formatValue = formatValueProp ?? ((n: number) => formatCompact(n, { locale: resolved }));\n  const state = usageState(used, limit, warnAt, blocked);\n  const suffix = unit ? ` ${unit}` : \"\";\n  const text =\n    limit === null\n      ? `${formatValue(used)}${suffix}, unlimited`\n      : `${formatValue(used)} of ${formatValue(limit)}${suffix}`;\n  const stateLabel = stateText[state];\n  const status = stateStatus(state);\n\n  if (limit === null) {\n    return (\n      <div className={cn(\"flex items-baseline justify-between gap-3 text-sb-base\", className)}>\n        <span className=\"font-medium\">{label}</span>\n        <span className=\"tabular-nums text-fg-muted\">{text}</span>\n      </div>\n    );\n  }\n\n  return (\n    <Meter\n      label={label}\n      locale={resolved}\n      value={Math.min(used, limit)}\n      max={limit}\n      tone={state === \"blocked\" ? \"danger\" : status ? \"warning\" : \"normal\"}\n      getAriaValueText={() => `${text}${stateLabel ? `, ${stateLabel.toLowerCase()}` : \"\"}`}\n      valueLabel={\n        <span className=\"inline-flex items-center gap-2 whitespace-nowrap tabular-nums text-fg-muted\">\n          {stateLabel && status ? (\n            <span\n              className={cn(\n                \"inline-flex items-center gap-1.5 font-semibold\",\n                status === \"danger\" ? \"text-danger-fg\" : \"text-warn-fg\",\n              )}\n            >\n              <StatusGlyph tone={status} />\n              {stateLabel}\n            </span>\n          ) : null}\n          {text}\n        </span>\n      }\n      className={className}\n      {...props}\n    />\n  );\n}\n",
      "type": "registry:ui",
      "target": "@components/ikada/meter.tsx"
    }
  ],
  "categories": [
    "feedback",
    "data-display"
  ],
  "type": "registry:ui"
}