{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pie-chart",
  "type": "registry:ui",
  "title": "Pie chart",
  "description": "An SVG pie chart with leader line labels, a legend and slices that pull out on hover or focus.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/pie-chart.tsx",
      "type": "registry:ui",
      "target": "components/ui/pie-chart.tsx",
      "content": "\"use client\"\nimport * as React from \"react\"\nimport { motion, useReducedMotion } from \"motion/react\"\nimport { cn } from \"@/lib/utils\"\n\n/** An SVG pie chart with leader line labels, a legend and slices that pull out on hover or focus. */\ntype PieDatum = {\n  label: string\n  value: number\n  /** Any CSS colour. Defaults to the chart tokens in order. */\n  color?: string\n}\n\ntype PieChartProps = {\n  data?: PieDatum[]\n  /** Heading shown above the chart and used as the accessible name. */\n  title?: string\n  /** Caption under the total when no slice is active. */\n  caption?: string\n  /** Formats values in the readout, legend and table. */\n  format?: (value: number) => string\n  /** Labels outside the slices with leader lines. Hidden automatically when the chart is narrow. */\n  showLabels?: boolean\n  showLegend?: boolean\n  /** Distance a hovered or focused slice pulls out, in chart units. */\n  pullOut?: number\n  className?: string\n}\n\nconst SAMPLE: PieDatum[] = [\n  { label: \"Enterprise\", value: 48200 },\n  { label: \"Business\", value: 31600 },\n  { label: \"Pro\", value: 22400 },\n  { label: \"Starter\", value: 9800 },\n  { label: \"Add-ons\", value: 4100 },\n]\n\nconst COLORS = [\"var(--chart-1)\", \"var(--chart-2)\", \"var(--chart-3)\", \"var(--chart-4)\", \"var(--chart-5)\"]\nconst SNAPPY = { type: \"spring\", bounce: 0.18, duration: 0.42 } as const\nconst usd = new Intl.NumberFormat(\"en-US\", { style: \"currency\", currency: \"USD\", notation: \"compact\", maximumFractionDigits: 1 })\nconst defaultFormat = (n: number) => usd.format(n)\n\nfunction arcPath(cx: number, cy: number, r: number, a0: number, a1: number) {\n  if (a1 - a0 >= Math.PI * 2 - 1e-6) {\n    return `M${cx} ${cy - r}A${r} ${r} 0 1 1 ${cx} ${cy + r}A${r} ${r} 0 1 1 ${cx} ${cy - r}Z`\n  }\n  const x0 = cx + r * Math.cos(a0)\n  const y0 = cy + r * Math.sin(a0)\n  const x1 = cx + r * Math.cos(a1)\n  const y1 = cy + r * Math.sin(a1)\n  const large = a1 - a0 > Math.PI ? 1 : 0\n  return `M${cx} ${cy}L${x0} ${y0}A${r} ${r} 0 ${large} 1 ${x1} ${y1}Z`\n}\n\n/** Push label y positions apart on one side so they never overlap. */\nfunction spread<T extends { y: number }>(items: T[], gap: number, min: number, max: number) {\n  const s = [...items].sort((a, b) => a.y - b.y)\n  for (let i = 1; i < s.length; i++) if (s[i].y - s[i - 1].y < gap) s[i].y = s[i - 1].y + gap\n  if (s.length && s[s.length - 1].y > max) {\n    s[s.length - 1].y = max\n    for (let i = s.length - 2; i >= 0; i--) if (s[i + 1].y - s[i].y < gap) s[i].y = s[i + 1].y - gap\n  }\n  for (const it of s) it.y = Math.max(min, it.y)\n  return s\n}\n\n/**\n * An SVG pie chart with outside labels on leader lines, a legend and a\n * readout. Hover or focus a slice and it pulls out on a spring while the\n * readout shows its value and share. Slices are keyboard focusable with\n * arrow keys, and a visually hidden table carries the data for screen readers.\n */\nfunction PieChart({\n  data = SAMPLE,\n  title = \"Revenue by plan\",\n  caption = \"Monthly recurring revenue\",\n  format = defaultFormat,\n  showLabels = true,\n  showLegend = true,\n  pullOut = 6,\n  className,\n}: PieChartProps) {\n  const reduce = useReducedMotion()\n  const [active, setActive] = React.useState<number | null>(null)\n  const [focusIdx, setFocusIdx] = React.useState(0)\n  const [narrow, setNarrow] = React.useState(false)\n  const boxRef = React.useRef<HTMLDivElement>(null)\n  const refs = React.useRef<(SVGPathElement | null)[]>([])\n  const titleId = React.useId()\n\n  React.useEffect(() => {\n    const el = boxRef.current\n    if (!el || typeof ResizeObserver === \"undefined\") return\n    const ro = new ResizeObserver(([e]) => setNarrow(e.contentRect.width < 320))\n    ro.observe(el)\n    return () => ro.disconnect()\n  }, [])\n\n  const labels = showLabels && !narrow\n  const W = labels ? 360 : 240\n  const H = 240\n  const cx = W / 2\n  const cy = H / 2\n  const r = labels ? 80 : 104\n\n  const total = data.reduce((s, d) => s + Math.max(0, d.value), 0)\n  const slices = React.useMemo(() => {\n    let a = -Math.PI / 2\n    return data.map((d, i) => {\n      const share = total ? Math.max(0, d.value) / total : 0\n      const a0 = a\n      const a1 = a + share * Math.PI * 2\n      a = a1\n      const mid = (a0 + a1) / 2\n      return { ...d, i, share, a0, a1, mid, color: d.color ?? COLORS[i % COLORS.length] }\n    })\n  }, [data, total])\n\n  const placed = React.useMemo(() => {\n    if (!labels) return []\n    const items = slices\n      .filter((s) => s.share > 0)\n      .map((s) => {\n        const right = Math.cos(s.mid) >= 0\n        return { s, right, y: cy + Math.sin(s.mid) * (r + 16) }\n      })\n    const gap = 14\n    return [\n      ...spread(items.filter((x) => x.right), gap, 10, H - 10),\n      ...spread(items.filter((x) => !x.right), gap, 10, H - 10),\n    ]\n  }, [labels, slices, cy, r])\n\n  const pct = (share: number) => `${(share * 100).toFixed(share < 0.1 ? 1 : 0)}%`\n  const cur = active != null ? slices[active] : null\n\n  const move = (to: number) => {\n    const n = (to + slices.length) % slices.length\n    setFocusIdx(n)\n    refs.current[n]?.focus()\n  }\n\n  return (\n    <figure\n      data-slot=\"pie-chart\"\n      aria-labelledby={titleId}\n      className={cn(\"w-full max-w-md rounded-xl border border-border bg-surface p-5 shadow-raised\", className)}\n    >\n      <figcaption>\n        <p id={titleId} className=\"text-[0.8125rem] font-medium text-fg\">\n          {title}\n        </p>\n        <div aria-hidden className=\"mt-1 flex h-14 flex-col justify-center\">\n          <p className=\"text-2xl font-medium tracking-[-0.025em] text-fg tabular-nums\">{format(cur ? cur.value : total)}</p>\n          <p className=\"mt-0.5 flex items-center gap-1.5 text-xs text-fg-muted\">\n            {cur ? (\n              <>\n                <span className=\"size-2 rounded-full\" style={{ background: cur.color }} />\n                <span className=\"text-fg\">{cur.label}</span>\n                <span className=\"tabular-nums\">· {(cur.share * 100).toFixed(1)}% of total</span>\n              </>\n            ) : (\n              caption\n            )}\n          </p>\n        </div>\n      </figcaption>\n\n      <div ref={boxRef} className=\"mt-3\">\n        <svg\n          viewBox={`0 0 ${W} ${H}`}\n          role=\"group\"\n          aria-label={`${title}, ${data.length} slices`}\n          className=\"mx-auto block h-auto w-full overflow-visible\"\n          style={{ maxWidth: labels ? 420 : 260 }}\n          onPointerLeave={() => setActive(null)}\n        >\n          {slices.map((s) =>\n            s.share > 0 ? (\n              <motion.path\n                key={s.label}\n                ref={(el) => {\n                  refs.current[s.i] = el\n                }}\n                d={arcPath(cx, cy, r, s.a0, s.a1)}\n                fill={s.color}\n                role=\"img\"\n                aria-label={`${s.label}: ${format(s.value)}, ${(s.share * 100).toFixed(1)}%`}\n                tabIndex={s.i === focusIdx ? 0 : -1}\n                initial={false}\n                animate={{\n                  x: active === s.i ? Math.cos(s.mid) * pullOut : 0,\n                  y: active === s.i ? Math.sin(s.mid) * pullOut : 0,\n                  opacity: active == null || active === s.i ? 1 : 0.5,\n                }}\n                transition={reduce ? { duration: 0 } : SNAPPY}\n                onPointerEnter={() => setActive(s.i)}\n                onFocus={() => {\n                  setFocusIdx(s.i)\n                  setActive(s.i)\n                }}\n                onBlur={() => setActive(null)}\n                onKeyDown={(e) => {\n                  if (e.key === \"ArrowRight\" || e.key === \"ArrowDown\") { e.preventDefault(); move(s.i + 1) }\n                  if (e.key === \"ArrowLeft\" || e.key === \"ArrowUp\") { e.preventDefault(); move(s.i - 1) }\n                  if (e.key === \"Home\") { e.preventDefault(); move(0) }\n                  if (e.key === \"End\") { e.preventDefault(); move(slices.length - 1) }\n                }}\n                strokeWidth={2}\n                strokeLinejoin=\"round\"\n                className=\"cursor-pointer stroke-surface outline-none focus-visible:stroke-fg\"\n              />\n            ) : null\n          )}\n          {placed.map(({ s, right, y }) => {\n            const p0x = cx + Math.cos(s.mid) * (r + 2)\n            const p0y = cy + Math.sin(s.mid) * (r + 2)\n            const ex = cx + Math.cos(s.mid) * (r + 12)\n            const tx = cx + (right ? 1 : -1) * (r + 24)\n            const on = active === s.i\n            return (\n              <g key={s.label} aria-hidden className={cn(\"transition-opacity duration-[140ms]\", active != null && !on && \"opacity-50\")}>\n                <polyline\n                  points={`${p0x},${p0y} ${ex},${y} ${tx - (right ? 4 : -4)},${y}`}\n                  fill=\"none\"\n                  strokeWidth={1}\n                  className={on ? \"stroke-fg-muted\" : \"stroke-fg-subtle opacity-50\"}\n                />\n                <text x={tx} y={y} dominantBaseline=\"middle\" textAnchor={right ? \"start\" : \"end\"} style={{ fontSize: 11 }}>\n                  <tspan className=\"fill-fg-muted\">{s.label}</tspan>\n                  <tspan dx={4} className=\"fill-fg font-medium tabular-nums\">\n                    {pct(s.share)}\n                  </tspan>\n                </text>\n              </g>\n            )\n          })}\n        </svg>\n      </div>\n\n      {showLegend ? (\n        <ul aria-hidden className={cn(\"mt-4 grid gap-x-6 gap-y-1 border-t border-border pt-3\", narrow ? \"grid-cols-1\" : \"grid-cols-2\")}>\n          {slices.map((s) => (\n            <li\n              key={s.label}\n              onPointerEnter={() => setActive(s.i)}\n              onPointerLeave={() => setActive(null)}\n              className={cn(\n                \"flex h-7 items-center gap-2 rounded-md px-1.5 text-xs transition-opacity duration-[140ms]\",\n                active != null && active !== s.i && \"opacity-50\"\n              )}\n            >\n              <span className=\"size-2 shrink-0 rounded-full\" style={{ background: s.color }} />\n              <span className=\"min-w-0 flex-1 truncate text-fg-muted\">{s.label}</span>\n              <span className=\"font-medium text-fg tabular-nums\">{format(s.value)}</span>\n            </li>\n          ))}\n        </ul>\n      ) : null}\n\n      <table className=\"sr-only\">\n        <caption>{title}</caption>\n        <thead>\n          <tr>\n            <th scope=\"col\">Segment</th>\n            <th scope=\"col\">Value</th>\n            <th scope=\"col\">Share</th>\n          </tr>\n        </thead>\n        <tbody>\n          {slices.map((s) => (\n            <tr key={s.label}>\n              <th scope=\"row\">{s.label}</th>\n              <td>{format(s.value)}</td>\n              <td>{(s.share * 100).toFixed(1)}%</td>\n            </tr>\n          ))}\n        </tbody>\n        <tfoot>\n          <tr>\n            <th scope=\"row\">Total</th>\n            <td>{format(total)}</td>\n            <td>100%</td>\n          </tr>\n        </tfoot>\n      </table>\n    </figure>\n  )\n}\n\nexport { PieChart }\nexport type { PieChartProps, PieDatum }\n"
    }
  ]
}
