{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "radial-chart",
  "type": "registry:ui",
  "title": "Radial chart",
  "description": "A radial bar chart of concentric progress rings with a legend, a rolling centre total and spring animated values.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://ui.minidev.pro/r/segmented-control.json",
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/radial-chart.tsx",
      "type": "registry:ui",
      "target": "components/ui/radial-chart.tsx",
      "content": "\"use client\"\nimport * as React from \"react\"\nimport { animate, motion, useMotionValue, useReducedMotion, useTransform } from \"motion/react\"\nimport { cn } from \"@/lib/utils\"\nimport { SegmentedControl } from \"@/components/ui/segmented-control\"\n\n/** A radial bar chart of concentric progress rings with a legend, a rolling centre total and spring animated values. */\ntype RadialDatum = {\n  label: string\n  value: number\n  /** The goal or ceiling. Defaults to 100. */\n  max?: number\n  /** Any CSS colour. Defaults to the chart tokens in order. */\n  color?: string\n  /** Formats value and max in the legend, e.g. currency. */\n  format?: (n: number) => string\n}\n\ntype RadialPeriod = { label: string; data: RadialDatum[] }\n\ntype RadialChartProps = {\n  /** Rings, outermost first. One item renders a single radial progress ring. */\n  data?: RadialDatum[]\n  /** Datasets to switch between with a segmented control. Ignored when `data` is set. */\n  periods?: RadialPeriod[]\n  title?: string\n  /** Label under the centre total. */\n  centerLabel?: string\n  /** Override the centre value. Defaults to the average progress of all rings. */\n  centerValue?: React.ReactNode\n  /** Ring thickness in chart units (the chart is 240 wide). */\n  thickness?: number\n  /** Gap between rings in chart units. */\n  gap?: number\n  showLegend?: boolean\n  className?: string\n}\n\nconst usd = new Intl.NumberFormat(\"en-US\", { style: \"currency\", currency: \"USD\", notation: \"compact\", maximumFractionDigits: 1 })\nconst money = (n: number) => usd.format(n)\nconst percent = (n: number) => `${n}%`\n\nconst PERIODS: RadialPeriod[] = [\n  {\n    label: \"October\",\n    data: [\n      { label: \"MRR target\", value: 84200, max: 100000, format: money },\n      { label: \"Activation\", value: 62, max: 70, format: percent },\n      { label: \"NPS\", value: 47, max: 60 },\n    ],\n  },\n  {\n    label: \"Q3\",\n    data: [\n      { label: \"MRR target\", value: 71500, max: 90000, format: money },\n      { label: \"Activation\", value: 49, max: 70, format: percent },\n      { label: \"NPS\", value: 41, max: 60 },\n    ],\n  },\n]\n\nconst COLORS = [\"var(--chart-1)\", \"var(--chart-2)\", \"var(--chart-4)\", \"var(--chart-3)\", \"var(--chart-5)\"]\nconst DATA_SPRING = { type: \"spring\", bounce: 0.12, duration: 0.7 } as const\nconst ROLL = { type: \"spring\", stiffness: 260, damping: 28, mass: 0.9 } as const\n\nconst SIZE = 240\nconst C = SIZE / 2\n\n/** Arc from 12 o'clock, clockwise, through `sweep` radians. */\nfunction arc(r: number, sweep: number) {\n  if (sweep >= Math.PI * 2 - 1e-6) return `M${C} ${C - r}A${r} ${r} 0 1 1 ${C} ${C + r}A${r} ${r} 0 1 1 ${C} ${C - r}`\n  const a = -Math.PI / 2 + sweep\n  return `M${C} ${C - r}A${r} ${r} 0 ${sweep > Math.PI ? 1 : 0} 1 ${C + r * Math.cos(a)} ${C + r * Math.sin(a)}`\n}\n\nfunction Rolling({ value, suffix = \"\" }: { value: number; suffix?: string }) {\n  const reduce = useReducedMotion()\n  const mv = useMotionValue(value)\n  const text = useTransform(mv, (v) => `${Math.round(v)}${suffix}`)\n  React.useEffect(() => {\n    if (reduce) {\n      mv.set(value)\n      return\n    }\n    const c = animate(mv, value, ROLL)\n    return () => c.stop()\n  }, [value, reduce, mv])\n  return <motion.span>{text}</motion.span>\n}\n\n/**\n * A radial bar chart of concentric progress rings, each labelled at its start,\n * with a legend and a centre total. Rings sweep to new values on the data\n * spring when the data changes and the total rolls. Pass a single item to get\n * a full radial progress ring.\n */\nfunction RadialChart({\n  data: dataProp,\n  periods = PERIODS,\n  title = \"Goals progress\",\n  centerLabel,\n  centerValue,\n  thickness,\n  gap = 6,\n  showLegend = true,\n  className,\n}: RadialChartProps) {\n  const reduce = useReducedMotion()\n  const [period, setPeriod] = React.useState(periods[0]?.label ?? \"\")\n  const [hot, setHot] = React.useState<number | null>(null)\n  const data = dataProp ?? periods.find((p) => p.label === period)?.data ?? periods[0]?.data ?? []\n  const single = data.length === 1\n  const sweep = single ? Math.PI * 2 : Math.PI * 1.5\n  const t = thickness ?? (single ? 18 : data.length > 4 ? 10 : 16)\n  const rings = data.map((d, i) => {\n    const max = d.max ?? 100\n    const p = max > 0 ? Math.max(0, Math.min(1, d.value / max)) : 0\n    const r = C - 4 - t / 2 - i * (t + gap)\n    const f = d.format ?? ((n: number) => n.toLocaleString(\"en-US\"))\n    return { ...d, i, max, p, r, color: d.color ?? COLORS[i % COLORS.length], text: `${f(d.value)} of ${f(d.max ?? 100)}` }\n  })\n  const avg = rings.length ? Math.round((rings.reduce((s, r) => s + r.p, 0) / rings.length) * 100) : 0\n  const centre = single ? Math.round(rings[0].p * 100) : avg\n  const sub = centerLabel ?? (single ? rings[0]?.label : \"of goals\")\n  const summary = rings.map((r) => `${r.label} ${Math.round(r.p * 100)}%, ${r.text}`).join(\"; \")\n\n  return (\n    <div\n      data-slot=\"radial-chart\"\n      className={cn(\"w-full rounded-xl border border-border bg-surface p-5 shadow-raised\", single ? \"max-w-xs\" : \"max-w-sm\", className)}\n    >\n      <div className=\"flex min-h-8 items-center justify-between gap-3\">\n        <p className=\"text-[0.8125rem] font-medium text-fg\">{title}</p>\n        {!dataProp && periods.length > 1 ? (\n          <SegmentedControl size=\"sm\" aria-label=\"Period\" value={period} onChange={setPeriod} items={periods.map((p) => p.label)} />\n        ) : null}\n      </div>\n\n      <div className=\"relative mx-auto mt-4 w-full max-w-60\">\n        <svg viewBox={`0 0 ${SIZE} ${SIZE}`} role=\"img\" aria-label={`${title}: ${summary}`} className=\"block h-auto w-full\">\n          {rings.map((r) => {\n            const dim = hot != null && hot !== r.i\n            return (\n              <g key={r.label} className={cn(\"transition-opacity duration-[140ms]\", dim && \"opacity-35\")}>\n                <path\n                  d={arc(r.r, sweep)}\n                  fill=\"none\"\n                  strokeWidth={t}\n                  strokeLinecap=\"round\"\n                  style={{ stroke: `color-mix(in oklch, ${r.color} 16%, transparent)` }}\n                />\n                <motion.path\n                  d={arc(r.r, sweep)}\n                  fill=\"none\"\n                  strokeWidth={t}\n                  strokeLinecap=\"round\"\n                  style={{ stroke: r.color }}\n                  initial={false}\n                  animate={{ pathLength: r.p, opacity: r.p > 0 ? 1 : 0 }}\n                  transition={reduce ? { duration: 0 } : DATA_SPRING}\n                />\n                {!single ? (\n                  <text\n                    x={C - t / 2 - 4}\n                    y={C - r.r}\n                    textAnchor=\"end\"\n                    dominantBaseline=\"central\"\n                    className=\"fill-fg-muted\"\n                    style={{ fontSize: Math.min(11, t * 0.7) }}\n                  >\n                    {r.label}\n                  </text>\n                ) : null}\n              </g>\n            )\n          })}\n        </svg>\n        <div\n          aria-hidden\n          className={cn(\"pointer-events-none absolute inset-0 flex flex-col items-center justify-center\", !single && \"pt-1\")}\n        >\n          <span className={cn(\"font-medium tracking-[-0.03em] text-fg tabular-nums\", single ? \"text-4xl\" : \"text-3xl\")}>\n            {centerValue ?? <Rolling value={centre} suffix=\"%\" />}\n          </span>\n          <span className=\"mt-0.5 max-w-24 truncate text-xs text-fg-muted\">{sub}</span>\n          {single ? <span className=\"mt-1 text-[11px] text-fg-subtle tabular-nums\">{rings[0].text}</span> : null}\n        </div>\n      </div>\n\n      {showLegend && !single ? (\n        <ul className=\"mt-4 space-y-0.5 border-t border-border pt-3\">\n          {rings.map((r) => (\n            <li\n              key={r.label}\n              onPointerEnter={() => setHot(r.i)}\n              onPointerLeave={() => setHot(null)}\n              className={cn(\"flex h-7 items-center gap-2 text-xs transition-opacity duration-[140ms]\", hot != null && hot !== r.i && \"opacity-50\")}\n            >\n              <span aria-hidden className=\"size-2 shrink-0 rounded-full\" style={{ background: r.color }} />\n              <span className=\"min-w-0 flex-1 truncate text-fg-muted\">{r.label}</span>\n              <span className=\"text-fg-subtle tabular-nums\">{r.text}</span>\n              <span className=\"w-10 text-right font-medium text-fg tabular-nums\">{Math.round(r.p * 100)}%</span>\n            </li>\n          ))}\n        </ul>\n      ) : null}\n    </div>\n  )\n}\n\nexport { RadialChart }\nexport type { RadialChartProps, RadialDatum, RadialPeriod }\n"
    }
  ]
}
