{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "meter",
  "title": "Meter",
  "description": "Usage against a limit as a bar or ring, with warning and danger states.",
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "@ikas/animated-number",
    "@ikas/chart-kit",
    "@ikas/motion",
    "@ikas/status",
    "@ikas/swap-text",
    "@ikas/utils"
  ],
  "files": [
    {
      "path": "registry/ikas/meter.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { AlertTriangleIcon, OctagonAlertIcon } from \"lucide-react\"\nimport { animate, motion, useMotionValue, useReducedMotion, useTransform } from \"motion/react\"\n\nimport { SPRING, springOrInstant } from \"@/lib/motion\"\nimport { cn } from \"@/lib/utils\"\nimport { statusTint } from \"@/lib/status\"\nimport { AnimatedNumber } from \"@/components/ikas/animated-number\"\nimport { defaultNumberFormat } from \"@/components/ikas/chart-kit\"\nimport { SwapText } from \"@/components/ikas/swap-text\"\n\ntype MeterStatus = \"neutral\" | \"success\" | \"warning\" | \"danger\"\n\n/** Fill + track from the same ramp, so the state reads across the whole bar. */\nconst track = (fill: string, amount: number) => `color-mix(in oklab, ${fill} ${amount}%, var(--card))`\nconst statusColors: Record<MeterStatus, { fill: string; track: string }> = {\n  neutral: { fill: \"var(--chart-ink)\", track: track(\"var(--chart-ink)\", 9) },\n  success: { fill: \"var(--success)\", track: track(\"var(--success)\", 16) },\n  warning: { fill: \"var(--warning)\", track: track(\"var(--warning)\", 22) },\n  danger: { fill: \"var(--danger)\", track: track(\"var(--danger)\", 16) },\n}\n\n/** Status is never color alone: warning and danger carry an icon and words. Icon and text use the Banner tint. */\nconst statusNote: Partial<Record<MeterStatus, { icon: React.ElementType; label: string; className: string }>> = {\n  warning: { icon: AlertTriangleIcon, label: \"Limite yaklaşıyor\", className: statusTint.warning },\n  danger: { icon: OctagonAlertIcon, label: \"Limit doldu\", className: statusTint.danger },\n}\n\n/** auto: ≥ 90% danger, ≥ 75% warning, otherwise neutral. */\nfunction resolveStatus(status: MeterStatus | \"auto\", ratio: number): MeterStatus {\n  if (status !== \"auto\") return status\n  if (ratio >= 0.9) return \"danger\"\n  if (ratio >= 0.75) return \"warning\"\n  return \"neutral\"\n}\n\ntype MeterProps = {\n  value: number\n  max: number\n  label: React.ReactNode\n  /** neutral, success, warning or danger. `auto` picks from the ratio: 75% warning, 90% danger. */\n  status?: MeterStatus | \"auto\"\n  /** Formats value and max in the caption. */\n  valueFormat?: (value: number) => string\n  size?: \"sm\" | \"default\"\n  className?: string\n}\n\n/** Usage against a limit: plan quotas, API calls, storage. */\nfunction Meter({ value, max, label, status = \"auto\", valueFormat = defaultNumberFormat, size = \"default\", className }: MeterProps) {\n  const reduce = useReducedMotion()\n  const ratio = max > 0 ? Math.min(value / max, 1) : 0\n  const resolved = resolveStatus(status, ratio)\n  const colors = statusColors[resolved]\n  const note = statusNote[resolved]\n  const labelId = React.useId()\n\n  return (\n    <div data-slot=\"meter\" data-status={resolved} className={cn(\"flex flex-col gap-2\", className)}>\n      <div className=\"flex items-baseline justify-between gap-3 text-[13px]\">\n        <span id={labelId} className=\"font-medium text-foreground\">\n          {label}\n        </span>\n        <span className=\"text-muted-foreground tabular-nums\">\n          <AnimatedNumber value={value} format={valueFormat} className=\"font-medium text-foreground\" /> / {valueFormat(max)}\n        </span>\n      </div>\n      <div\n        role=\"meter\"\n        aria-labelledby={labelId}\n        aria-valuemin={0}\n        aria-valuemax={max}\n        aria-valuenow={value}\n        aria-valuetext={`${valueFormat(value)} / ${valueFormat(max)}`}\n        className={cn(\"overflow-hidden rounded-full\", size === \"sm\" ? \"h-1.5\" : \"h-2\")}\n        style={{ background: colors.track }}\n      >\n        <motion.div\n          className=\"h-full rounded-full\"\n          style={{ background: colors.fill }}\n          initial={reduce ? false : { width: 0 }}\n          animate={{ width: `${ratio * 100}%` }}\n          transition={springOrInstant(reduce)}\n        />\n      </div>\n      {note && (\n        <span className={cn(\"flex items-center gap-1.5 text-[12px] font-medium text-(--s-text)\", note.className)}>\n          <note.icon className=\"size-3.5 text-(--s-icon)\" aria-hidden />\n          {note.label}\n        </span>\n      )}\n    </div>\n  )\n}\n\ntype RadialMeterBand = {\n  /** Lowest value of the band, inclusive. */\n  from: number\n  status: MeterStatus\n  /** Named under the number, e.g. \"Hedefte\". Status is never color alone. */\n  label: string\n}\n\ntype RadialMeterProps = {\n  value: number\n  max?: number\n  label?: React.ReactNode\n  /** Ring color. Usually neutral while running, success when done. Bands override it in the gauge. */\n  status?: MeterStatus | \"auto\"\n  /** Diameter in px (ring) or width in px (gauge). */\n  size?: number\n  strokeWidth?: number\n  /** ring: a full circle with the value inside. gauge: a half circle for a score, with a band label. */\n  variant?: \"ring\" | \"gauge\"\n  /** Text in the middle, e.g. `(v, max) => \\`${v}/${max}\\`` for steps. Defaults to \"%NN\" (ring) or the value (gauge). */\n  valueFormat?: (value: number, max: number) => string\n  /** Gauge only: score bands, lowest first. The highest band reached sets the color and the label. */\n  bands?: RadialMeterBand[]\n  className?: string\n}\n\n/** Compact ring with the value in the middle (a single job), or a half-circle gauge for a score. */\nfunction RadialMeter({ variant = \"ring\", ...props }: RadialMeterProps) {\n  return variant === \"gauge\" ? <Gauge {...props} /> : <Ring {...props} />\n}\n\nfunction Ring({ value, max = 100, label, status = \"neutral\", size = 96, strokeWidth = 8, valueFormat, className }: Omit<RadialMeterProps, \"variant\" | \"bands\">) {\n  const reduce = useReducedMotion()\n  const ratio = max > 0 ? Math.min(Math.max(value / max, 0), 1) : 0\n  const colors = statusColors[resolveStatus(status, ratio)]\n  const r = (size - strokeWidth) / 2\n  const circumference = 2 * Math.PI * r\n  const percent = Math.round(ratio * 100)\n  const text = valueFormat?.(value, max)\n\n  return (\n    <div data-slot=\"radial-meter\" className={cn(\"inline-flex flex-col items-center gap-2\", className)}>\n      <div\n        role=\"meter\"\n        aria-valuemin={0}\n        aria-valuemax={max}\n        aria-valuenow={value}\n        aria-valuetext={text ?? `%${percent}`}\n        aria-label={typeof label === \"string\" ? label : undefined}\n        className=\"relative\"\n        style={{ width: size, height: size }}\n      >\n        <svg width={size} height={size} className=\"-rotate-90\">\n          <circle cx={size / 2} cy={size / 2} r={r} fill=\"none\" stroke={colors.track} strokeWidth={strokeWidth} />\n          <motion.circle\n            cx={size / 2}\n            cy={size / 2}\n            r={r}\n            fill=\"none\"\n            stroke={colors.fill}\n            strokeWidth={strokeWidth}\n            strokeLinecap=\"round\"\n            strokeDasharray={circumference}\n            initial={reduce ? false : { strokeDashoffset: circumference }}\n            animate={{ strokeDashoffset: circumference * (1 - ratio), opacity: ratio > 0 ? 1 : 0 }}\n            transition={springOrInstant(reduce)}\n          />\n        </svg>\n        <span className=\"absolute inset-0 flex items-center justify-center text-base font-semibold tracking-[-0.02em] tabular-nums\">\n          {text !== undefined ? <SwapText direction={1}>{text}</SwapText> : <AnimatedNumber value={percent} format={(n) => `%${n}`} />}\n        </span>\n      </div>\n      {label && <span className=\"text-center text-[13px] text-muted-foreground\">{label}</span>}\n    </div>\n  )\n}\n\nconst bandOf = (bands: RadialMeterBand[] | undefined, value: number) => bands?.reduce<RadialMeterBand | undefined>((hit, band) => (value >= band.from ? band : hit), undefined)\n\n/** Half circle, opening downward. The arc is re-pathed every frame, so its round end stays exact at every value. */\nfunction Gauge({ value, max = 100, label, status = \"neutral\", size = 220, strokeWidth = 12, valueFormat, bands, className }: Omit<RadialMeterProps, \"variant\">) {\n  const reduce = useReducedMotion()\n  const ratio = max > 0 ? Math.min(Math.max(value / max, 0), 1) : 0\n  const band = bandOf(bands, value)\n  const colors = statusColors[band?.status ?? resolveStatus(status, ratio)]\n  const sweep = useMotionValue(reduce ? ratio : 0)\n  const [previous, setPrevious] = React.useState(value)\n  const [direction, setDirection] = React.useState<1 | -1>(1)\n  if (value !== previous) {\n    setDirection(value > previous ? 1 : -1)\n    setPrevious(value)\n  }\n\n  React.useEffect(() => {\n    if (reduce) return sweep.jump(ratio)\n    const controls = animate(sweep, ratio, SPRING)\n    return () => controls.stop()\n  }, [ratio, reduce, sweep])\n\n  const R = 80\n  const path = useTransform(sweep, (t) => {\n    const a = Math.PI * (1 - Math.max(t, 0.0001))\n    return `M20 100 A${R} ${R} 0 0 1 ${(100 + R * Math.cos(a)).toFixed(2)} ${(100 - R * Math.sin(a)).toFixed(2)}`\n  })\n  // At 0 the round cap would show as a dot, so the arc fades in over the first 2%.\n  const opacity = useTransform(sweep, (t) => Math.min(1, t / 0.02))\n  const shown = valueFormat ? valueFormat(value, max) : undefined\n\n  return (\n    <figure\n      data-slot=\"radial-meter\"\n      data-variant=\"gauge\"\n      className={cn(\"m-0 inline-flex flex-col items-center gap-1\", className)}\n      style={{ width: size, \"--gauge-fill\": colors.fill, \"--gauge-track\": colors.track } as React.CSSProperties}\n    >\n      <div\n        role=\"meter\"\n        aria-valuemin={0}\n        aria-valuemax={max}\n        aria-valuenow={value}\n        aria-valuetext={[shown ?? String(value), band?.label].filter(Boolean).join(\", \")}\n        aria-label={typeof label === \"string\" ? label : undefined}\n        className=\"relative w-full\"\n      >\n        <svg viewBox=\"0 -6 200 112\" className=\"block w-full overflow-visible\" aria-hidden>\n          <path d=\"M20 100 A80 80 0 0 1 180 100\" fill=\"none\" strokeWidth={strokeWidth} strokeLinecap=\"round\" className=\"stroke-(--gauge-track) transition-[stroke] duration-200 ease-out\" />\n          <motion.path d={path} fill=\"none\" strokeWidth={strokeWidth} strokeLinecap=\"round\" style={{ opacity }} className=\"stroke-(--gauge-fill) transition-[stroke] duration-200 ease-out\" />\n        </svg>\n        <div aria-hidden className=\"absolute inset-x-0 bottom-0.5 flex flex-col items-center gap-0.5\">\n          <span className=\"text-[30px] leading-none font-semibold tracking-[-0.02em] tabular-nums\">\n            {shown !== undefined ? <SwapText direction={direction}>{shown}</SwapText> : <AnimatedNumber value={value} />}\n          </span>\n          {band && (\n            <SwapText direction={direction} className={cn(\"text-[12.5px] font-medium\", band.status !== \"neutral\" && \"text-(--gauge-fill)\")}>\n              {band.label}\n            </SwapText>\n          )}\n        </div>\n      </div>\n      {label && <figcaption className=\"text-center text-[13px] text-muted-foreground\">{label}</figcaption>}\n    </figure>\n  )\n}\n\nexport { Meter, RadialMeter, type MeterProps, type MeterStatus, type RadialMeterBand, type RadialMeterProps }\n",
      "type": "registry:component",
      "target": "@components/ikas/meter.tsx"
    }
  ],
  "docs": "https://builders.ikas.com/tr/docs/app-development/ui-kit/components/meter",
  "categories": [
    "data-display",
    "feedback"
  ],
  "type": "registry:component"
}