{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ink/dither-radar-chart",
  "title": "Dither Radar Chart",
  "description": "Dithered radar charts for comparing multiple series across terminal-friendly radial axes",
  "dependencies": [
    "ink"
  ],
  "registryDependencies": [
    "https://termcn.dev/r/ink/interaction.json",
    "https://termcn.dev/r/ink/terminal-style.json",
    "https://termcn.dev/r/ink/use-motion.json",
    "https://termcn.dev/r/ink/use-theme.json",
    "https://termcn.dev/r/ink/use-unicode.json"
  ],
  "files": [
    {
      "path": "registry/lib/dither-chart-utils.ts",
      "content": "export type DitherColor =\n  | \"green\"\n  | \"blue\"\n  | \"purple\"\n  | \"pink\"\n  | \"orange\"\n  | \"red\"\n  | \"grey\";\n\nexport type AreaVariant = \"gradient\" | \"dotted\" | \"hatched\" | \"solid\";\nexport type StrokeVariant = \"solid\" | \"dashed\";\nexport type StackType = \"default\" | \"stacked\" | \"percent\";\nexport type ChartType = \"area\" | \"line\" | \"bar\" | \"pie\" | \"radar\";\nexport type SeriesKind = \"area\" | \"line\" | \"bar\" | \"radar\";\nexport type DotVariant = \"border\" | \"colored-border\" | \"filled\";\nexport type BloomLevel = \"off\" | \"low\" | \"high\" | \"aura\";\nexport type BloomBlend = \"plus-lighter\" | \"screen\" | \"lighten\";\n\nexport interface BloomConfig {\n  blend?: BloomBlend;\n  blur: number;\n  brightness: number;\n  opacity: number;\n  saturate?: number;\n}\n\nexport type BloomInput = BloomLevel | BloomConfig;\n\nexport type ChartConfig = Record<\n  string,\n  {\n    color: DitherColor;\n    label?: string;\n  }\n>;\n\nexport type ChartRow = Record<string, unknown>;\n\nexport interface DitherCell {\n  bold?: boolean;\n  char: string;\n  color?: string;\n  dim?: boolean;\n  inverse?: boolean;\n  priority?: number;\n}\n\nexport interface DotSpec {\n  radius: number;\n  variant: DotVariant;\n}\n\nexport interface SeriesSpec {\n  activeDot?: DotSpec;\n  dataKey: string;\n  dot?: DotSpec;\n  isClickable: boolean;\n  kind: SeriesKind;\n  strokeVariant: StrokeVariant;\n  variant: AreaVariant;\n}\n\nexport interface AxisSpec {\n  dataKey?: string;\n  maxTicks?: number;\n  tickCount?: number;\n  tickFormatter?: (value: unknown, index: number) => string;\n}\n\nexport interface GridSpec {\n  horizontal: boolean;\n  strokeDasharray: string;\n  vertical: boolean;\n}\n\nexport interface ChartMargins {\n  bottom: number;\n  left: number;\n  right: number;\n  top: number;\n}\n\nexport interface DitherRenderInput {\n  bloom: BloomInput;\n  bloomOnHover: boolean;\n  config: ChartConfig;\n  data: ChartRow[];\n  focusDataKey: string | null;\n  grid?: GridSpec;\n  height: number;\n  hovered: boolean;\n  kind: ChartType;\n  margins?: Partial<ChartMargins>;\n  markerIndex: number | null;\n  nameKey?: string;\n  pie?: {\n    innerRadius: number;\n    variant: AreaVariant;\n    valueKey: string;\n  };\n  progress: number;\n  selectedDataKey: string | null;\n  series: SeriesSpec[];\n  stackType: StackType;\n  unicode: boolean;\n  width: number;\n  xAxis?: AxisSpec;\n  yAxis?: AxisSpec;\n}\n\nexport interface DitherRenderResult {\n  frame: DitherCell[][];\n  max: number;\n  names: string[];\n  plot: {\n    height: number;\n    left: number;\n    top: number;\n    width: number;\n  };\n}\n\nexport type DitherChartRenderer = (\n  input: DitherRenderInput\n) => DitherRenderResult;\n\nexport interface PreparedDitherChart {\n  bands: Record<string, [number, number][]>;\n  frame: DitherCell[][];\n  input: DitherRenderInput;\n  max: number;\n  names: string[];\n  plot: DitherRenderResult[\"plot\"];\n}\n\nconst PALETTE: Record<DitherColor, string> = {\n  blue: \"#3590f3\",\n  green: \"#28d26e\",\n  grey: \"#8c8c96\",\n  orange: \"#ff9632\",\n  pink: \"#f05abe\",\n  purple: \"#966eff\",\n  red: \"#f04646\",\n};\n\nconst BAYER4 = [\n  [0, 8, 2, 10],\n  [12, 4, 14, 6],\n  [3, 11, 1, 9],\n  [15, 7, 13, 5],\n] as const;\n\nexport const clamp = (value: number, min: number, max: number): number =>\n  Math.max(min, Math.min(max, value));\n\nconst finiteNumber = (value: unknown): number =>\n  typeof value === \"number\" && Number.isFinite(value) ? Math.max(0, value) : 0;\n\nexport const makeFrame = (width: number, height: number): DitherCell[][] =>\n  Array.from({ length: height }, () =>\n    Array.from({ length: width }, () => ({ char: \" \", priority: 0 }))\n  );\n\nexport const paint = (\n  frame: DitherCell[][],\n  x: number,\n  y: number,\n  cell: DitherCell\n): void => {\n  const row = frame[y];\n  const current = row?.[x];\n  if (!current || (current.priority ?? 0) > (cell.priority ?? 1)) {\n    return;\n  }\n  row[x] = cell;\n};\n\nconst ditherChar = (\n  density: number,\n  variant: AreaVariant,\n  x: number,\n  y: number,\n  unicode: boolean\n): string => {\n  const threshold = ((BAYER4[y % 4]?.[x % 4] ?? 0) + 0.5) / 16;\n  const level = clamp(density * 1.2 - threshold * 0.2, 0, 1);\n  if (variant === \"solid\") {\n    return unicode ? \"█\" : \"#\";\n  }\n  if (level <= threshold * 0.68) {\n    return \" \";\n  }\n  if (variant === \"dotted\") {\n    if (!unicode) {\n      return \".\";\n    }\n    return level > 0.82 ? \"•\" : \"·\";\n  }\n  if (variant === \"hatched\") {\n    if (!unicode) {\n      return \"/\";\n    }\n    return (x + y) % 2 === 0 ? \"╱\" : \"╲\";\n  }\n  if (unicode) {\n    if (level > 0.88) {\n      return \"█\";\n    }\n    if (level > 0.68) {\n      return \"▓\";\n    }\n    if (level > 0.46) {\n      return \"▒\";\n    }\n    if (level > 0.26) {\n      return \"░\";\n    }\n    return \"·\";\n  }\n  if (level > 0.82) {\n    return \"#\";\n  }\n  if (level > 0.58) {\n    return \"+\";\n  }\n  if (level > 0.34) {\n    return \":\";\n  }\n  return \".\";\n};\n\nexport const paintDither = (\n  frame: DitherCell[][],\n  x: number,\n  y: number,\n  density: number,\n  variant: AreaVariant,\n  color: string,\n  unicode: boolean,\n  dim: boolean,\n  priority = 4\n): void => {\n  let char = ditherChar(density, variant, x, y, unicode);\n  if (char === \" \") {\n    return;\n  }\n  if (unicode && density > 0.9 && (x * 17 + y * 31) % 47 === 0) {\n    char = \"✦\";\n  }\n  paint(frame, x, y, { char, color, dim, priority });\n};\n\nexport const lineChar = (unicode: boolean, active = false): string => {\n  if (!unicode) {\n    return active ? \"O\" : \"*\";\n  }\n  return active ? \"◉\" : \"●\";\n};\n\nexport const dotChar = (unicode: boolean, variant: DotVariant): string => {\n  if (variant === \"filled\") {\n    return lineChar(unicode);\n  }\n  return unicode ? \"○\" : \"o\";\n};\n\nexport const colorFor = (config: ChartConfig, key: string): string =>\n  PALETTE[config[key]?.color ?? \"grey\"];\n\nexport const labelOf = (config: ChartConfig, key: string): string =>\n  config[key]?.label ?? key;\n\nexport const valueAt = (row: ChartRow | undefined, key: string): number =>\n  finiteNumber(row?.[key]);\n\nexport const chartNames = (\n  kind: ChartType,\n  data: ChartRow[],\n  series: SeriesSpec[],\n  nameKey?: string\n): string[] => {\n  if (kind === \"pie\") {\n    return data.map((row, index) => String(row[nameKey ?? \"name\"] ?? index));\n  }\n  return series.map((item) => item.dataKey);\n};\n\nexport const computeBands = (\n  data: ChartRow[],\n  series: SeriesSpec[],\n  stackType: StackType\n): { bands: Record<string, [number, number][]>; max: number } => {\n  const bands: Record<string, [number, number][]> = {};\n  let max = 0;\n  if (stackType === \"default\") {\n    for (const item of series) {\n      const itemBands: [number, number][] = [];\n      for (const row of data) {\n        const value = valueAt(row, item.dataKey);\n        max = Math.max(max, value);\n        itemBands.push([0, value]);\n      }\n      bands[item.dataKey] = itemBands;\n    }\n    return { bands, max: max || 1 };\n  }\n\n  for (let index = 0; index < data.length; index += 1) {\n    const row = data[index] ?? {};\n    const total = series.reduce(\n      (sum, item) => sum + valueAt(row, item.dataKey),\n      0\n    );\n    let floor = 0;\n    for (const item of series) {\n      const raw = valueAt(row, item.dataKey);\n      const value = stackType === \"percent\" && total > 0 ? raw / total : raw;\n      const top = floor + value;\n      (bands[item.dataKey] ??= [])[index] = [floor, top];\n      floor = top;\n      max = Math.max(max, top);\n    }\n  }\n  return { bands, max: max || 1 };\n};\n\nconst defaultMargins = (input: DitherRenderInput): ChartMargins => ({\n  bottom: input.xAxis ? 2 : 0,\n  left: input.yAxis ? 7 : 0,\n  right: 0,\n  top: 0,\n  ...input.margins,\n});\n\nexport const resolvePlot = (\n  input: DitherRenderInput\n): DitherRenderResult[\"plot\"] => {\n  const margins = defaultMargins(input);\n  return {\n    height: Math.max(2, input.height - margins.top - margins.bottom),\n    left: clamp(margins.left, 0, Math.max(0, input.width - 2)),\n    top: clamp(margins.top, 0, Math.max(0, input.height - 2)),\n    width: Math.max(2, input.width - margins.left - margins.right),\n  };\n};\n\nexport const xPosition = (\n  index: number,\n  count: number,\n  left: number,\n  width: number\n): number => {\n  if (count <= 1) {\n    return left + Math.floor((width - 1) / 2);\n  }\n  return left + Math.round((index / (count - 1)) * (width - 1));\n};\n\nexport const yPosition = (\n  value: number,\n  max: number,\n  top: number,\n  height: number\n): number => top + height - 1 - Math.round((value / max) * (height - 1));\n\nconst formatNumber = (value: number): string => {\n  if (Math.abs(value) >= 1000) {\n    return `${(value / 1000).toFixed(1)}k`;\n  }\n  if (Number.isInteger(value)) {\n    return String(value);\n  }\n  return value.toFixed(1);\n};\n\nexport const paintGrid = (\n  frame: DitherCell[][],\n  input: DitherRenderInput,\n  plot: DitherRenderResult[\"plot\"],\n  max: number\n): void => {\n  const { grid } = input;\n  if (!grid) {\n    return;\n  }\n  const char = input.unicode ? \"┄\" : \".\";\n  if (grid.horizontal) {\n    for (let tick = 0; tick < 4; tick += 1) {\n      const value = (tick / 3) * max;\n      const y = yPosition(value, max, plot.top, plot.height);\n      for (let x = plot.left; x < plot.left + plot.width; x += 1) {\n        if (!grid.strokeDasharray || x % 2 === 0) {\n          paint(frame, x, y, { char, dim: true, priority: 1 });\n        }\n      }\n    }\n  }\n  if (grid.vertical) {\n    const step = Math.max(1, Math.ceil(input.data.length / 8));\n    for (let index = 0; index < input.data.length; index += 1) {\n      if (index % step !== 0) {\n        continue;\n      }\n      const x = xPosition(index, input.data.length, plot.left, plot.width);\n      for (let y = plot.top; y < plot.top + plot.height; y += 1) {\n        if (!grid.strokeDasharray || y % 2 === 0) {\n          paint(frame, x, y, {\n            char: input.unicode ? \"┊\" : \":\",\n            dim: true,\n            priority: 1,\n          });\n        }\n      }\n    }\n  }\n};\n\nexport const paintCrosshair = (\n  frame: DitherCell[][],\n  input: DitherRenderInput,\n  plot: DitherRenderResult[\"plot\"]\n): void => {\n  if (input.markerIndex === null || input.data.length === 0) {\n    return;\n  }\n  const index = clamp(input.markerIndex, 0, input.data.length - 1);\n  const x = xPosition(index, input.data.length, plot.left, plot.width);\n  for (let y = plot.top; y < plot.top + plot.height; y += 1) {\n    paint(frame, x, y, {\n      char: input.unicode ? \"┊\" : \":\",\n      dim: true,\n      priority: 2,\n    });\n  }\n};\n\nconst paintYAxis = (\n  frame: DitherCell[][],\n  input: DitherRenderInput,\n  plot: DitherRenderResult[\"plot\"],\n  max: number\n): void => {\n  if (!input.yAxis) {\n    return;\n  }\n  const count = Math.max(2, input.yAxis.tickCount ?? 4);\n  for (let tick = 0; tick < count; tick += 1) {\n    const value = (tick / (count - 1)) * max;\n    const y = yPosition(value, max, plot.top, plot.height);\n    const formatter = input.yAxis.tickFormatter;\n    const label = formatter ? formatter(value, tick) : formatNumber(value);\n    const fitted = label.slice(-Math.max(1, plot.left - 1));\n    const padded = fitted.padStart(Math.max(1, plot.left - 1));\n    for (let index = 0; index < padded.length; index += 1) {\n      paint(frame, index, y, {\n        char: padded[index] ?? \" \",\n        dim: true,\n        priority: 8,\n      });\n    }\n    paint(frame, plot.left - 1, y, {\n      char: input.unicode ? \"┤\" : \"|\",\n      dim: true,\n      priority: 8,\n    });\n  }\n};\n\nconst paintXAxis = (\n  frame: DitherCell[][],\n  input: DitherRenderInput,\n  plot: DitherRenderResult[\"plot\"]\n): void => {\n  if (!input.xAxis || input.data.length === 0) {\n    return;\n  }\n  const axisY = plot.top + plot.height;\n  if (axisY < frame.length) {\n    for (let x = plot.left; x < plot.left + plot.width; x += 1) {\n      paint(frame, x, axisY, {\n        char: input.unicode ? \"─\" : \"-\",\n        dim: true,\n        priority: 8,\n      });\n    }\n  }\n  const maxTicks = Math.max(2, input.xAxis.maxTicks ?? 8);\n  const step = Math.max(1, Math.ceil(input.data.length / maxTicks));\n  for (let index = 0; index < input.data.length; index += 1) {\n    if (index % step !== 0 && index !== input.data.length - 1) {\n      continue;\n    }\n    const row = input.data[index] ?? {};\n    const raw = input.xAxis.dataKey ? row[input.xAxis.dataKey] : index;\n    const label = input.xAxis.tickFormatter\n      ? input.xAxis.tickFormatter(raw, index)\n      : String(raw ?? \"\");\n    const x = xPosition(index, input.data.length, plot.left, plot.width);\n    const start = clamp(\n      x - Math.floor(label.length / 2),\n      plot.left,\n      Math.max(plot.left, plot.left + plot.width - label.length)\n    );\n    const visibleLabel = label.slice(0, plot.width);\n    for (let offset = 0; offset < visibleLabel.length; offset += 1) {\n      paint(frame, start + offset, axisY + 1, {\n        char: visibleLabel[offset] ?? \" \",\n        dim: true,\n        priority: 8,\n      });\n    }\n  }\n};\n\nexport const paintAxes = (\n  frame: DitherCell[][],\n  input: DitherRenderInput,\n  plot: DitherRenderResult[\"plot\"],\n  max: number\n): void => {\n  paintYAxis(frame, input, plot, max);\n  paintXAxis(frame, input, plot);\n};\n\nconst bloomLevel = (input: BloomInput): BloomLevel => {\n  if (typeof input === \"string\") {\n    return input;\n  }\n  if (input.blur >= 10) {\n    return \"aura\";\n  }\n  if (input.blur >= 5) {\n    return \"high\";\n  }\n  return \"low\";\n};\n\ninterface BloomSource {\n  color: string;\n  x: number;\n  y: number;\n}\n\nconst bloomSources = (frame: DitherCell[][]): BloomSource[] => {\n  const sources: BloomSource[] = [];\n  for (let y = 0; y < frame.length; y += 1) {\n    const row = frame[y] ?? [];\n    for (let x = 0; x < row.length; x += 1) {\n      const cell = row[x];\n      if ((cell?.priority ?? 0) >= 4 && cell?.color) {\n        sources.push({ color: cell.color, x, y });\n      }\n    }\n  }\n  return sources;\n};\n\nconst paintBloomSource = (\n  frame: DitherCell[][],\n  input: DitherRenderInput,\n  radius: number,\n  source: BloomSource\n): void => {\n  for (let dy = -radius; dy <= radius; dy += 1) {\n    for (let dx = -radius; dx <= radius; dx += 1) {\n      if (dx === 0 && dy === 0) {\n        continue;\n      }\n      const distance = Math.abs(dx) + Math.abs(dy);\n      if (distance > radius + 1) {\n        continue;\n      }\n      paint(frame, source.x + dx, source.y + dy, {\n        char: input.unicode ? \"·\" : \".\",\n        color: source.color,\n        dim: true,\n        priority: 2,\n      });\n    }\n  }\n};\n\nconst applyBloom = (frame: DitherCell[][], input: DitherRenderInput): void => {\n  const level = bloomLevel(input.bloom);\n  if (level === \"off\" || (input.bloomOnHover && !input.hovered)) {\n    return;\n  }\n  const configuredRadius =\n    typeof input.bloom === \"string\"\n      ? undefined\n      : clamp(Math.round(input.bloom.blur / 3), 1, 3);\n  const radius = Math.max(\n    1,\n    Math.round(configuredRadius ?? (level === \"aura\" ? 2 : 1))\n  );\n\n  for (const source of bloomSources(frame)) {\n    paintBloomSource(frame, input, radius, source);\n  }\n};\n\nexport const prepareDitherChart = (\n  input: DitherRenderInput,\n  kind: ChartType\n): PreparedDitherChart => {\n  const width = Math.max(8, Math.floor(input.width));\n  const height = Math.max(3, Math.floor(input.height));\n  const normalizedInput = { ...input, height, kind, width };\n  const frame = makeFrame(width, height);\n  const plot = resolvePlot(normalizedInput);\n  const { bands, max } = computeBands(\n    normalizedInput.data,\n    normalizedInput.series,\n    normalizedInput.stackType\n  );\n  const names = chartNames(\n    kind,\n    normalizedInput.data,\n    normalizedInput.series,\n    normalizedInput.nameKey\n  );\n  return { bands, frame, input: normalizedInput, max, names, plot };\n};\n\nexport const finishDitherChart = ({\n  frame,\n  input,\n  max,\n  names,\n  plot,\n}: PreparedDitherChart): DitherRenderResult => {\n  applyBloom(frame, input);\n  return { frame, max, names, plot };\n};\n\nexport const easeInOutCubic = (value: number): number =>\n  value < 0.5 ? 4 * value * value * value : 1 - (-2 * value + 2) ** 3 / 2;\n",
      "type": "registry:file",
      "target": "lib/dither-chart-utils.ts"
    },
    {
      "path": "registry/lib/dither-radar-chart-utils.ts",
      "content": "import {\n  clamp,\n  colorFor,\n  finishDitherChart,\n  paint,\n  paintDither,\n  prepareDitherChart,\n  valueAt,\n} from \"./dither-chart-utils\";\nimport type {\n  DitherCell,\n  DitherChartRenderer,\n  DitherRenderInput,\n  DitherRenderResult,\n  SeriesSpec,\n} from \"./dither-chart-utils\";\n\ninterface Point {\n  x: number;\n  y: number;\n}\n\nconst pointInPolygon = (point: Point, polygon: Point[]): boolean => {\n  let inside = false;\n  for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i, i += 1) {\n    const a = polygon[i];\n    const b = polygon[j];\n    if (!(a && b)) {\n      continue;\n    }\n    const intersects =\n      a.y > point.y !== b.y > point.y &&\n      point.x < ((b.x - a.x) * (point.y - a.y)) / (b.y - a.y || 1) + a.x;\n    if (intersects) {\n      inside = !inside;\n    }\n  }\n  return inside;\n};\n\nconst distanceToSegment = (point: Point, start: Point, end: Point): number => {\n  const dx = end.x - start.x;\n  const dy = end.y - start.y;\n  const lengthSquared = dx * dx + dy * dy;\n  const amount =\n    lengthSquared === 0\n      ? 0\n      : clamp(\n          ((point.x - start.x) * dx + (point.y - start.y) * dy) / lengthSquared,\n          0,\n          1\n        );\n  return Math.hypot(\n    point.x - (start.x + amount * dx),\n    point.y - (start.y + amount * dy)\n  );\n};\n\nconst paintRadarFrame = (\n  frame: DitherCell[][],\n  input: DitherRenderInput,\n  axes: number,\n  cx: number,\n  cy: number,\n  radiusX: number,\n  radiusY: number\n): void => {\n  for (let ring = 1; ring <= 4; ring += 1) {\n    const ratio = ring / 4;\n    for (let step = 0; step < 72; step += 1) {\n      const angle = (step / 72) * Math.PI * 2 - Math.PI / 2;\n      paint(\n        frame,\n        Math.round(cx + Math.cos(angle) * radiusX * ratio),\n        Math.round(cy + Math.sin(angle) * radiusY * ratio),\n        {\n          char: input.unicode ? \"·\" : \".\",\n          dim: true,\n          priority: 1,\n        }\n      );\n    }\n  }\n\n  for (let index = 0; index < input.data.length; index += 1) {\n    const row = input.data[index] ?? {};\n    const angle = (index / axes) * Math.PI * 2 - Math.PI / 2;\n    const end = {\n      x: Math.round(cx + Math.cos(angle) * radiusX),\n      y: Math.round(cy + Math.sin(angle) * radiusY),\n    };\n    const steps = Math.max(Math.abs(end.x - cx), Math.abs(end.y - cy));\n    for (let step = 0; step <= steps; step += 1) {\n      paint(\n        frame,\n        Math.round(cx + ((end.x - cx) * step) / Math.max(1, steps)),\n        Math.round(cy + ((end.y - cy) * step) / Math.max(1, steps)),\n        {\n          char: input.unicode ? \"·\" : \".\",\n          dim: true,\n          priority: 1,\n        }\n      );\n    }\n    const label = String(row[input.nameKey ?? \"name\"] ?? index).slice(0, 8);\n    const labelX = clamp(\n      end.x - Math.floor(label.length / 2),\n      0,\n      Math.max(0, input.width - label.length)\n    );\n    for (let offset = 0; offset < label.length; offset += 1) {\n      paint(frame, labelX + offset, clamp(end.y, 0, input.height - 1), {\n        char: label[offset] ?? \" \",\n        dim: true,\n        priority: 9,\n      });\n    }\n  }\n};\n\nconst paintRadarSeries = (\n  frame: DitherCell[][],\n  input: DitherRenderInput,\n  plot: DitherRenderResult[\"plot\"],\n  max: number,\n  axes: number,\n  cx: number,\n  cy: number,\n  radiusX: number,\n  radiusY: number,\n  series: SeriesSpec,\n  seriesIndex: number\n): void => {\n  const polygon = input.data.map((row, index) => {\n    const angle = (index / axes) * Math.PI * 2 - Math.PI / 2;\n    const ratio = (valueAt(row, series.dataKey) / max) * input.progress;\n    return {\n      x: cx + Math.cos(angle) * radiusX * ratio,\n      y: cy + Math.sin(angle) * radiusY * ratio,\n    };\n  });\n  const color = colorFor(input.config, series.dataKey);\n  const emphasis = input.selectedDataKey ?? input.focusDataKey;\n  const dim = emphasis !== null && emphasis !== series.dataKey;\n  for (let y = plot.top; y < plot.top + plot.height; y += 1) {\n    for (let x = plot.left; x < plot.left + plot.width; x += 1) {\n      const point = { x, y };\n      const inside = pointInPolygon(point, polygon);\n      const edge = polygon.some(\n        (start, index) =>\n          distanceToSegment(\n            point,\n            start,\n            polygon[(index + 1) % polygon.length] ?? start\n          ) <= 0.58\n      );\n      if (inside) {\n        paintDither(\n          frame,\n          x,\n          y,\n          0.68 + (input.hovered ? 0.1 : 0),\n          series.variant,\n          color,\n          input.unicode,\n          dim,\n          4 + seriesIndex\n        );\n      }\n      if (edge) {\n        paint(frame, x, y, {\n          bold: true,\n          char: input.unicode ? \"•\" : \"*\",\n          color,\n          dim,\n          priority: 7 + seriesIndex,\n        });\n      }\n    }\n  }\n};\n\nexport const renderDitherRadarChart: DitherChartRenderer = (input) => {\n  const prepared = prepareDitherChart(input, \"radar\");\n  const { frame, input: normalized, max, plot } = prepared;\n  const axes = normalized.data.length;\n\n  if (axes >= 3) {\n    const cx = plot.left + (plot.width - 1) / 2;\n    const cy = plot.top + (plot.height - 1) / 2;\n    const radiusY = Math.max(\n      2,\n      Math.min((plot.height - 2) / 2, plot.width / 4)\n    );\n    const radiusX = Math.max(4, Math.min((plot.width - 4) / 2, radiusY * 2));\n    paintRadarFrame(frame, normalized, axes, cx, cy, radiusX, radiusY);\n    for (\n      let seriesIndex = 0;\n      seriesIndex < normalized.series.length;\n      seriesIndex += 1\n    ) {\n      const series = normalized.series[seriesIndex];\n      if (series) {\n        paintRadarSeries(\n          frame,\n          normalized,\n          plot,\n          max,\n          axes,\n          cx,\n          cy,\n          radiusX,\n          radiusY,\n          series,\n          seriesIndex\n        );\n      }\n    }\n  }\n\n  return finishDitherChart(prepared);\n};\n",
      "type": "registry:file",
      "target": "lib/dither-radar-chart-utils.ts"
    },
    {
      "path": "registry/ui/dither-chart.tsx",
      "content": "import { useIsScreenReaderEnabled, useStdout, Box, Text } from \"ink\";\nimport * as React from \"react\";\n\nimport { useInteraction } from \"@/hooks/use-interaction\";\nimport { useMotion } from \"@/hooks/use-motion\";\nimport { useTheme } from \"@/hooks/use-theme\";\nimport { useUnicode } from \"@/hooks/use-unicode\";\nimport {\n  colorFor,\n  easeInOutCubic,\n  labelOf,\n  valueAt,\n} from \"@/lib/dither-chart-utils\";\nimport type {\n  AreaVariant,\n  AxisSpec,\n  BloomBlend,\n  BloomConfig,\n  BloomInput,\n  ChartConfig,\n  ChartMargins,\n  ChartRow,\n  ChartType,\n  DitherCell,\n  DitherChartRenderer,\n  DitherColor,\n  DotSpec,\n  DotVariant,\n  GridSpec,\n  SeriesSpec,\n  SeriesKind,\n  StackType,\n  StrokeVariant,\n} from \"@/lib/dither-chart-utils\";\nimport { resolveBorderStyle } from \"@/lib/terminal-style\";\n\nexport type {\n  AreaVariant,\n  BloomBlend,\n  BloomConfig,\n  BloomInput,\n  ChartConfig,\n  ChartMargins as Margins,\n  ChartType,\n  DitherColor,\n  DotVariant,\n  SeriesKind,\n  StackType,\n  StrokeVariant,\n};\n\ntype DataObject = object;\n\nexport interface SeriesProps {\n  children?: React.ReactNode;\n  dataKey: string;\n  isClickable?: boolean;\n  strokeVariant?: StrokeVariant;\n  variant?: AreaVariant;\n}\n\nexport interface DotProps {\n  r?: number;\n  variant?: DotVariant;\n}\n\nexport interface GridProps {\n  horizontal?: boolean;\n  strokeDasharray?: string;\n  vertical?: boolean;\n}\n\nexport interface XAxisProps {\n  dataKey?: string;\n  maxTicks?: number;\n  tickFormatter?: (value: unknown, index: number) => string;\n  tickMargin?: number;\n}\n\nexport interface YAxisProps {\n  tickCount?: number;\n  tickFormatter?: (value: number) => string;\n  tickMargin?: number;\n}\n\nexport interface LegendProps {\n  align?: \"left\" | \"center\" | \"right\";\n  isClickable?: boolean;\n}\n\nexport type TooltipVariant = \"default\" | \"frosted-glass\";\n\nexport interface TooltipProps {\n  labelKey?: string;\n  valueFormatter?: (value: number, name: string) => string;\n  variant?: TooltipVariant;\n}\n\ntype ChartRole =\n  | \"active-dot\"\n  | \"area\"\n  | \"bar\"\n  | \"dot\"\n  | \"grid\"\n  | \"legend\"\n  | \"line\"\n  | \"pie\"\n  | \"radar\"\n  | \"tooltip\"\n  | \"x-axis\"\n  | \"y-axis\";\n\ntype ChartPart = React.ComponentType<Record<string, unknown>> & {\n  chartRole?: ChartRole;\n};\n\nexport function Dot(_props: DotProps): null {\n  return null;\n}\nDot.chartRole = \"dot\" as const;\n\nexport function ActiveDot(_props: DotProps): null {\n  return null;\n}\nActiveDot.chartRole = \"active-dot\" as const;\n\nexport function Grid(_props: GridProps): null {\n  return null;\n}\nGrid.chartRole = \"grid\" as const;\n\nexport function XAxis(_props: XAxisProps): null {\n  return null;\n}\nXAxis.chartRole = \"x-axis\" as const;\n\nexport function YAxis(_props: YAxisProps): null {\n  return null;\n}\nYAxis.chartRole = \"y-axis\" as const;\n\nexport function Legend(_props: LegendProps): null {\n  return null;\n}\nLegend.chartRole = \"legend\" as const;\n\nexport function Tooltip(_props: TooltipProps): null {\n  return null;\n}\nTooltip.chartRole = \"tooltip\" as const;\n\ninterface ParsedParts {\n  grid?: GridSpec;\n  legend?: Required<LegendProps>;\n  pieVariant: AreaVariant;\n  series: SeriesSpec[];\n  tooltip?: Required<Pick<TooltipProps, \"variant\">> & TooltipProps;\n  xAxis?: AxisSpec;\n  yAxis?: AxisSpec;\n}\n\ninterface ParsedPieProps {\n  variant?: AreaVariant;\n}\n\ninterface ParsedRadarProps {\n  dataKey: string;\n  variant?: AreaVariant;\n}\n\nconst dotSpec = (props: DotProps): DotSpec => ({\n  radius: props.r ?? 2,\n  variant: props.variant ?? \"border\",\n});\n\nconst parseDots = (\n  children: React.ReactNode\n): Pick<SeriesSpec, \"activeDot\" | \"dot\"> => {\n  const result: Pick<SeriesSpec, \"activeDot\" | \"dot\"> = {};\n  React.Children.forEach(children, (child) => {\n    if (!React.isValidElement(child)) {\n      return;\n    }\n    const role = (child.type as ChartPart).chartRole;\n    const props = child.props as DotProps;\n    if (role === \"dot\") {\n      result.dot = dotSpec(props);\n    }\n    if (role === \"active-dot\") {\n      result.activeDot = dotSpec(props);\n    }\n  });\n  return result;\n};\n\nconst parseParts = (children: React.ReactNode): ParsedParts => {\n  const parsed: ParsedParts = { pieVariant: \"gradient\", series: [] };\n\n  const visit = (nodes: React.ReactNode): void => {\n    React.Children.forEach(nodes, (child) => {\n      if (!React.isValidElement(child)) {\n        return;\n      }\n      const role = (child.type as ChartPart).chartRole;\n      if (!role) {\n        const nested = (child.props as { children?: React.ReactNode }).children;\n        if (nested) {\n          visit(nested);\n        }\n        return;\n      }\n      if (role === \"area\" || role === \"line\" || role === \"bar\") {\n        const props = child.props as SeriesProps;\n        parsed.series.push({\n          ...parseDots(props.children),\n          dataKey: props.dataKey,\n          isClickable: props.isClickable ?? false,\n          kind: role,\n          strokeVariant: props.strokeVariant ?? \"solid\",\n          variant: props.variant ?? \"gradient\",\n        });\n        return;\n      }\n      if (role === \"radar\") {\n        const props = child.props as ParsedRadarProps;\n        parsed.series.push({\n          dataKey: props.dataKey,\n          isClickable: false,\n          kind: \"radar\",\n          strokeVariant: \"solid\",\n          variant: props.variant ?? \"gradient\",\n        });\n        return;\n      }\n      if (role === \"pie\") {\n        parsed.pieVariant =\n          (child.props as ParsedPieProps).variant ?? \"gradient\";\n        return;\n      }\n      if (role === \"grid\") {\n        const props = child.props as GridProps;\n        parsed.grid = {\n          horizontal: props.horizontal ?? true,\n          strokeDasharray: props.strokeDasharray ?? \"3 3\",\n          vertical: props.vertical ?? false,\n        };\n        return;\n      }\n      if (role === \"x-axis\") {\n        const props = child.props as XAxisProps;\n        parsed.xAxis = {\n          dataKey: props.dataKey,\n          maxTicks: props.maxTicks ?? 8,\n          tickFormatter: props.tickFormatter,\n        };\n        return;\n      }\n      if (role === \"y-axis\") {\n        const props = child.props as YAxisProps;\n        parsed.yAxis = {\n          tickCount: props.tickCount ?? 4,\n          tickFormatter: props.tickFormatter\n            ? (value) => props.tickFormatter?.(Number(value)) ?? \"\"\n            : undefined,\n        };\n        return;\n      }\n      if (role === \"legend\") {\n        const props = child.props as LegendProps;\n        parsed.legend = {\n          align: props.align ?? \"right\",\n          isClickable: props.isClickable ?? false,\n        };\n        return;\n      }\n      if (role === \"tooltip\") {\n        const props = child.props as TooltipProps;\n        parsed.tooltip = { ...props, variant: props.variant ?? \"default\" };\n      }\n    });\n  };\n\n  visit(children);\n  return parsed;\n};\n\nexport interface TerminalChartOptions {\n  \"aria-label\"?: string;\n  accessibleSummary?: string;\n  autoFocus?: boolean;\n  className?: string;\n  disabled?: boolean;\n  emptyMessage?: string;\n  height?: number;\n  id?: string;\n  isActive?: boolean;\n  noColor?: boolean;\n  reducedMotion?: boolean;\n  screenReaderMode?: boolean;\n  selectedDataKey?: string | null;\n  showControls?: boolean;\n  title?: string;\n  unicode?: boolean;\n  width?: number;\n}\n\nexport interface InteractionOptions {\n  bloom?: BloomInput;\n  bloomOnHover?: boolean;\n  defaultSelectedDataKey?: string | null;\n  hovered?: boolean;\n  interactive?: boolean;\n  markerIndex?: number | null;\n  onHoverChange?: (index: number | null) => void;\n  onSelectionChange?: (key: string | null) => void;\n}\n\nexport interface MotionOptions {\n  animate?: boolean;\n  animationDuration?: number;\n  replayToken?: number;\n}\n\nexport type CartesianChartProps<TData extends DataObject> =\n  TerminalChartOptions &\n    InteractionOptions &\n    MotionOptions & {\n      children: React.ReactNode;\n      config: ChartConfig;\n      data: TData[];\n      margins?: Partial<ChartMargins>;\n      stackType?: StackType;\n    };\n\nexport interface DitherChartProps\n  extends TerminalChartOptions, InteractionOptions, MotionOptions {\n  children: React.ReactNode;\n  config: ChartConfig;\n  data: ChartRow[];\n  dataKey?: string;\n  kind: ChartType;\n  innerRadius?: number;\n  margins?: Partial<ChartMargins>;\n  nameKey?: string;\n  renderChart: DitherChartRenderer;\n  stackType?: StackType;\n}\n\nconst useEntrance = ({\n  animate,\n  data,\n  duration,\n  reduced,\n  replayToken,\n}: {\n  animate: boolean;\n  data: ChartRow[];\n  duration: number;\n  reduced: boolean;\n  replayToken: number;\n}): number => {\n  const [progress, setProgress] = React.useState(animate && !reduced ? 0 : 1);\n\n  React.useEffect(() => {\n    if (!animate || reduced || duration <= 0) {\n      setProgress(1);\n      return;\n    }\n    const startedAt = Date.now();\n    setProgress(0);\n    const timer = setInterval(() => {\n      const elapsed = Date.now() - startedAt;\n      const next = Math.min(1, elapsed / duration);\n      setProgress(easeInOutCubic(next));\n      if (next >= 1) {\n        clearInterval(timer);\n      }\n    }, 80);\n    return () => clearInterval(timer);\n  }, [animate, data, duration, reduced, replayToken]);\n\n  return progress;\n};\n\ninterface CellRun extends Omit<DitherCell, \"char\"> {\n  start: number;\n  text: string;\n}\n\nconst cellStyleKey = (cell: DitherCell, noColor: boolean): string =>\n  [\n    noColor ? \"\" : (cell.color ?? \"\"),\n    cell.bold ? \"1\" : \"0\",\n    cell.dim ? \"1\" : \"0\",\n    cell.inverse ? \"1\" : \"0\",\n  ].join(\":\");\n\nconst rowRuns = (row: DitherCell[], noColor: boolean): CellRun[] => {\n  const runs: CellRun[] = [];\n  for (let index = 0; index < row.length; index += 1) {\n    const cell = row[index];\n    if (!cell) {\n      continue;\n    }\n    const previous = runs.at(-1);\n    const key = cellStyleKey(cell, noColor);\n    const previousKey = previous\n      ? cellStyleKey(\n          {\n            bold: previous.bold,\n            char: \"\",\n            color: previous.color,\n            dim: previous.dim,\n            inverse: previous.inverse,\n          },\n          noColor\n        )\n      : \"\";\n    if (previous && previousKey === key) {\n      previous.text += cell.char;\n      continue;\n    }\n    runs.push({\n      bold: cell.bold,\n      color: noColor ? undefined : cell.color,\n      dim: cell.dim,\n      inverse: cell.inverse,\n      start: index,\n      text: cell.char,\n    });\n  }\n  return runs;\n};\n\nconst clampIndex = (index: number, length: number): number | null =>\n  length > 0 ? Math.max(0, Math.min(length - 1, index)) : null;\n\nexport const DitherChart = ({\n  animate = true,\n  animationDuration = 900,\n  bloom = \"off\",\n  bloomOnHover = false,\n  children,\n  config,\n  data,\n  dataKey,\n  defaultSelectedDataKey = null,\n  emptyMessage = \"No data\",\n  height = 12,\n  hovered = false,\n  innerRadius = 0,\n  interactive = true,\n  isActive = true,\n  margins,\n  markerIndex = null,\n  nameKey,\n  noColor = false,\n  onHoverChange,\n  onSelectionChange,\n  reducedMotion,\n  renderChart,\n  replayToken = 0,\n  screenReaderMode = false,\n  selectedDataKey: controlledSelected,\n  showControls = false,\n  stackType = \"default\",\n  title,\n  unicode,\n  width = 56,\n  kind,\n  id,\n  autoFocus,\n  disabled,\n  accessibleSummary,\n  \"aria-label\": ariaLabel,\n}: DitherChartProps) => {\n  const theme = useTheme();\n  const motion = useMotion();\n  const isInkScreenReaderEnabled = useIsScreenReaderEnabled();\n  const unicodeContext = useUnicode();\n  const { stdout } = useStdout();\n  const [terminalColumns, setTerminalColumns] = React.useState(\n    () => stdout.columns ?? width\n  );\n  React.useEffect(() => {\n    const updateColumns = () => setTerminalColumns(stdout.columns ?? width);\n    updateColumns();\n    stdout.on(\"resize\", updateColumns);\n    return () => {\n      stdout.off(\"resize\", updateColumns);\n    };\n  }, [stdout, width]);\n  const resolvedWidth = Math.min(\n    Math.max(8, Math.floor(width)),\n    Math.max(8, terminalColumns)\n  );\n  const parts = React.useMemo(() => parseParts(children), [children]);\n  const names = React.useMemo(\n    () =>\n      kind === \"pie\"\n        ? data.map((row, index) => String(row[nameKey ?? \"name\"] ?? index))\n        : parts.series.map((series) => series.dataKey),\n    [data, kind, nameKey, parts.series]\n  );\n  const [hoverIndex, setHoverIndex] = React.useState<number | null>(null);\n  const [focusIndex, setFocusIndex] = React.useState<number | null>(null);\n  const [internalSelected, setInternalSelected] = React.useState<string | null>(\n    defaultSelectedDataKey\n  );\n  const [localReplay, setLocalReplay] = React.useState(0);\n  const selected =\n    controlledSelected === undefined ? internalSelected : controlledSelected;\n  const activeIndex = markerIndex ?? hoverIndex;\n  const focusKey = focusIndex === null ? null : (names[focusIndex] ?? null);\n  const selectable =\n    (parts.legend?.isClickable ?? false) ||\n    parts.series.some((series) => series.isClickable);\n  const effectiveScreenReaderMode =\n    screenReaderMode || isInkScreenReaderEnabled;\n  const reduced =\n    reducedMotion ?? (motion.reduced || effectiveScreenReaderMode || !isActive);\n  const useUnicodeGlyphs = unicode ?? unicodeContext;\n  const progress = useEntrance({\n    animate,\n    data,\n    duration: animationDuration,\n    reduced,\n    replayToken: replayToken + localReplay,\n  });\n\n  const moveHover = React.useCallback(\n    (direction: -1 | 1) => {\n      const start = hoverIndex ?? (direction > 0 ? -1 : data.length);\n      const next = clampIndex(start + direction, data.length);\n      setHoverIndex(next);\n      onHoverChange?.(next);\n    },\n    [data.length, hoverIndex, onHoverChange]\n  );\n\n  const moveFocus = React.useCallback(\n    (direction: -1 | 1) => {\n      if (names.length === 0) {\n        return;\n      }\n      setFocusIndex((current) => {\n        if (current === null) {\n          return direction > 0 ? 0 : names.length - 1;\n        }\n        return (current + direction + names.length) % names.length;\n      });\n    },\n    [names.length]\n  );\n\n  const selectFocused = React.useCallback(() => {\n    if (!selectable || names.length === 0) {\n      return;\n    }\n    const key = focusKey ?? names[0] ?? null;\n    const next = selected === key ? null : key;\n    if (controlledSelected === undefined) {\n      setInternalSelected(next);\n    }\n    onSelectionChange?.(next);\n  }, [\n    controlledSelected,\n    focusKey,\n    names,\n    onSelectionChange,\n    selectable,\n    selected,\n  ]);\n\n  useInteraction(\n    (input, key) => {\n      if (key.leftArrow || input === \"h\") {\n        moveHover(-1);\n      } else if (key.rightArrow || input === \"l\") {\n        moveHover(1);\n      } else if (key.upArrow || input === \"k\") {\n        moveFocus(-1);\n      } else if (key.downArrow || input === \"j\") {\n        moveFocus(1);\n      } else if (key.return || input === \" \") {\n        selectFocused();\n      } else if (key.escape) {\n        setHoverIndex(null);\n        setFocusIndex(null);\n        onHoverChange?.(null);\n      } else if (input.toLowerCase() === \"r\" && animate && !reduced) {\n        setLocalReplay((value) => value + 1);\n      }\n    },\n    { autoFocus, disabled, id, isActive: interactive && isActive }\n  );\n\n  const rendered = React.useMemo(\n    () =>\n      renderChart({\n        bloom,\n        bloomOnHover,\n        config,\n        data,\n        focusDataKey: focusKey,\n        grid: parts.grid,\n        height,\n        hovered: hovered || hoverIndex !== null,\n        kind,\n        margins,\n        markerIndex: activeIndex,\n        nameKey,\n        pie:\n          kind === \"pie\" && dataKey\n            ? {\n                innerRadius: Math.max(0, Math.min(0.92, innerRadius)),\n                valueKey: dataKey,\n                variant: parts.pieVariant,\n              }\n            : undefined,\n        progress,\n        selectedDataKey: selected,\n        series: parts.series,\n        stackType,\n        unicode: useUnicodeGlyphs,\n        width: resolvedWidth,\n        xAxis: parts.xAxis,\n        yAxis: parts.yAxis,\n      }),\n    [\n      activeIndex,\n      bloom,\n      bloomOnHover,\n      config,\n      data,\n      dataKey,\n      focusKey,\n      height,\n      hoverIndex,\n      hovered,\n      innerRadius,\n      kind,\n      margins,\n      nameKey,\n      parts,\n      progress,\n      renderChart,\n      selected,\n      stackType,\n      useUnicodeGlyphs,\n      resolvedWidth,\n    ]\n  );\n\n  if (data.length === 0) {\n    return <Text color={theme.colors.mutedForeground}>{emptyMessage}</Text>;\n  }\n\n  const tooltipIndex =\n    activeIndex === null ? null : clampIndex(activeIndex, data.length);\n  const tooltipRow = tooltipIndex === null ? undefined : data[tooltipIndex];\n  const tooltipLabelKey = parts.tooltip?.labelKey ?? parts.xAxis?.dataKey;\n  const tooltipHeading =\n    tooltipRow && tooltipLabelKey\n      ? String(tooltipRow[tooltipLabelKey] ?? \"\")\n      : kind === \"pie\" && tooltipIndex !== null\n        ? names[tooltipIndex]\n        : tooltipIndex === null\n          ? \"\"\n          : String(tooltipIndex);\n  let tooltipNames = names;\n  if (kind === \"pie\" && tooltipIndex !== null) {\n    const pieName = names[tooltipIndex];\n    tooltipNames = pieName ? [pieName] : [];\n  }\n\n  if (effectiveScreenReaderMode) {\n    return (\n      <Box flexDirection=\"column\" aria-role=\"list\">\n        <Text\n          aria-label={\n            ariaLabel ??\n            accessibleSummary ??\n            `${title ?? `${kind} chart`}. ${data.length} data points.`\n          }\n        >\n          {\"\"}\n        </Text>\n        {data.slice(0, 12).map((row, index) => {\n          const heading =\n            kind === \"pie\"\n              ? String(row[nameKey ?? \"name\"] ?? index)\n              : parts.xAxis?.dataKey\n                ? String(row[parts.xAxis.dataKey] ?? index)\n                : String(index);\n          const values =\n            kind === \"pie\" && dataKey\n              ? `${labelOf(config, heading)} ${valueAt(row, dataKey)}`\n              : names\n                  .map(\n                    (name) => `${labelOf(config, name)} ${valueAt(row, name)}`\n                  )\n                  .join(\", \");\n          return (\n            <Box key={`${heading}-${index}`} aria-role=\"listitem\">\n              <Text>{`${heading}: ${values}`}</Text>\n            </Box>\n          );\n        })}\n        {data.length > 12 && (\n          <Text>{`${data.length - 12} additional data points omitted.`}</Text>\n        )}\n      </Box>\n    );\n  }\n\n  return (\n    <Box flexDirection=\"column\" width={resolvedWidth}>\n      {title && (\n        <Text bold color={noColor ? undefined : theme.colors.primary}>\n          {title}\n        </Text>\n      )}\n      {parts.legend && (\n        <Box\n          flexDirection=\"row\"\n          flexWrap=\"wrap\"\n          justifyContent={\n            parts.legend.align === \"left\"\n              ? \"flex-start\"\n              : parts.legend.align === \"right\"\n                ? \"flex-end\"\n                : \"center\"\n          }\n        >\n          {names.map((name, index) => {\n            const isFocused = focusIndex === index;\n            const isSelected = selected === name;\n            return (\n              <Text\n                key={name}\n                bold={isFocused || isSelected}\n                color={noColor ? undefined : colorFor(config, name)}\n                dimColor={selected !== null && !isSelected && !isFocused}\n                inverse={isSelected}\n              >\n                {`${useUnicodeGlyphs ? \"■\" : \"#\"} ${labelOf(config, name)} `}\n              </Text>\n            );\n          })}\n        </Box>\n      )}\n      {rendered.frame.map((row, rowIndex) => (\n        <Text key={`dither-row-${rowIndex}`} wrap=\"truncate-end\">\n          {rowRuns(row, noColor).map((run) => (\n            <Text\n              key={`${rowIndex}-${run.start}`}\n              bold={run.bold}\n              color={run.color}\n              dimColor={run.dim}\n              inverse={run.inverse}\n            >\n              {run.text}\n            </Text>\n          ))}\n        </Text>\n      ))}\n      {parts.tooltip && tooltipRow && tooltipIndex !== null && (\n        <Box\n          borderColor={noColor ? undefined : theme.colors.border}\n          borderStyle={resolveBorderStyle(\n            parts.tooltip.variant === \"frosted-glass\" ? \"round\" : \"single\",\n            useUnicodeGlyphs\n          )}\n          flexDirection=\"column\"\n          paddingX={1}\n        >\n          <Text dimColor>{tooltipHeading}</Text>\n          {tooltipNames.map((name) => {\n            const value =\n              kind === \"pie\" && dataKey\n                ? valueAt(tooltipRow, dataKey)\n                : valueAt(tooltipRow, name);\n            const formatted = parts.tooltip?.valueFormatter\n              ? parts.tooltip.valueFormatter(value, name)\n              : value.toLocaleString();\n            return (\n              <Text\n                key={name}\n                dimColor={selected !== null && selected !== name}\n              >\n                {`${useUnicodeGlyphs ? \"▪\" : \"*\"} ${labelOf(config, name)}  ${formatted}`}\n              </Text>\n            );\n          })}\n        </Box>\n      )}\n      {showControls && interactive && (\n        <Text color={noColor ? undefined : theme.colors.mutedForeground}>\n          {useUnicodeGlyphs\n            ? \"←/→ inspect  ↑/↓ series  enter select  esc clear  r replay\"\n            : \"left/right inspect  up/down series  enter select  esc clear  r replay\"}\n        </Text>\n      )}\n    </Box>\n  );\n};\n",
      "type": "registry:file",
      "target": "components/ui/dither-chart.tsx"
    },
    {
      "path": "registry/ui/dither-radar-chart.tsx",
      "content": "import * as React from \"react\";\n\nimport type {\n  AreaVariant,\n  ChartConfig,\n  ChartMargins,\n  ChartRow,\n} from \"@/lib/dither-chart-utils\";\nimport { renderDitherRadarChart } from \"@/lib/dither-radar-chart-utils\";\n\nimport { DitherChart } from \"./dither-chart\";\nimport type {\n  InteractionOptions,\n  MotionOptions,\n  TerminalChartOptions,\n} from \"./dither-chart\";\n\nexport { Grid, Legend, Tooltip } from \"./dither-chart\";\nexport type {\n  AreaVariant,\n  BloomBlend,\n  BloomConfig,\n  BloomInput,\n  ChartConfig,\n  GridProps,\n  LegendProps,\n  Margins,\n  TooltipProps,\n  TooltipVariant,\n} from \"./dither-chart\";\n\nexport interface RadarProps {\n  dataKey: string;\n  variant?: AreaVariant;\n}\n\nexport type RadarChartProps<TData extends object> = TerminalChartOptions &\n  InteractionOptions &\n  MotionOptions & {\n    children: React.ReactNode;\n    config: ChartConfig;\n    data: TData[];\n    margins?: Partial<ChartMargins>;\n    nameKey: string;\n  };\n\nexport function Radar(_props: RadarProps): null {\n  return null;\n}\nRadar.chartRole = \"radar\" as const;\n\nexport function RadarChart<TData extends object>(\n  props: RadarChartProps<TData>\n) {\n  return (\n    <DitherChart\n      {...props}\n      data={props.data as unknown as ChartRow[]}\n      kind=\"radar\"\n      margins={props.margins}\n      renderChart={renderDitherRadarChart}\n    />\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "data"
  ],
  "type": "registry:ui"
}
