{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "circular-progress",
  "title": "Circular Progress",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "Concentric progress rings or a 270 degree gauge in theme colors with animated fills, a legend with values and a center slot, for goals, quotas and activity summaries.",
  "dependencies": [
    "motion@^12"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/circular-progress.tsx",
      "content": "// Ballmac UI: Circular Progress. https://ui.ballmac.com/components/circular-progress\n\"use client\"\n\nimport * as React from \"react\"\nimport { motion, useReducedMotion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\ntype RingTone = \"primary\" | \"chart-1\" | \"chart-2\" | \"chart-3\" | \"chart-4\" | \"chart-5\" | \"destructive\"\n\nconst STROKE: Record<RingTone, string> = {\n  primary: \"stroke-primary\",\n  \"chart-1\": \"stroke-chart-1\",\n  \"chart-2\": \"stroke-chart-2\",\n  \"chart-3\": \"stroke-chart-3\",\n  \"chart-4\": \"stroke-chart-4\",\n  \"chart-5\": \"stroke-chart-5\",\n  destructive: \"stroke-destructive\",\n}\nconst DOT: Record<RingTone, string> = {\n  primary: \"bg-primary\",\n  \"chart-1\": \"bg-chart-1\",\n  \"chart-2\": \"bg-chart-2\",\n  \"chart-3\": \"bg-chart-3\",\n  \"chart-4\": \"bg-chart-4\",\n  \"chart-5\": \"bg-chart-5\",\n  destructive: \"bg-destructive\",\n}\nconst AUTO: RingTone[] = [\"chart-1\", \"chart-2\", \"chart-3\", \"chart-4\", \"chart-5\"]\n\ntype ProgressRing = {\n  /** What this ring measures, such as \"Move\". */\n  label: string\n  /** Current value. */\n  value: number\n  /** Value that fills the ring. */\n  max?: number\n  /** Ring color. Defaults to the next chart color. */\n  tone?: RingTone\n  /** Text shown for the value in the legend, such as \"420 kcal\". Defaults to the percentage. */\n  display?: string\n}\n\ntype CircularProgressProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  /** One ring per item, outermost first. */\n  rings: ProgressRing[]\n  /** Diameter in pixels. */\n  size?: number\n  /** Ring thickness in pixels. */\n  thickness?: number\n  /** Space between rings in pixels. */\n  gap?: number\n  /** Degrees of the circle that the rings cover. 360 is a full circle; 270 makes a gauge with the opening at the bottom. */\n  sweep?: number\n  /** Show a legend with each ring's name and value. */\n  legend?: boolean\n  /** Seconds the rings take to fill. */\n  duration?: number\n  /** Content in the middle. Defaults to the first ring's percentage. */\n  children?: React.ReactNode\n}\n\nfunction CircularProgress({\n  rings,\n  size = 176,\n  thickness = 14,\n  gap = 5,\n  sweep = 360,\n  legend = true,\n  duration = 1.1,\n  className,\n  children,\n  ...props\n}: CircularProgressProps) {\n  const reduce = useReducedMotion()\n  const arc = Math.min(Math.max(sweep, 30), 360) / 360\n  // A gauge opens at the bottom, so it starts at the lower left.\n  const rotation = sweep >= 360 ? -90 : 90 + (360 - sweep) / 2\n  const center = size / 2\n  const data = rings.map((r, i) => {\n    const max = r.max ?? 100\n    const fraction = max > 0 ? Math.min(Math.max(r.value / max, 0), 1) : 0\n    return { ...r, tone: r.tone ?? AUTO[i % AUTO.length]!, fraction, percent: Math.round(fraction * 100), radius: center - thickness / 2 - i * (thickness + gap) - 1, circumference: 0 }\n  })\n  data.forEach((r) => {\n    r.circumference = 2 * Math.PI * r.radius\n  })\n  const first = data[0]\n  const summary = data.map((r) => `${r.label} ${r.display ?? `${r.percent}%`}`).join(\", \")\n\n  return (\n    <div\n      data-slot=\"circular-progress\"\n      className={cn(\"inline-flex flex-col items-center gap-4\", className)}\n      {...props}\n    >\n      <div\n        role={data.length === 1 ? \"meter\" : \"img\"}\n        aria-label={data.length === 1 ? first?.label : `Progress rings: ${summary}`}\n        aria-valuemin={data.length === 1 ? 0 : undefined}\n        aria-valuemax={data.length === 1 ? (rings[0]?.max ?? 100) : undefined}\n        aria-valuenow={data.length === 1 ? first?.value : undefined}\n        aria-valuetext={data.length === 1 ? summary : undefined}\n        className=\"relative\"\n        style={{ width: size, height: size }}\n      >\n        <svg aria-hidden=\"true\" viewBox={`0 0 ${size} ${size}`} width={size} height={size} style={{ transform: `rotate(${rotation}deg)` }}>\n          {data.map((r, i) => (\n            <g key={i}>\n              <circle cx={center} cy={center} r={r.radius} fill=\"none\" strokeWidth={thickness} strokeLinecap=\"round\" strokeDasharray={`${arc * r.circumference} ${r.circumference}`} className=\"stroke-muted\" />\n              <motion.circle\n                cx={center}\n                cy={center}\n                r={r.radius}\n                fill=\"none\"\n                strokeWidth={thickness}\n                strokeLinecap=\"round\"\n                className={STROKE[r.tone]}\n                opacity={r.fraction === 0 ? 0 : 1}\n                initial={reduce ? false : { strokeDasharray: `0 ${r.circumference}` }}\n                animate={{ strokeDasharray: `${r.fraction * arc * r.circumference} ${r.circumference}` }}\n                transition={{ duration: reduce ? 0 : duration, delay: reduce ? 0 : i * 0.12, ease: [0.22, 1, 0.36, 1] }}\n              />\n            </g>\n          ))}\n        </svg>\n        <div className=\"absolute inset-0 flex flex-col items-center justify-center text-center\" style={{ padding: (data.length * (thickness + gap)) + 4 }}>\n          {children ?? (\n            <>\n              <span className=\"text-2xl font-semibold tracking-tight text-foreground tabular-nums\">{first?.display ?? `${first?.percent ?? 0}%`}</span>\n              {first && <span className=\"text-xs text-muted-foreground\">{first.label}</span>}\n            </>\n          )}\n        </div>\n      </div>\n      {legend && data.length > 0 && (\n        <ul className=\"grid gap-1.5\">\n          {data.map((r, i) => (\n            <li key={i} className=\"flex items-center gap-2 text-[13px]\">\n              <span aria-hidden=\"true\" className={cn(\"size-2.5 rounded-full\", DOT[r.tone])} />\n              <span className=\"text-foreground\">{r.label}</span>\n              <span className=\"ms-auto ps-4 font-mono text-xs text-muted-foreground tabular-nums\">{r.display ?? `${r.percent}%`}</span>\n            </li>\n          ))}\n        </ul>\n      )}\n    </div>\n  )\n}\n\nexport { CircularProgress, type CircularProgressProps, type ProgressRing, type RingTone }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/circular-progress.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "progress",
      "rings",
      "gauge",
      "activity",
      "chart"
    ],
    "version": "1.0.0",
    "updated": "2026-10-01",
    "ai": {
      "summary": "<CircularProgress rings={[{ label, value, max, tone, display }]} size thickness gap sweep legend>center</CircularProgress>. One ring is role=meter; several are a named group whose value text lists every ring. sweep=270 makes a gauge.",
      "whenToUse": [
        "Daily goals and multi-metric summaries",
        "Storage, quota or score gauges"
      ],
      "whenNotToUse": [
        "A single small ring in a table (progress-ring)",
        "Time series (chart)"
      ],
      "composesWith": [
        "progress-ring",
        "usage-meter",
        "stat-card"
      ],
      "a11y": [
        {
          "keys": "Screen readers",
          "action": "Value text such as Move 74%, Exercise 38%; the legend repeats values as text"
        },
        {
          "keys": "Color",
          "action": "Each ring is named in the legend; color is not the only cue"
        },
        {
          "keys": "Reduced motion",
          "action": "Rings are drawn full length at once"
        }
      ],
      "customization": [
        "rings",
        "sweep",
        "thickness and gap",
        "legend",
        "duration"
      ]
    },
    "examples": [
      "circular-progress-demo",
      "circular-progress-gauge"
    ],
    "url": "https://ui.ballmac.com/components/circular-progress"
  },
  "categories": [
    "data-display"
  ],
  "type": "registry:ui"
}