{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pie-chart",
  "title": "Pie chart",
  "description": "Shares of a whole: one hairline ring cut into arcs with paper between them, numbered round the rim, and the total as one very large number inside that rolls to the share of the arc you point at.",
  "registryDependencies": [
    "https://thedirectors.agency/ui/r/0nlytype.json",
    "https://thedirectors.agency/ui/r/radial-chart.json"
  ],
  "files": [
    {
      "path": "registry/0nlytype/ui/pie-chart.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { roll } from \"@/lib/0nlytype/roll\"\nimport { rove } from \"@/lib/0nlytype/rove\"\nimport { Ring } from \"@/components/ui/radial-chart\"\n\ntype PieChartProps = Omit<React.ComponentProps<\"figure\">, \"children\"> & {\n  /** One arc per entry, clockwise from the top in this order. */\n  data: { label: string; value: number }[]\n  /** Names the chart for assistive tech, e.g. \"Where visits came from, September\". */\n  label: string\n  /** What the values count. At rest the number reads \"visits in all\". */\n  unit?: string\n  format?: (value: number) => string\n  /** ring: a whole ring, the total inside. horizon: a half ring standing on a horizon, the total on the line. */\n  variant?: \"ring\" | \"horizon\"\n  /** Pins the pointed-at look on one arc; set on the root for documentation. */\n  \"data-force\"?: string\n}\n\nconst figures = new Intl.NumberFormat(\"en-GB\")\n// The paper left between two arcs, as a share of the ring. ponytail: a fixed share, about 9px on a 22rem ring.\nconst GAP = 0.01\nconst place = (i: number) => String(i + 1).padStart(2, \"0\")\n\n/**\n * Shares of a whole: one ring cut into arcs with a hairline of paper between them, numbered round the rim, and the\n * total as one very large number inside. Point at an arc, or focus its name, and it inks in the accent, the others\n * step back, and the total rolls down to that arc's share; leave and it rolls back up to the total.\n */\nfunction PieChart({ data, label, unit, format = figures.format, variant = \"ring\", className, style, ...props }: PieChartProps) {\n  const force = props[\"data-force\"]?.split(\" \").includes(\"hover\")\n  const [shown, setShown] = React.useState(force ? 0 : -1)\n  const target = React.useRef(shown)\n  const value = React.useRef<HTMLSpanElement>(null)\n  const face = React.useRef<HTMLDivElement>(null)\n  const key = React.useRef<HTMLUListElement>(null)\n\n  const values = data.map((d) => (Number.isFinite(d.value) && d.value > 0 ? d.value : 0))\n  const total = values.reduce((a, b) => a + b, 0)\n  const shares = values.map((v) => (total > 0 ? v / total : 0))\n  const starts = shares.map((_, i) => shares.slice(0, i).reduce((a, b) => a + b, 0))\n  const many = shares.filter(Boolean).length > 1\n\n  function show(i: number) {\n    if (i === target.current || !value.current) return\n    const was = target.current < 0 ? 1 : shares[target.current]\n    const next = i < 0 ? 1 : shares[i]\n    target.current = i\n    roll(value.current, () => setShown(i), \"0.3em\", next > was ? 1 : -1)\n  }\n  const rest = () => !key.current?.contains(document.activeElement) && show(-1)\n\n  // The arc under the pointer, read by its angle; off the ring (the middle included) the number rolls back to the total.\n  function pick(e: React.PointerEvent) {\n    const ring = face.current?.querySelector<HTMLElement>(\".ot-ring\")\n    if (!ring || total <= 0) return\n    const box = ring.getBoundingClientRect()\n    const r = box.width / 2\n    const rtl = getComputedStyle(e.currentTarget).direction === \"rtl\"\n    const dx = (e.clientX - box.left - r) * (rtl ? -1 : 1)\n    const dy = e.clientY - box.top - r\n    if (Math.abs(Math.hypot(dx, dy) - r) > 28) return rest()\n    let t = (Math.atan2(dx, -dy) / (2 * Math.PI) + 1) % 1\n    if (variant === \"horizon\") {\n      t = ((t + 0.25) % 1) / 0.5\n      if (t > 1) return rest()\n    }\n    const i = starts.findIndex((s, k) => shares[k] > 0 && t >= s && t < s + shares[k])\n    if (i >= 0) show(i)\n  }\n\n  const at = shown >= 0 ? data[shown] : undefined\n  const share = shown >= 0 ? shares[shown] * 100 : 0\n  const percent = share > 0 && share < 1 ? \"<1\" : share.toFixed(share < 10 && share % 1 ? 1 : 0)\n  const all = `${format(total)}${unit ? ` ${unit}` : \"\"}`\n\n  return (\n    <figure\n      data-slot=\"pie-chart\"\n      data-variant={variant}\n      data-pointed={shown >= 0 || undefined}\n      aria-label={label}\n      className={cn(\"ot-pie\", className)}\n      style={{ \"--n\": data.length, ...style } as React.CSSProperties}\n      {...props}\n    >\n      <div ref={face} data-slot=\"pie-chart-face\" className=\"ot-pie-face\" aria-hidden=\"true\" onPointerMove={pick} onPointerLeave={rest}>\n        <div className=\"ot-pie-arcs\">\n          {total > 0 ? (\n            data.map((d, i) => {\n              if (!shares[i]) return null\n              // Each arc gives up the gap at both ends, but never more than half of itself.\n              const len = many ? Math.max(shares[i] - GAP, shares[i] / 2) : 1\n              return <Ring key={d.label} value={len} from={starts[i] + (shares[i] - len) / 2} head=\"none\" data-shown={i === shown || undefined} />\n            })\n          ) : (\n            <Ring value={0} head=\"none\" data-empty=\"\" />\n          )}\n        </div>\n        {many\n          ? data.map((d, i) =>\n              shares[i] ? (\n                <span key={d.label} className=\"ot-pie-tick\" data-shown={i === shown || undefined} style={{ \"--t\": starts[i] + shares[i] / 2 } as React.CSSProperties}>\n                  {place(i)}\n                </span>\n              ) : null,\n            )\n          : null}\n      </div>\n      <figcaption data-slot=\"pie-chart-read\" className=\"ot-pie-read\">\n        <span ref={value} className=\"ot-pie-value\">\n          {at ? (\n            <>\n              {percent}\n              <span className=\"ot-pie-unit\">%</span>\n            </>\n          ) : (\n            format(total)\n          )}\n        </span>\n        <span className=\"ot-pie-words\">\n          <span className=\"ot-pie-name\">{at ? at.label : unit ? `${unit} in all` : \"In all\"}</span>\n          {/* At rest the line is held open, so the number doesn't move when an arc is pointed at. */}\n          <span className=\"ot-pie-of\">{at ? `${format(values[shown])} of ${all}` : \"\\u00a0\"}</span>\n        </span>\n      </figcaption>\n      <ul\n        ref={key}\n        role=\"group\"\n        aria-label={label}\n        data-slot=\"pie-chart-key\"\n        className=\"ot-pie-key\"\n        onKeyDown={(e) => rove(e, \"button\", true)}\n        onPointerLeave={rest}\n        onBlur={(e) => !key.current?.contains(e.relatedTarget) && show(-1)}\n      >\n        {data.map((d, i) => (\n          <li key={d.label} role=\"none\">\n            <button\n              type=\"button\"\n              tabIndex={i === Math.max(shown, 0) ? 0 : -1}\n              data-shown={i === shown || undefined}\n              onPointerEnter={() => show(i)}\n              onFocus={() => show(i)}\n            >\n              <span className=\"ot-pie-place\" aria-hidden=\"true\">{place(i)}</span>\n              <span className=\"ot-pie-key-name\">{d.label}</span>\n              <span className=\"ot-pie-key-value\">{format(values[i])}</span>\n              <span className=\"ot-sr\">{`, ${Math.round(shares[i] * 100)}% of ${all}`}</span>\n            </button>\n          </li>\n        ))}\n      </ul>\n    </figure>\n  )\n}\n\nexport { PieChart, type PieChartProps }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/0nlytype/styles/pie-chart.css",
      "content": "@layer components {\n  /* ── ot-pie ── shares of a whole, from the dot calendar's rings: radial-chart's ring cut into arcs, a hairline of\n     paper between each, no wedges and no fills. The arcs are numbered round the rim, in the small print, and the total\n     stands inside as one very large number. Point at an arc and it inks in the accent and swells, the others step back\n     to pencil, and the total rolls down to that arc's share. */\n  .ot-pie {\n    --ot-pie-inset: var(--ot-space-6);\n    --dir: 1;\n    margin: 0; display: grid; grid-template-columns: minmax(0, 1fr); inline-size: min(100%, var(--ot-pie-size, 22rem)); row-gap: var(--ot-space-5); container-type: inline-size;\n  }\n  .ot-pie:is([dir=\"rtl\"], [dir=\"rtl\"] *) { --dir: -1; }\n  .ot-pie-face { grid-area: 1 / 1; position: relative; aspect-ratio: 1; container-type: inline-size; cursor: crosshair; }\n  .ot-pie-arcs { position: absolute; inset: 0; }\n  .ot-pie-arcs:is([dir=\"rtl\"] *) { scale: -1 1; }\n\n  /* The arcs are the whole ring: no track under them, so the gaps are paper. */\n  .ot-pie .ot-ring { --ot-ring-inset: var(--ot-pie-inset); --ot-ring-track: transparent; }\n  .ot-pie .ot-ring[data-empty] { --ot-ring-track: var(--ot-rule-strong); }\n  .ot-pie .ot-ring[data-shown] { --ot-ring-ink: var(--ot-accent); --ot-stroke: calc(3 * var(--ot-hairline)); }\n  .ot-pie:is([data-pointed], [data-force~=\"hover\"]) .ot-ring:not([data-shown]) { --ot-ring-ink: var(--ot-rule-strong); }\n\n  /* The numbers round the rim, each at the middle of its arc, halfway into the margin the ring stands in. They stay\n     upright: turned out to the arc, then back. */\n  .ot-pie-tick {\n    position: absolute; top: 50%; left: 50%;\n    transform: translate(-50%, -50%) rotate(calc(var(--dir) * (var(--ot-ring-start, 0deg) + var(--t) * var(--ot-ring-sweep, 1) * 1turn))) translateY(calc(var(--ot-pie-inset) / 2 - 50cqi)) rotate(calc(-1 * var(--dir) * (var(--ot-ring-start, 0deg) + var(--t) * var(--ot-ring-sweep, 1) * 1turn)));\n    font-size: var(--ot-pp); line-height: 1; letter-spacing: var(--ot-pp-tr); font-variant-numeric: tabular-nums lining-nums; color: var(--ot-pencil);\n    transition: color var(--ot-allegro) var(--ot-exhale);\n  }\n  .ot-pie-tick[data-shown] { color: var(--ot-ink); }\n\n  /* The number stands in the middle, as large as the ring allows: SPECTRA's scale against the small print. */\n  .ot-pie-read {\n    grid-area: 1 / 1; place-self: center; display: grid; justify-items: center; row-gap: var(--ot-space-2);\n    max-inline-size: calc(100% - 2 * var(--ot-pie-inset) - var(--ot-space-6)); text-align: center; pointer-events: none;\n  }\n  .ot-pie-value {\n    display: inline-block; font-size: clamp(var(--ot-f), 22cqi, var(--ot-fff)); line-height: 0.9; letter-spacing: var(--ot-ff-tr); font-weight: 200;\n    color: var(--ot-ink); font-variant-numeric: tabular-nums lining-nums; white-space: nowrap;\n  }\n  .ot-pie-unit { font-size: 0.4em; letter-spacing: 0; vertical-align: 0.95em; margin-inline-start: 0.08em; }\n  .ot-pie-words { display: grid; justify-items: center; font-size: var(--ot-pp); line-height: var(--ot-pp-lh); letter-spacing: var(--ot-pp-tr); }\n  .ot-pie-name { font-weight: 500; color: var(--ot-ink); }\n  .ot-pie-of { color: var(--ot-pencil); font-variant-numeric: tabular-nums; }\n\n  /* The key: a contents page of the arcs, number, name and figure on one line. One tab stop; the arrows walk it. */\n  .ot-pie-key { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; column-gap: var(--ot-space-4); margin: 0; padding: 0; list-style: none; }\n  .ot-pie-key > li { display: grid; grid-column: 1 / -1; grid-template-columns: subgrid; }\n  .ot-pie-key button {\n    appearance: none; border: 0; margin: 0; padding: var(--ot-space-1) 0; background: none; font: inherit; cursor: crosshair; text-align: start;\n    display: grid; grid-column: 1 / -1; grid-template-columns: subgrid; align-items: baseline;\n    font-size: var(--ot-pp); line-height: var(--ot-pp-lh); letter-spacing: var(--ot-pp-tr); color: var(--ot-pencil);\n    transition: color var(--ot-allegro) var(--ot-exhale);\n  }\n  .ot-pie-place, .ot-pie-key-value { font-variant-numeric: tabular-nums lining-nums; }\n  .ot-pie-key-value { color: var(--ot-graphite); transition: color var(--ot-allegro) var(--ot-exhale); }\n  .ot-pie-key button:is(:hover, :focus-visible, [data-shown]) { color: var(--ot-ink); }\n  .ot-pie-key button:is(:hover, :focus-visible, [data-shown]) .ot-pie-key-value { color: var(--ot-ink); }\n\n  /* horizon: the Eclipse. A half ring stands on a horizon that runs past it, and everything else is said below it:\n     the number on the line (cap to baseline, so its feet stand on it), its words under the line. The rows keep no gap,\n     or the subgrid would push the number half a gap below the line. Right to left, the arcs rise from the right. */\n  .ot-pie[data-variant=\"horizon\"] { --ot-ring-sweep: 0.5; --ot-ring-start: -90deg; grid-template-rows: auto auto auto; row-gap: 0; }\n  .ot-pie[data-variant=\"horizon\"] > .ot-pie-face { aspect-ratio: 2; }\n  .ot-pie[data-variant=\"horizon\"] .ot-ring { inset-block-end: auto; aspect-ratio: 1; }\n  .ot-pie[data-variant=\"horizon\"] .ot-pie-tick { top: 100%; }\n  .ot-pie[data-variant=\"horizon\"] > .ot-pie-face::after { content: \"\"; position: absolute; inset-inline: 0; inset-block-end: 0; block-size: var(--ot-hairline); background: var(--ot-rule-strong); }\n  .ot-pie[data-variant=\"horizon\"] > .ot-pie-read { grid-row: 1 / span 2; place-self: stretch; grid-template-rows: subgrid; max-inline-size: none; row-gap: 0; }\n  .ot-pie[data-variant=\"horizon\"] .ot-pie-value { align-self: end; justify-self: center; font-size: clamp(var(--ot-mf), 18cqi, var(--ot-fff)); line-height: 1; text-box: trim-end cap alphabetic; }\n  .ot-pie[data-variant=\"horizon\"] .ot-pie-words { padding-block-start: var(--ot-space-3); }\n  .ot-pie[data-variant=\"horizon\"] > .ot-pie-key { grid-row: 3; margin-block-start: var(--ot-space-5); }\n\n  @media (forced-colors: active) {\n    .ot-pie-tick { color: CanvasText; }\n  }\n}\n",
      "type": "registry:file",
      "target": "styles/0nlytype/pie-chart.css"
    }
  ],
  "css": {
    "@import \"../styles/0nlytype/pie-chart.css\"": {}
  },
  "meta": {
    "movement": "X",
    "underneath": "hook",
    "contract": "ot-pie"
  },
  "type": "registry:ui"
}
