{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "radial-chart",
  "title": "Radial chart",
  "description": "Progress against one maximum: a hairline ring per series with its arc inked as far as it has got, and one very large number inside that rolls to the ring you point at.",
  "registryDependencies": [
    "https://thedirectors.agency/ui/r/0nlytype.json"
  ],
  "files": [
    {
      "path": "registry/0nlytype/ui/radial-chart.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { roll } from \"@/lib/0nlytype/roll\"\n\nconst share = (n: number) => (Number.isFinite(n) ? Math.min(1, Math.max(0, n)) : 0)\n\ntype RingProps = Omit<React.ComponentProps<\"span\">, \"children\"> & {\n  /** How much of the ring the arc covers, 0–1. */\n  value: number\n  /** Where the arc begins, 0–1 of the way round from the ring's start. */\n  from?: number\n  /** The end of the arc that carries the dot, or none. */\n  head?: \"end\" | \"start\" | \"none\"\n}\n\n/**\n * One hairline ring with an arc drawn on it at stroke width, and a dot at one end of the arc: the calendar's ring\n * for what's to come, inked for what was had. Absolutely placed in a positioned box, square unless the box says\n * otherwise. The parent sets how much of a turn the ring spans (--ot-ring-sweep, 1 by default) and where it\n * starts (--ot-ring-start, 0deg is the top); --ot-ring-inset steps it in, --ot-ring-ink and --ot-ring-head colour it.\n * The arc glides between values at andante. Decorative: the words that say its value live elsewhere.\n */\nfunction Ring({ value, from = 0, head = \"end\", className, style, ...props }: RingProps) {\n  return (\n    <span\n      data-slot=\"ring\"\n      data-head={head === \"none\" ? undefined : head}\n      aria-hidden=\"true\"\n      className={cn(\"ot-ring\", className)}\n      style={{ \"--ot-ring-p\": share(value), \"--ot-ring-from\": share(from), ...style } as React.CSSProperties}\n      {...props}\n    >\n      {head === \"none\" ? null : <i className=\"ot-ring-head\" />}\n    </span>\n  )\n}\n\ntype RadialChartProps = Omit<React.ComponentProps<\"figure\">, \"children\"> & {\n  /** One ring per entry, the first outermost. */\n  data: { label: string; value: number }[]\n  /** Names the chart for assistive tech, e.g. \"Reading goals, 2026\". */\n  label: string\n  /** What a closed ring means, shared by every series. */\n  max?: number\n  /** What the values count. The readout reads \"of 100 pages\". */\n  unit?: string\n  /** The series the number rests on; its dot carries the accent. Defaults to the first. */\n  now?: number\n  format?: (value: number) => string\n  /** ring: full concentric rings, the number inside. horizon: half rings standing on a horizon, the number on it. */\n  variant?: \"ring\" | \"horizon\"\n  /** Pins the pointed-at look on the series the number is on; set on the root for documentation. */\n  \"data-force\"?: string\n}\n\nconst figures = new Intl.NumberFormat(\"en-GB\")\n\n/**\n * Progress against a shared maximum: one hairline ring per series, its arc inked as far as it has got,\n * and one very large number inside. Point at a ring or focus its name and that ring inks, the others step\n * back and the number rolls to it; leave and it rolls back to now.\n */\nfunction RadialChart({ data, label, max = 100, unit, now = 0, format = figures.format, variant = \"ring\", className, style, ...props }: RadialChartProps) {\n  const [shown, setShown] = React.useState(now)\n  const [pointed, setPointed] = React.useState(false)\n  const target = React.useRef(now)\n  const value = React.useRef<HTMLSpanElement>(null)\n  const face = React.useRef<HTMLDivElement>(null)\n  const key = React.useRef<HTMLUListElement>(null)\n  const top = max > 0 ? max : 100\n  const many = data.length > 1\n\n  function show(i: number, on = true) {\n    setPointed(on)\n    if (i === target.current || !value.current || !data[i]) return\n    const dir = data[i].value > data[target.current].value ? 1 : -1\n    target.current = i\n    roll(value.current, () => setShown(i), \"0.3em\", dir)\n  }\n\n  // The ring nearest the pointer, by radius; the middle, where the number stands, keeps what's shown.\n  function pick(e: React.PointerEvent) {\n    const rings = face.current?.querySelectorAll<HTMLElement>(\".ot-ring\")\n    if (!rings?.length) return\n    const box = rings[0].getBoundingClientRect()\n    const d = Math.hypot(e.clientX - (box.left + box.width / 2), e.clientY - (box.top + box.height / 2))\n    let best = -1\n    let gap = 14\n    rings.forEach((r, i) => {\n      const off = Math.abs(r.getBoundingClientRect().width / 2 - d)\n      if (off < gap) [best, gap] = [i, off]\n    })\n    if (best >= 0) show(best)\n  }\n\n  const at = data[shown] ?? data[now]\n  const of = `of ${format(top)}${unit ? ` ${unit}` : \"\"}`\n\n  return (\n    <figure\n      data-slot=\"radial-chart\"\n      data-variant={variant}\n      data-pointed={pointed || undefined}\n      aria-label={label}\n      className={cn(\"ot-radial\", className)}\n      style={{ \"--n\": data.length, ...style } as React.CSSProperties}\n      {...props}\n    >\n      <div\n        ref={face}\n        data-slot=\"radial-chart-face\"\n        className=\"ot-radial-face\"\n        aria-hidden=\"true\"\n        onPointerMove={many ? pick : undefined}\n        onPointerLeave={many ? () => !key.current?.contains(document.activeElement) && show(now, false) : undefined}\n      >\n        {data.map((d, i) => (\n          <Ring key={d.label} value={d.value / top} data-shown={i === shown || undefined} style={{ \"--i\": i } as React.CSSProperties} />\n        ))}\n      </div>\n      <figcaption data-slot=\"radial-chart-read\" className=\"ot-radial-read\">\n        <span ref={value} className=\"ot-radial-value\">\n          {at ? format(at.value) : \"\"}\n        </span>\n        <span className=\"ot-radial-words\">\n          <span className=\"ot-radial-name\">{at?.label}</span>\n          <span className=\"ot-radial-of\">{of}</span>\n        </span>\n      </figcaption>\n      {many ? (\n        <ul\n          ref={key}\n          data-slot=\"radial-chart-key\"\n          className=\"ot-radial-key\"\n          onPointerLeave={() => !key.current?.contains(document.activeElement) && show(now, false)}\n          onBlur={(e) => !key.current?.contains(e.relatedTarget) && show(now, false)}\n        >\n          {data.map((d, i) => (\n            <li key={d.label}>\n              <button type=\"button\" data-shown={i === shown || undefined} onPointerEnter={() => show(i)} onFocus={() => show(i)}>\n                <span>{d.label}</span> <span className=\"ot-radial-key-value\">{format(d.value)}</span>\n                <span className=\"ot-sr\"> {of}</span>\n              </button>\n            </li>\n          ))}\n        </ul>\n      ) : null}\n    </figure>\n  )\n}\n\nexport { RadialChart, Ring, type RadialChartProps, type RingProps }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/0nlytype/styles/radial-chart.css",
      "content": "@layer components {\n  /* ── ot-ring ── one hairline ring with an arc inked on it and a dot at one end. The calendar's vocabulary: a pencil\n     ring for what's to come, ink for what was had. Both strokes are inset box-shadows cut by a conic mask (as the dot\n     calendar sweeps its days), so they stay one hairline and one stroke at any size. The parent sets the span of a turn\n     (--ot-ring-sweep, 1) and where it starts (--ot-ring-start, 0deg is the top, clockwise); --ot-ring-inset steps a\n     ring in, --ot-ring-ink, --ot-ring-track and --ot-ring-head colour it. The timer and the pie chart stand on it. */\n  @property --ot-ring-p { syntax: \"<number>\"; inherits: true; initial-value: 0; }\n  @property --ot-ring-from { syntax: \"<number>\"; inherits: true; initial-value: 0; }\n  .ot-ring {\n    --ot-ring-p: 0; --ot-ring-from: 0;\n    position: absolute; inset: var(--ot-ring-inset, 0); border-radius: 50%; pointer-events: none;\n    transition: --ot-ring-p var(--ot-andante) var(--ot-exhale), --ot-ring-from var(--ot-andante) var(--ot-exhale);\n  }\n  .ot-ring::before, .ot-ring::after { content: \"\"; position: absolute; inset: 0; border-radius: 50%; }\n  .ot-ring::before {\n    box-shadow: inset 0 0 0 var(--ot-hairline) var(--ot-ring-track, var(--ot-rule-strong));\n    mask: conic-gradient(from var(--ot-ring-start, 0deg), #000 calc(var(--ot-ring-sweep, 1) * 1turn), #0000 0);\n  }\n  .ot-ring::after {\n    box-shadow: inset 0 0 0 var(--ot-stroke) var(--ot-ring-ink, var(--ot-ink));\n    mask: conic-gradient(from calc(var(--ot-ring-start, 0deg) + var(--ot-ring-from) * var(--ot-ring-sweep, 1) * 1turn), #000 calc(var(--ot-ring-p) * var(--ot-ring-sweep, 1) * 1turn), #0000 0);\n    transition: box-shadow var(--ot-allegro) var(--ot-exhale);\n  }\n  /* The dot rides the arc's end, its centre on the stroke's centre line. */\n  .ot-ring-head { position: absolute; top: 0; left: 50%; inline-size: 0; block-size: 50%; transform-origin: 50% 100%; rotate: calc(var(--ot-ring-start, 0deg) + (var(--ot-ring-from) + var(--ot-ring-p)) * var(--ot-ring-sweep, 1) * 1turn); }\n  .ot-ring[data-head=\"start\"] > .ot-ring-head { rotate: calc(var(--ot-ring-start, 0deg) + var(--ot-ring-from) * var(--ot-ring-sweep, 1) * 1turn); }\n  .ot-ring-head::after {\n    content: \"\"; position: absolute; top: calc(var(--ot-stroke) / 2); left: 50%; inline-size: var(--ot-dot); block-size: var(--ot-dot); border-radius: 50%; translate: -50% -50%;\n    background: var(--ot-ring-head, var(--ot-ink));\n    transition: background-color var(--ot-allegro) var(--ot-exhale), scale var(--ot-moderato) var(--ot-spiccato);\n  }\n  @media (forced-colors: active) {\n    .ot-ring::before { box-shadow: inset 0 0 0 var(--ot-hairline) GrayText; }\n    .ot-ring::after { box-shadow: inset 0 0 0 var(--ot-stroke) CanvasText; }\n    .ot-ring-head::after { background: Highlight; }\n  }\n\n  /* ── ot-radial ── progress against one maximum, from the dot calendar: a hairline ring per series, the arc inked as\n     far as it has got, and one very large number inside it. Pointing at a ring, or its name, inks it and steps the\n     others back to pencil; the number rolls to it and the accent dot moves to its ring. */\n  .ot-radial {\n    --ot-radial-gap: var(--ot-space-4);\n    margin: 0; display: grid; grid-template-columns: minmax(0, 1fr); inline-size: min(100%, var(--ot-radial-size, 20rem)); row-gap: var(--ot-space-5); container-type: inline-size;\n  }\n  .ot-radial-face { grid-area: 1 / 1; position: relative; aspect-ratio: 1; }\n  .ot-radial .ot-ring { --ot-ring-inset: calc(var(--i, 0) * var(--ot-radial-gap)); }\n  .ot-radial .ot-ring[data-shown] { --ot-ring-head: var(--ot-accent); }\n  .ot-radial:is([data-pointed], [data-force~=\"hover\"]) .ot-ring:not([data-shown]) { --ot-ring-ink: var(--ot-rule-strong); --ot-ring-head: var(--ot-rule-strong); --ot-ring-track: var(--ot-rule); }\n  .ot-radial-face { cursor: crosshair; }\n\n  /* The number stands in the middle, as large as the innermost ring allows: SPECTRA's scale against the small print. */\n  .ot-radial-read {\n    grid-area: 1 / 1; place-self: center; display: grid; justify-items: center; row-gap: var(--ot-space-2); max-inline-size: calc(100% - 2 * var(--n, 1) * var(--ot-radial-gap) - var(--ot-space-5));\n    text-align: center; pointer-events: none;\n  }\n  .ot-radial-value {\n    display: inline-block; font-size: clamp(var(--ot-f), calc(34cqi - var(--n, 1) * 3cqi), 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-radial-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-radial-name { font-weight: 500; color: var(--ot-ink); }\n  .ot-radial-of { color: var(--ot-pencil); }\n\n  /* The key names each ring, outermost first. Pointing at a name, or focusing it, inks its ring. */\n  .ot-radial-key { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--ot-space-2) var(--ot-space-5); margin: 0; padding: 0; list-style: none; }\n  .ot-radial-key button {\n    appearance: none; border: 0; margin: 0; padding: var(--ot-space-1) 0; background: none; font: inherit; cursor: crosshair;\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-radial-key-value { color: var(--ot-graphite); font-variant-numeric: tabular-nums; transition: color var(--ot-allegro) var(--ot-exhale); }\n  .ot-radial-key button:is(:hover, :focus-visible, [data-shown]) { color: var(--ot-ink); }\n  .ot-radial-key button:is(:hover, :focus-visible, [data-shown]) .ot-radial-key-value { color: var(--ot-ink); }\n\n  /* horizon: the Eclipse. Half rings stand on a horizon that runs past them, and everything else is said below it:\n     the number sits on the line, its words under the line. Right to left, the arcs rise from the right. */\n  .ot-radial[data-variant=\"horizon\"] { --ot-ring-sweep: 0.5; --ot-ring-start: -90deg; grid-template-rows: auto auto auto; row-gap: 0; padding-inline: var(--ot-space-5); inline-size: min(100%, calc(var(--ot-radial-size, 20rem) + 2 * var(--ot-space-5))); }\n  .ot-radial[data-variant=\"horizon\"] > .ot-radial-face { aspect-ratio: 2; }\n  .ot-radial[data-variant=\"horizon\"] .ot-ring { inset-block-end: auto; aspect-ratio: 1; }\n  .ot-radial[data-variant=\"horizon\"] > .ot-radial-face::after { content: \"\"; position: absolute; inset-inline: calc(-1 * var(--ot-space-5)); inset-block-end: 0; block-size: var(--ot-hairline); background: var(--ot-rule-strong); }\n  .ot-radial[data-variant=\"horizon\"]:is([dir=\"rtl\"], [dir=\"rtl\"] *) > .ot-radial-face { scale: -1 1; }\n  .ot-radial[data-variant=\"horizon\"] > .ot-radial-read { grid-row: 1 / span 2; place-self: stretch; grid-template-rows: subgrid; max-inline-size: none; row-gap: 0; }\n  .ot-radial[data-variant=\"horizon\"] .ot-radial-value { align-self: end; justify-self: center; font-size: clamp(var(--ot-mf), calc(26cqi - var(--n, 1) * 3cqi), var(--ot-fff)); line-height: 1; text-box: trim-end cap alphabetic; }\n  .ot-radial[data-variant=\"horizon\"] .ot-radial-words { padding-block-start: var(--ot-space-3); }\n  /* No row gap on the horizon: the subgrid would push the number half a gap below the line. The key keeps its distance. */\n  .ot-radial[data-variant=\"horizon\"] > .ot-radial-key { margin-block-start: var(--ot-space-5); }\n}\n",
      "type": "registry:file",
      "target": "styles/0nlytype/radial-chart.css"
    }
  ],
  "css": {
    "@import \"../styles/0nlytype/radial-chart.css\"": {}
  },
  "meta": {
    "movement": "X",
    "underneath": "hook",
    "contract": "ot-radial"
  },
  "type": "registry:ui"
}
