{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "radar-chart",
  "title": "Radar chart",
  "description": "Several measures on one shape: a hairline polygon on a dotted construction sheet, each axis named in small caps at its end, and the axis you point at measured off as a dimension line with its figure on it.",
  "registryDependencies": [
    "https://thedirectors.agency/ui/r/0nlytype.json",
    "https://thedirectors.agency/ui/r/grid.json"
  ],
  "files": [
    {
      "path": "registry/0nlytype/ui/radar-chart.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { activeIndex } from \"@/lib/0nlytype/chart-index\"\nimport { rove } from \"@/lib/0nlytype/rove\"\nimport { Grid } from \"@/components/ui/grid\"\n\n/** One axis: its name and a number under each series key (`value` when there's one series). */\ntype RadarDatum = { label: string; value?: number; [key: string]: string | number | undefined }\ntype RadarSeries = { key: string; label: string }\n\ntype RadarChartProps = Omit<React.ComponentProps<\"figure\">, \"children\"> & {\n  /** One axis per entry, clockwise from the top. Three or more. */\n  data: RadarDatum[]\n  /** Two or three shapes on one sheet, each read from its key in every datum. */\n  series?: RadarSeries[]\n  /** Names the chart for assistive tech, e.g. \"How the three typefaces compare\". */\n  label: string\n  /** The value at the rim, shared by every axis; the measured figure reads against it (8/10). Defaults to the next round number above the largest. */\n  max?: number\n  format?: (value: number) => string\n  /** The axis the keyboard starts on, and the one a forced hover measures. Defaults to the first. */\n  now?: number\n  /** The side of one square of the dotted sheet behind, in pixels. */\n  cell?: number\n  /** Pins the pointed-at look on `now`; set on the root for documentation. */\n  \"data-force\"?: string\n}\n\nconst figures = new Intl.NumberFormat(\"en-GB\")\n// The rim's radius: a third of the plot's width, less where the plot is narrow, so the axis names keep ROOM\n// pixels beside it and stay off the sheet's lettered border.\nconst RIM = 0.34\nconst ROOM = 76\n// How far each dimension line stands off its spoke, in pixels, and how long its end ticks are.\nconst OFF = 12\nconst TICK = 5\n\n// ponytail: chart's 1, 2, 2.5, 5, 10 ceiling, kept here so a radar doesn't pull in the bar chart.\nfunction ceiling(n: number) {\n  if (n <= 0) return 1\n  const p = 10 ** Math.floor(Math.log10(n))\n  return ([1, 2, 2.5, 5, 10].find((s) => s * p >= n) ?? 10) * p\n}\n\ntype XY = [number, number]\nconst at = (r: number, a: number): XY => [Math.sin(a) * r, -Math.cos(a) * r]\nconst poly = (p: XY[]) => `M${p.map(([x, y]) => `${x},${y}`).join(\"L\")}Z`\n\n/**\n * Several measures on one shape: a hairline polygon on a dotted construction sheet, each axis named in small caps at\n * its end. Point at an axis, or focus its name, and its value is measured off as a draughtsman's dimension line, from\n * the centre out along the spoke to the point, with its figure set on it.\n */\nfunction RadarChart({ data, series, label, max, format = figures.format, now = 0, cell = 27, className, style, ...props }: RadarChartProps) {\n  const force = props[\"data-force\"]?.split(\" \").includes(\"hover\")\n  const [active, setShown] = React.useState(force ? now : -1)\n  const shown = activeIndex(active, data.length)\n  // Forget an axis that was removed, so adding axes later cannot revive an old highlight.\n  if (active !== shown) setShown(shown)\n  const [w, setW] = React.useState(0)\n  const draw = React.useRef<SVGSVGElement>(null)\n  const plot = React.useRef<HTMLDivElement>(null)\n  const observe = React.useCallback((el: SVGSVGElement | null) => {\n    draw.current = el\n    if (!el) return\n    const ro = new ResizeObserver(([e]) => setW(e.contentRect.width))\n    ro.observe(el)\n    return () => {\n      ro.disconnect()\n      draw.current = null\n    }\n  }, [])\n\n  const list = series?.length ? series.slice(0, 3) : [{ key: \"value\", label: \"\" }]\n  const n = data.length\n  const read = (d: RadarDatum, k: string) => Math.max(0, Number(d[k] ?? 0) || 0)\n  const top = max && max > 0 ? max : ceiling(Math.max(...data.flatMap((d) => list.map((s) => read(d, s.key)))))\n  const angle = (i: number) => (i / n) * 2 * Math.PI\n  const R = Math.max(0.2 * w, Math.min(RIM * w, w / 2 - ROOM))\n  const rest = () => !plot.current?.contains(document.activeElement) && setShown(-1)\n\n  // The axis nearest the pointer, by angle; the very middle keeps what's shown.\n  function pick(e: React.PointerEvent) {\n    const box = draw.current?.getBoundingClientRect()\n    if (!box) return\n    const rtl = getComputedStyle(e.currentTarget).direction === \"rtl\"\n    const dx = (e.clientX - box.left - box.width / 2) * (rtl ? -1 : 1)\n    const dy = e.clientY - box.top - box.height / 2\n    if (Math.hypot(dx, dy) < 8) return\n    const t = (Math.atan2(dx, -dy) / (2 * Math.PI) + 1) % 1\n    setShown(Math.round(t * n) % n)\n  }\n\n  if (n < 3) {\n    return (\n      <figure data-slot=\"radar-chart\" aria-label={label} className={cn(\"ot-radar\", className)} style={style} {...props}>\n        <p className=\"ot-radar-note\">A radar needs three measures or more; this one has {n}.</p>\n      </figure>\n    )\n  }\n\n  const says = (d: RadarDatum) => list.map((s) => `${s.label ? `${s.label} ` : \"\"}${format(read(d, s.key))}`).join(\", \")\n\n  return (\n    <figure\n      data-slot=\"radar-chart\"\n      data-pointed={shown >= 0 || undefined}\n      className={cn(\"ot-radar\", className)}\n      style={{ \"--n\": n, \"--m\": list.length, ...style } as React.CSSProperties}\n      {...props}\n    >\n      <Grid variant=\"dots\" cell={cell} className=\"ot-radar-sheet\">\n        <div\n          ref={plot}\n          role=\"group\"\n          aria-label={label}\n          data-slot=\"radar-chart-plot\"\n          className=\"ot-radar-plot\"\n          style={w ? ({ \"--r\": `${R}px` } as React.CSSProperties) : undefined}\n          onKeyDown={(e) => rove(e, \"button\", true)}\n          onPointerMove={pick}\n          onPointerLeave={rest}\n          onBlur={(e) => !plot.current?.contains(e.relatedTarget) && setShown(-1)}\n        >\n          <svg ref={observe} data-slot=\"radar-chart-draw\" className=\"ot-radar-draw\" aria-hidden=\"true\">\n            {w ? (\n              <g transform={`translate(${w / 2} ${w / 2})`}>\n                <g className=\"ot-radar-build\">\n                  {[0.25, 0.5, 0.75, 1].map((q) => (\n                    <path key={q} data-rim={q === 1 || undefined} d={poly(data.map((_, i) => at(R * q, angle(i))))} />\n                  ))}\n                  {data.map((d, i) => {\n                    const [x, y] = at(R, angle(i))\n                    return <line key={d.label} x1={0} y1={0} x2={x} y2={y} />\n                  })}\n                </g>\n                {list.map((s, k) => {\n                  const p = data.map((d, i) => at((R * Math.min(read(d, s.key), top)) / top, angle(i)))\n                  return (\n                    <g key={s.key} data-series={k} className=\"ot-radar-series\">\n                      <path className=\"ot-radar-line\" d={poly(p)} />\n                      {p.map(([x, y], i) => (\n                        <circle key={i} className=\"ot-radar-bead\" data-shown={i === shown || undefined} cx={x} cy={y} r={3} />\n                      ))}\n                    </g>\n                  )\n                })}\n                {shown >= 0 ? (\n                  // The dimension lines: one per series, stepped off the spoke, from the centre to the point, a tick at each end.\n                  <g key={shown} className=\"ot-radar-dims\">\n                    {list.map((s, k) => {\n                      const a = angle(shown)\n                      const r = (R * Math.min(read(data[shown], s.key), top)) / top\n                      const off = OFF * (k + 1)\n                      const [nx, ny] = [Math.cos(a), Math.sin(a)]\n                      const [ex, ey] = at(r, a)\n                      const [x0, y0, x1, y1] = [nx * off, ny * off, ex + nx * off, ey + ny * off]\n                      return (\n                        <g key={s.key} data-series={k} className=\"ot-radar-dim\">\n                          <path d={`M${x0},${y0}L${x1},${y1}M${x0 - nx * TICK},${y0 - ny * TICK}L${x0 + nx * TICK},${y0 + ny * TICK}M${x1 - nx * TICK},${y1 - ny * TICK}L${x1 + nx * TICK},${y1 + ny * TICK}`} />\n                        </g>\n                      )\n                    })}\n                  </g>\n                ) : null}\n              </g>\n            ) : null}\n          </svg>\n          {shown >= 0 && w && list.length === 1\n            ? list.map((s, k) => {\n                const a = angle(shown)\n                const v = read(data[shown], s.key)\n                const [mx, my] = at((R * Math.min(v, top)) / top / 2, a)\n                // Clear of its dimension line whichever way the spoke runs: half the figure's extent across the spoke.\n                const text = `${format(v)}/${format(top)}`\n                const off = OFF + 4 + Math.abs(Math.cos(a)) * text.length * 3.8 + Math.abs(Math.sin(a)) * 7\n                return (\n                  <span\n                    key={`${shown}-${s.key}`}\n                    data-series={k}\n                    className=\"ot-radar-figure\"\n                    aria-hidden=\"true\"\n                    style={{ \"--x\": `${mx + Math.cos(a) * off}px`, \"--y\": `${my + Math.sin(a) * off}px` } as React.CSSProperties}\n                  >\n                    {format(v)}\n                    <span className=\"ot-radar-of\">/{format(top)}</span>\n                  </span>\n                )\n              })\n            : null}\n          {data.map((d, i) => {\n            const a = angle(i)\n            return (\n              <button\n                key={d.label}\n                type=\"button\"\n                tabIndex={i === (shown >= 0 ? shown : activeIndex(now, n, 0)) ? 0 : -1}\n                data-slot=\"radar-chart-axis\"\n                className=\"ot-radar-axis\"\n                data-shown={i === shown || undefined}\n                style={{ \"--c\": Math.sin(a).toFixed(4), \"--s\": (-Math.cos(a)).toFixed(4) } as React.CSSProperties}\n                aria-label={`${d.label}: ${says(d)} of ${format(top)}`}\n                onPointerEnter={() => setShown(i)}\n                onFocus={() => setShown(i)}\n              >\n                {d.label}\n              </button>\n            )\n          })}\n        </div>\n      </Grid>\n      {list.length > 1 ? (\n        <figcaption data-slot=\"radar-chart-key\" className=\"ot-radar-key\">\n          {list.map((s, k) => (\n            <span key={s.key} data-series={k} className=\"ot-radar-name\">\n              <svg className=\"ot-radar-swatch\" aria-hidden=\"true\"><line x1=\"0\" y1=\"50%\" x2=\"100%\" y2=\"50%\" /></svg>\n              {s.label}\n              {shown >= 0 ? <span className=\"ot-radar-key-value\"> {format(read(data[shown], s.key))}</span> : null}\n            </span>\n          ))}\n        </figcaption>\n      ) : null}\n    </figure>\n  )\n}\n\nexport { RadarChart, type RadarChartProps, type RadarDatum, type RadarSeries }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/0nlytype/styles/radar-chart.css",
      "content": "@layer components {\n  /* ── ot-radar ── several measures on one shape, drawn as Paul Rand draws \"design\": a dotted construction sheet (the\n     grid item), dotted construction polygons at the quarters and dotted spokes, the shape itself one ink hairline with a\n     paper bead at each point, and each axis named in small caps at its end. Pointing at an axis measures it off as a\n     dimension line, ticked at both ends and stepped off the spoke, with its figure set on it. */\n  .ot-radar { --dir: 1; margin: 0; display: grid; row-gap: var(--ot-space-4); inline-size: min(100%, var(--ot-radar-size, 30rem)); }\n  .ot-radar:is([dir=\"rtl\"], [dir=\"rtl\"] *) { --dir: -1; }\n  .ot-radar-plot { position: relative; aspect-ratio: 1; cursor: crosshair; }\n  .ot-radar-draw { position: absolute; inset: 0; inline-size: 100%; block-size: 100%; overflow: visible; pointer-events: none; }\n  .ot-radar-draw:is([dir=\"rtl\"] *) { scale: -1 1; }\n\n  /* The construction: dotted, faint, the rim a step firmer. */\n  .ot-radar-build > * { fill: none; stroke: var(--ot-rule-strong); stroke-width: var(--ot-hairline); stroke-dasharray: 1 4; }\n  .ot-radar-build > [data-rim] { stroke: var(--ot-pencil); stroke-dasharray: 1 3; }\n\n  /* The shape: ink hairline, paper beads. The second series dashed graphite, the third dashed pencil (chart's voices). */\n  .ot-radar-line { fill: none; stroke: var(--c, var(--ot-ink)); stroke-width: var(--ot-hairline); stroke-linejoin: round; }\n  .ot-radar-series[data-series=\"1\"], .ot-radar-dim[data-series=\"1\"], .ot-radar-name[data-series=\"1\"] { --c: var(--ot-graphite); --dash: 4 2; }\n  .ot-radar-series[data-series=\"2\"], .ot-radar-dim[data-series=\"2\"], .ot-radar-name[data-series=\"2\"] { --c: var(--ot-pencil); --dash: 1 2; }\n  .ot-radar-series > .ot-radar-line { stroke-dasharray: var(--dash, none); }\n  .ot-radar-bead {\n    fill: var(--ot-paper); stroke: var(--c, var(--ot-ink)); stroke-width: var(--ot-hairline);\n    transition: r var(--ot-moderato) var(--ot-spiccato), fill var(--ot-allegro) var(--ot-exhale);\n  }\n  .ot-radar-series[data-series=\"0\"] > .ot-radar-bead[data-shown] { r: 4.5px; fill: var(--ot-accent); stroke: var(--ot-accent); }\n\n  /* The dimension line: drawn out from the centre along the spoke (moderato, exhale), a tick at each end. */\n  .ot-radar-dims { animation: ot-radar-measure var(--ot-moderato) var(--ot-exhale) both; transform-origin: 0 0; }\n  .ot-radar-dim > path { fill: none; stroke: var(--c, var(--ot-ink)); stroke-width: var(--ot-hairline); }\n  @keyframes ot-radar-measure { from { scale: 0; opacity: 0; } to { scale: 1; opacity: 1; } }\n\n  .ot-radar-figure {\n    position: absolute; inset-inline-start: calc(50% + var(--x)); inset-block-start: calc(50% + var(--y)); pointer-events: none;\n    font-size: var(--ot-pp); line-height: 1; font-variant-numeric: tabular-nums lining-nums; white-space: nowrap;\n  }\n  .ot-radar-figure { translate: -50% -50%; font-weight: 500; color: var(--c, var(--ot-ink)); animation: ot-radar-set var(--ot-moderato) var(--ot-exhale) both; }\n  .ot-radar-figure[data-series=\"1\"] { font-family: var(--ot-expression); font-style: italic; font-size: calc(var(--ot-pp) * var(--ot-expression-scale)); font-weight: 400; }\n  .ot-radar-figure:is([dir=\"rtl\"] *) { translate: 50% -50%; }\n  @keyframes ot-radar-set { from { opacity: 0; } }\n  /* The rim's value after the figure, in pencil: SPECTRA's fractions, the measure against its whole. */\n  .ot-radar-of { font-weight: 400; color: var(--ot-pencil); }\n\n  /* The axis names at the spokes' ends, in small caps, each hung off its end in the direction the spoke runs. The inline\n     inset mirrors itself right to left; the translate is physical, so it turns with --dir. */\n  .ot-radar-axis {\n    position: absolute; appearance: none; border: 0; margin: 0; padding: var(--ot-space-1); background: none; font: inherit; cursor: crosshair;\n    inset-inline-start: calc(50% + var(--c) * (var(--r, 34%) + var(--ot-space-2))); inset-block-start: calc(50% + var(--s) * (var(--r, 34%) + var(--ot-space-2)));\n    translate: calc(var(--dir) * (-50% + var(--c) * 50%)) calc(-50% + var(--s) * 50%);\n    inline-size: max-content; max-inline-size: 11em; font-size: var(--ot-pp); line-height: var(--ot-pp-lh); font-variant-caps: all-small-caps; letter-spacing: 0.08em; font-weight: 500;\n    color: var(--ot-pencil); text-align: center; transition: color var(--ot-allegro) var(--ot-exhale);\n  }\n  .ot-radar-axis:is(:hover, :focus-visible, [data-shown]) { color: var(--ot-ink); }\n\n  /* With series, a key: each name after a sample of its line, and its figure on the axis you're on. */\n  .ot-radar-key { display: flex; flex-wrap: wrap; gap: var(--ot-space-2) var(--ot-space-5); font-size: var(--ot-pp); line-height: var(--ot-pp-lh); letter-spacing: var(--ot-pp-tr); color: var(--ot-graphite); }\n  .ot-radar-name { display: inline-flex; align-items: baseline; gap: var(--ot-space-2); }\n  .ot-radar-name[data-series=\"1\"] { font-family: var(--ot-expression); font-style: italic; font-size: calc(var(--ot-pp) * var(--ot-expression-scale)); }\n  .ot-radar-name[data-series=\"2\"] { color: var(--ot-pencil); }\n  .ot-radar-swatch { inline-size: var(--ot-space-5); block-size: 0.5em; overflow: visible; }\n  .ot-radar-swatch > line { stroke: var(--c, var(--ot-ink)); stroke-width: var(--ot-hairline); stroke-dasharray: var(--dash, none); }\n  .ot-radar-key-value { color: var(--ot-ink); font-variant-numeric: tabular-nums; }\n  .ot-radar-note { font-size: var(--ot-p); color: var(--ot-graphite); }\n\n  @media (forced-colors: active) {\n    .ot-radar-line, .ot-radar-dim > path { stroke: CanvasText; }\n    .ot-radar-build > * { stroke: GrayText; }\n    .ot-radar-series[data-series=\"0\"] > .ot-radar-bead[data-shown] { fill: Highlight; stroke: Highlight; }\n  }\n}\n",
      "type": "registry:file",
      "target": "styles/0nlytype/radar-chart.css"
    }
  ],
  "css": {
    "@import \"../styles/0nlytype/radar-chart.css\"": {}
  },
  "meta": {
    "movement": "X",
    "underneath": "hook",
    "contract": "ot-radar"
  },
  "type": "registry:ui"
}
