{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chart-radar",
  "title": "Chart Radar",
  "description": "Spider chart comparing textured series across axes, with a drawn-in grid, polygons springing from the center and axis tooltips.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://trytweenly.vercel.app/r/chart-kit.json",
    "utils"
  ],
  "files": [
    {
      "path": "registry/new-york/chart-radar/chart-radar.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useId, useRef, useState } from \"react\"\nimport { animate, motion, useMotionValue, useTransform, type MotionValue } from \"motion/react\"\nimport {\n  ChartCard,\n  ChartLegend,\n  ChartPattern,\n  ChartTooltip,\n  chartTheme,\n  createFormatter,\n  polar,\n  resolveSeries,\n  textureFill,\n  toNumber,\n  useChartReveal,\n  type ChartBaseProps,\n  type ChartDatum,\n  type ChartSeries,\n  type ChartTheme,\n  type Point,\n  type ResolvedSeries,\n} from \"@/registry/new-york/lib/chart-kit\"\n\nexport interface ChartRadarProps extends ChartBaseProps {\n  /** Rows to plot, one axis per row. */\n  data: ChartDatum[]\n  /** Key holding each axis label, e.g. \"metric\". */\n  index: string\n  /** Series drawn as polygons, e.g. [{ key: \"team\" }, { key: \"benchmark\" }]. */\n  series: ChartSeries[]\n  /** Value at the outer ring. Default: a nice number above the largest value */\n  max?: number\n  /** Number of grid rings. Default: 4 */\n  levels?: number\n  /** Shape of the grid rings. Default: \"polygon\" */\n  gridShape?: \"polygon\" | \"circle\"\n  /** Opacity of solid polygon fills; textured fills scale with it. Default: 0.25 */\n  fillOpacity?: number\n  /** Draw a dot at every value. Default: true */\n  showPoints?: boolean\n  /** Show axis labels around the chart. Default: true */\n  showAxisLabels?: boolean\n  /** Show the value of each ring on the top axis. Default: false */\n  showLevelLabels?: boolean\n  /** Hovering an axis highlights it and lists every series' value. Default: true */\n  showTooltip?: boolean\n  /** Show a legend; click an item to toggle its series. Default: true */\n  showLegend?: boolean\n  /** Chart height in px; labels get extra room on the sides. Default: 260 */\n  size?: number\n  /** Polygon edges: straight or a closed smooth curve. Default: \"linear\" */\n  curve?: \"linear\" | \"smooth\"\n}\n\nfunction niceMax(value: number, levels: number) {\n  if (value <= 0) return levels\n  const raw = value / levels\n  const mag = 10 ** Math.floor(Math.log10(raw))\n  const step = [1, 2, 2.5, 5, 10].map((m) => m * mag).find((s) => s >= raw * 1.0001) ?? raw\n  return step * levels\n}\n\nfunction closedPath(pts: Point[], curve: \"linear\" | \"smooth\") {\n  if (pts.length < 2) return \"\"\n  if (curve === \"linear\" || pts.length < 3) return `${pts.map((p, i) => `${i ? \"L\" : \"M\"}${p[0]},${p[1]}`).join(\"\")}Z`\n  const n = pts.length\n  let d = `M${pts[0][0]},${pts[0][1]}`\n  for (let i = 0; i < n; i++) {\n    const p0 = pts[(i - 1 + n) % n]\n    const p1 = pts[i]\n    const p2 = pts[(i + 1) % n]\n    const p3 = pts[(i + 2) % n]\n    d += `C${p1[0] + (p2[0] - p0[0]) / 6},${p1[1] + (p2[1] - p0[1]) / 6} ${p2[0] - (p3[0] - p1[0]) / 6},${p2[1] - (p3[1] - p1[1]) / 6} ${p2[0]},${p2[1]}`\n  }\n  return `${d}Z`\n}\n\nfunction RadarGlyph() {\n  return (\n    <svg viewBox=\"0 0 16 16\" className=\"size-3.5\" fill=\"none\" stroke=\"currentColor\" strokeWidth={1.6} strokeLinejoin=\"round\">\n      <path d=\"M8 2l5.7 4.1-2.2 6.6h-7L2.3 6.1z\" />\n      <path d=\"M8 5.5l2.6 2-1 3H6.4l-1-3z\" />\n    </svg>\n  )\n}\n\nexport function ChartRadar({\n  data,\n  index,\n  series,\n  max,\n  levels = 4,\n  gridShape = \"polygon\",\n  fillOpacity = 0.25,\n  showPoints = true,\n  showAxisLabels = true,\n  showLevelLabels = false,\n  showTooltip = true,\n  showLegend = true,\n  size = 260,\n  curve = \"linear\",\n  title,\n  description,\n  titleSize,\n  surface = \"dark\",\n  accent,\n  palette,\n  icon,\n  radius,\n  bare,\n  animate: animateProp = true,\n  duration = 1.2,\n  delay = 0,\n  once = true,\n  valueFormat = \"number\",\n  currency,\n  decimals,\n  className,\n}: ChartRadarProps) {\n  const theme = chartTheme(surface, accent)\n  const fmt = createFormatter(valueFormat, { currency, decimals })\n  const uid = useId().replace(/[^a-zA-Z0-9_-]/g, \"\")\n  const { ref, shown, still } = useChartReveal(once, animateProp)\n  const boxRef = useRef<HTMLDivElement>(null)\n  const [hidden, setHidden] = useState<Set<string>>(() => new Set())\n  const [active, setActive] = useState<number | null>(null)\n  const [tip, setTip] = useState({ x: 0, y: 0, w: 1 })\n\n  const resolved = resolveSeries(series, theme, palette)\n  const n = data.length\n  const lv = Math.max(1, Math.round(levels))\n  const dataMax = Math.max(0, ...data.flatMap((row) => resolved.map((s) => toNumber(row[s.key]))))\n  const top = max && max > 0 ? max : niceMax(dataMax, lv)\n  const labelRoom = showAxisLabels ? 22 : 6\n  const R = Math.max(20, size / 2 - labelRoom)\n  const sideRoom = showAxisLabels ? 44 : 6\n  const vw = size + sideRoom * 2\n  const angleOf = (i: number) => (i * 360) / Math.max(n, 1)\n  const reveal = still ? 0 : 1\n\n  const ring = (r: number) =>\n    gridShape === \"circle\" || n < 3\n      ? `M0,${-r}A${r},${r} 0 1 1 0,${r}A${r},${r} 0 1 1 0,${-r}Z`\n      : closedPath(\n          data.map((_, i) => polar(0, 0, r, angleOf(i))),\n          \"linear\"\n        )\n\n  const toggle = (key: string) =>\n    setHidden((prev) => {\n      const next = new Set(prev)\n      if (next.has(key)) next.delete(key)\n      else if (next.size < resolved.length - 1) next.add(key)\n      return next\n    })\n\n  const track = (e: React.PointerEvent) => {\n    const box = boxRef.current?.getBoundingClientRect()\n    if (box) setTip({ x: e.clientX - box.left, y: e.clientY - box.top, w: box.width })\n  }\n\n  const activeRow = active !== null ? data[active] : undefined\n  const gridT = (i: number) => ({\n    duration: still ? 0 : duration * 0.55,\n    delay: still ? 0 : delay + i * 0.06,\n    ease: [0.65, 0, 0.35, 1] as const,\n  })\n\n  return (\n    <ChartCard\n      theme={theme}\n      title={title}\n      description={description}\n      titleSize={titleSize}\n      icon={icon === undefined ? <RadarGlyph /> : icon}\n      radius={radius}\n      bare={bare}\n      className={className}\n    >\n      <div ref={ref} className=\"flex w-full flex-col items-center gap-5\">\n        <div ref={boxRef} className=\"relative flex w-full justify-center\">\n          <svg\n            viewBox={`${-vw / 2} ${-size / 2} ${vw} ${size}`}\n            width={vw}\n            className=\"h-auto max-w-full overflow-visible\"\n            role=\"img\"\n            aria-label={title ?? \"Radar chart\"}\n            onPointerLeave={() => setActive(null)}\n          >\n            <defs>\n              {resolved.map((s) => (\n                <ChartPattern key={s.key} id={`${uid}-${s.key}`} color={s.color} texture={s.texture} theme={theme} />\n              ))}\n            </defs>\n\n            {Array.from({ length: lv }, (_, k) => (\n              <motion.path\n                key={`${gridShape}-${k}`}\n                d={ring((R * (k + 1)) / lv)}\n                fill=\"none\"\n                stroke={theme.grid}\n                strokeWidth={1}\n                initial={reveal ? { pathLength: 0, opacity: 0 } : false}\n                animate={shown ? { pathLength: 1, opacity: 1 } : { pathLength: 0, opacity: 0 }}\n                transition={gridT(k)}\n              />\n            ))}\n            {data.map((_, i) => {\n              const [x, y] = polar(0, 0, R, angleOf(i))\n              const on = active === i\n              return (\n                <motion.line\n                  key={`spoke-${i}`}\n                  x1={0}\n                  y1={0}\n                  x2={x}\n                  y2={y}\n                  stroke={on ? theme.muted : theme.grid}\n                  strokeWidth={1}\n                  initial={reveal ? { pathLength: 0 } : false}\n                  animate={{ pathLength: shown ? 1 : 0 }}\n                  transition={gridT(i * 0.5)}\n                />\n              )\n            })}\n            {showLevelLabels &&\n              Array.from({ length: lv }, (_, k) => (\n                <motion.text\n                  key={`lvl-${k}`}\n                  x={4}\n                  y={-(R * (k + 1)) / lv + 3}\n                  fontSize={9}\n                  fill={theme.muted}\n                  initial={reveal ? { opacity: 0 } : false}\n                  animate={{ opacity: shown ? 0.8 : 0 }}\n                  transition={{ duration: 0.4, delay: still ? 0 : delay + duration * 0.4 }}\n                  className=\"pointer-events-none select-none\"\n                  style={{ fontVariantNumeric: \"tabular-nums\" }}\n                >\n                  {fmt(((k + 1) * top) / lv)}\n                </motion.text>\n              ))}\n\n            {resolved.map((s, si) => (\n              <SeriesShape\n                key={s.key}\n                s={s}\n                si={si}\n                points={data.map((row, i) =>\n                  polar(0, 0, (Math.min(Math.max(toNumber(row[s.key]), 0), top * 1.08) / top) * R, angleOf(i))\n                )}\n                fill={textureFill(`${uid}-${s.key}`, s.texture, s.color)}\n                theme={theme}\n                fillOpacity={s.texture === \"solid\" ? fillOpacity : Math.min(1, fillOpacity * 3.2)}\n                curve={curve}\n                hidden={hidden.has(s.key)}\n                shown={shown}\n                still={still}\n                duration={duration}\n                delay={delay}\n                showPoints={showPoints}\n                active={active}\n              />\n            ))}\n\n            {showAxisLabels &&\n              data.map((row, i) => {\n                const a = angleOf(i)\n                const [x, y] = polar(0, 0, R + 12, a)\n                const sin = Math.sin((a * Math.PI) / 180)\n                const cos = Math.cos((a * Math.PI) / 180)\n                const on = active === i\n                return (\n                  <motion.text\n                    key={`label-${i}`}\n                    x={x}\n                    y={y + (cos < -0.3 ? 8 : cos > 0.3 ? -2 : 3.5)}\n                    textAnchor={sin > 0.2 ? \"start\" : sin < -0.2 ? \"end\" : \"middle\"}\n                    fontSize={11.5}\n                    fontWeight={on ? 600 : 500}\n                    fill={on ? theme.fg : theme.muted}\n                    initial={reveal ? { opacity: 0 } : false}\n                    animate={{ opacity: shown ? 1 : 0 }}\n                    transition={{ duration: 0.4, delay: still ? 0 : delay + duration * 0.3 + i * 0.03 }}\n                    className=\"pointer-events-none select-none\"\n                  >\n                    {String(row[index] ?? \"\")}\n                  </motion.text>\n                )\n              })}\n\n            {n > 0 &&\n              data.map((_, i) => {\n                const half = 180 / n\n                const a = angleOf(i)\n                const [x0, y0] = polar(0, 0, R + 24, a - half)\n                const [x1, y1] = polar(0, 0, R + 24, a + half)\n                const large = n < 2 ? 1 : 0\n                const d =\n                  n < 2\n                    ? `M0,${-(R + 24)}A${R + 24},${R + 24} 0 1 1 0,${R + 24}A${R + 24},${R + 24} 0 1 1 0,${-(R + 24)}Z`\n                    : `M0,0L${x0},${y0}A${R + 24},${R + 24} 0 ${large} 1 ${x1},${y1}Z`\n                return (\n                  <path\n                    key={`hit-${i}`}\n                    d={d}\n                    fill=\"transparent\"\n                    onPointerEnter={() => setActive(i)}\n                    onPointerMove={track}\n                  />\n                )\n              })}\n          </svg>\n          <ChartTooltip\n            open={showTooltip && !!activeRow}\n            x={tip.x}\n            y={tip.y}\n            width={tip.w}\n            theme={theme}\n            title={activeRow ? String(activeRow[index] ?? \"\") : undefined}\n            rows={\n              activeRow\n                ? resolved\n                    .filter((s) => !hidden.has(s.key))\n                    .map((s) => ({ label: s.label, value: fmt(toNumber(activeRow[s.key])), color: s.color, texture: s.texture }))\n                : []\n            }\n          />\n        </div>\n        {showLegend && (\n          <motion.div\n            initial={reveal ? { opacity: 0 } : false}\n            animate={{ opacity: shown ? 1 : 0 }}\n            transition={{ duration: 0.4, delay: still ? 0 : delay + duration * 0.5 }}\n          >\n            <ChartLegend series={resolved} theme={theme} hidden={hidden} onToggle={toggle} patternPrefix={uid} align=\"center\" />\n          </motion.div>\n        )}\n      </div>\n    </ChartCard>\n  )\n}\n\nfunction SeriesShape({\n  s,\n  si,\n  points,\n  fill,\n  theme,\n  fillOpacity,\n  curve,\n  hidden,\n  shown,\n  still,\n  duration,\n  delay,\n  showPoints,\n  active,\n}: {\n  s: ResolvedSeries\n  si: number\n  points: Point[]\n  fill: string\n  theme: ChartTheme\n  fillOpacity: number\n  curve: \"linear\" | \"smooth\"\n  hidden: boolean\n  shown: boolean\n  still: boolean\n  duration: number\n  delay: number\n  showPoints: boolean\n  active: number | null\n}) {\n  const grow = useMotionValue(still && !hidden ? 1 : 0)\n  const played = useRef(false)\n\n  useEffect(() => {\n    const target = hidden ? 0 : 1\n    if (still) {\n      grow.set(target)\n      return\n    }\n    if (!shown) {\n      grow.set(0)\n      played.current = false\n      return\n    }\n    const wait = played.current ? 0 : delay + duration * 0.35 + si * 0.15\n    played.current = true\n    const c = animate(grow, target, { type: \"spring\", stiffness: 140, damping: 15, mass: 0.9, delay: wait })\n    return () => c.stop()\n  }, [grow, hidden, shown, still, duration, delay, si])\n\n  const d = useTransform(grow, (k) => (k < 0.002 ? \"\" : closedPath(points.map(([x, y]) => [x * k, y * k] as Point), curve)))\n  const stroke = s.texture === \"muted\" ? theme.muted : s.color\n\n  return (\n    <g className=\"pointer-events-none\">\n      <motion.path\n        d={d}\n        fill={fill}\n        fillOpacity={fillOpacity}\n        stroke={stroke}\n        strokeWidth={1.75}\n        strokeLinejoin=\"round\"\n        strokeDasharray={s.texture === \"dots\" ? \"3 3\" : undefined}\n      />\n      {showPoints &&\n        points.map((p, i) => (\n          <RadarPoint\n            key={i}\n            p={p}\n            grow={grow}\n            color={stroke}\n            solid={s.texture === \"solid\"}\n            theme={theme}\n            on={!hidden && (shown || still)}\n            hot={active === i}\n            wait={still ? 0 : delay + duration * 0.35 + si * 0.15 + 0.25 + i * 0.03}\n          />\n        ))}\n    </g>\n  )\n}\n\nfunction RadarPoint({\n  p,\n  grow,\n  color,\n  solid,\n  theme,\n  on,\n  hot,\n  wait,\n}: {\n  p: Point\n  grow: MotionValue<number>\n  color: string\n  solid: boolean\n  theme: ChartTheme\n  on: boolean\n  hot: boolean\n  wait: number\n}) {\n  const cx = useTransform(grow, (k) => p[0] * k)\n  const cy = useTransform(grow, (k) => p[1] * k)\n\n  return (\n    <motion.circle\n      cx={cx}\n      cy={cy}\n      fill={solid ? color : theme.bg}\n      stroke={color}\n      strokeWidth={solid ? 0 : 1.5}\n      initial={{ r: 0 }}\n      animate={{ r: on ? 3 : 0, scale: hot ? 1.5 : 1 }}\n      transition={{\n        r: { type: \"spring\", stiffness: 520, damping: 18, delay: on ? wait : 0 },\n        scale: { type: \"spring\", stiffness: 520, damping: 26 },\n      }}\n    />\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "docs": "Docs, live playground and every prop: https://trytweenly.vercel.app/docs/components/chart-radar",
  "type": "registry:component"
}