{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "scatter-chart-card",
  "title": "Scatter Chart",
  "description": "Correlation scatter with optional bubble encoding and series average tiles.",
  "dependencies": [
    "recharts@^3.10.1"
  ],
  "registryDependencies": [
    "https://components.exe.xyz/r/chart-card.json"
  ],
  "files": [
    {
      "path": "src/components/charts/scatter-chart-card.tsx",
      "content": "import {\n  CartesianGrid,\n  ResponsiveContainer,\n  Scatter,\n  ScatterChart,\n  Tooltip,\n  XAxis,\n  YAxis,\n  ZAxis,\n} from \"recharts\"\n\nimport {\n  ChartCard,\n  ChartPlot,\n  ChartTiles,\n  axisTick,\n  chartColor,\n  formatNumber,\n  usePrefersReducedMotion,\n} from \"@/components/charts/chart-card\"\n\nexport type ScatterPoint = {\n  x: number\n  y: number\n  z?: number\n  label?: string\n}\n\nexport type ScatterSeries = {\n  label: string\n  color?: string\n  points: ScatterPoint[]\n}\n\nconst DEFAULT_SERIES: ScatterSeries[] = [\n  {\n    label: \"Starter\",\n    points: [\n      { x: 24, y: 200, z: 6, label: \"Northwind\" },\n      { x: 31, y: 220, z: 8, label: \"Birch\" },\n      { x: 38, y: 240, z: 7, label: \"Cove\" },\n      { x: 44, y: 260, z: 9, label: \"Lumen\" },\n      { x: 52, y: 280, z: 5, label: \"Pebble\" },\n      { x: 58, y: 300, z: 10, label: \"Nimbus\" },\n      { x: 66, y: 320, z: 8, label: \"Harbor\" },\n      { x: 74, y: 340, z: 11, label: \"Field\" },\n    ],\n  },\n  {\n    label: \"Growth\",\n    points: [\n      { x: 88, y: 500, z: 14, label: \"Atlas\" },\n      { x: 104, y: 620, z: 16, label: \"Summit\" },\n      { x: 118, y: 700, z: 18, label: \"River\" },\n      { x: 132, y: 760, z: 15, label: \"Canvas\" },\n      { x: 148, y: 820, z: 20, label: \"Forge\" },\n      { x: 162, y: 900, z: 17, label: \"Orbit\" },\n      { x: 176, y: 980, z: 19, label: \"Pike\" },\n      { x: 190, y: 984, z: 22, label: \"Willow\" },\n    ],\n  },\n  {\n    label: \"Scale\",\n    points: [\n      { x: 210, y: 980, z: 28, label: \"Helix\" },\n      { x: 236, y: 1100, z: 32, label: \"Copper\" },\n      { x: 258, y: 1200, z: 30, label: \"Aether\" },\n      { x: 282, y: 1320, z: 36, label: \"Vault\" },\n      { x: 310, y: 1480, z: 40, label: \"Quarry\" },\n      { x: 338, y: 1600, z: 34, label: \"Sable\" },\n      { x: 362, y: 1700, z: 38, label: \"Meridian\" },\n      { x: 388, y: 1724, z: 42, label: \"Ironclad\" },\n    ],\n  },\n]\n\nfunction averageY(points: ScatterPoint[]) {\n  if (points.length === 0) return 0\n  return points.reduce((sum, point) => sum + point.y, 0) / points.length\n}\n\nfunction ScatterTooltip({\n  active,\n  payload,\n  format,\n  formatX,\n  axisLabels,\n}: {\n  active?: boolean\n  payload?: ReadonlyArray<{\n    payload?: ScatterPoint\n    name?: string\n    color?: string\n  }>\n  format: (n: number) => string\n  formatX: (n: number) => string\n  axisLabels: [string, string]\n}) {\n  if (!active || !payload?.length) return null\n  const row = payload[0]\n  const point = row.payload\n  if (!point) return null\n  return (\n    <div className=\"rounded-[2px] border border-border bg-card px-2.5 py-1.5 text-xs shadow-sm\">\n      <p className=\"mb-1 font-mono text-[11px] text-muted-foreground\">\n        {point.label ?? row.name}\n      </p>\n      <ul className=\"flex flex-col gap-0.5\">\n        <li className=\"flex items-center gap-2\">\n          <span\n            className=\"size-1.5 rounded-[1px]\"\n            style={{ background: row.color }}\n          />\n          <span className=\"text-muted-foreground\">{axisLabels[0]}</span>\n          <span className=\"ml-auto font-mono tabular-nums\">\n            {formatX(point.x)}\n          </span>\n        </li>\n        <li className=\"flex items-center gap-2\">\n          <span className=\"size-1.5 rounded-[1px] bg-transparent\" />\n          <span className=\"text-muted-foreground\">{axisLabels[1]}</span>\n          <span className=\"ml-auto font-mono tabular-nums\">\n            {format(point.y)}\n          </span>\n        </li>\n      </ul>\n    </div>\n  )\n}\n\nexport function ScatterChartCard({\n  title = \"Revenue per account\",\n  series = DEFAULT_SERIES,\n  axisLabels = [\"Sessions\", \"Revenue\"],\n  format = (n) => formatNumber(Math.round(n)),\n  formatX = (n) => formatNumber(Math.round(n)),\n  bubble,\n  tiles = true,\n  headline = 742,\n  delta = 6.8,\n  range = \"This quarter\",\n}: {\n  title?: string\n  series?: ScatterSeries[]\n  axisLabels?: [string, string]\n  format?: (n: number) => string\n  formatX?: (n: number) => string\n  bubble?: boolean\n  tiles?: boolean\n  headline?: number\n  delta?: number\n  range?: string\n}) {\n  const reduced = usePrefersReducedMotion()\n  const hasZ = series.some((item) =>\n    item.points.some((point) => point.z != null)\n  )\n  const useBubble = bubble ?? hasZ\n  const averages = series.map((item, index) => ({\n    id: item.label,\n    label: item.label,\n    value: format(averageY(item.points)),\n    color: chartColor(index, item.color),\n  }))\n  const summary = `${title} ${format(headline)}. ${averages\n    .map((item) => `${item.label} average ${item.value}`)\n    .join(\", \")}`\n\n  return (\n    <ChartCard\n      title={title}\n      headline={headline}\n      headlineFormat={format}\n      delta={delta}\n      period={range}\n      summary={summary}\n      tiles={\n        tiles ? (\n          <ChartTiles\n            items={averages.map((item) => ({\n              id: item.id,\n              label: item.label,\n              value: item.value,\n              color: item.color,\n            }))}\n          />\n        ) : undefined\n      }\n    >\n      <ChartPlot height={240} summary={summary}>\n        <ResponsiveContainer width=\"100%\" height=\"100%\">\n          <ScatterChart margin={{ top: 8, right: 8, left: 0, bottom: 8 }}>\n            <CartesianGrid stroke=\"var(--border)\" strokeDasharray=\"1 4\" />\n            <XAxis\n              type=\"number\"\n              dataKey=\"x\"\n              name={axisLabels[0]}\n              tick={axisTick}\n              tickLine={false}\n              axisLine={{ stroke: \"var(--border)\" }}\n              tickFormatter={formatX}\n            />\n            <YAxis\n              type=\"number\"\n              dataKey=\"y\"\n              name={axisLabels[1]}\n              tick={axisTick}\n              tickLine={false}\n              axisLine={false}\n              width={44}\n              tickFormatter={format}\n            />\n            {useBubble ? (\n              <ZAxis type=\"number\" dataKey=\"z\" range={[40, 220]} />\n            ) : null}\n            <Tooltip\n              cursor={{\n                stroke: \"var(--ring)\",\n                strokeDasharray: \"4 4\",\n                strokeWidth: 1,\n              }}\n              content={\n                <ScatterTooltip\n                  format={format}\n                  formatX={formatX}\n                  axisLabels={axisLabels}\n                />\n              }\n            />\n            {series.map((item, index) => (\n              <Scatter\n                key={item.label}\n                name={item.label}\n                data={item.points}\n                fill={chartColor(index, item.color)}\n                isAnimationActive={!reduced}\n              />\n            ))}\n          </ScatterChart>\n        </ResponsiveContainer>\n      </ChartPlot>\n      <table className=\"sr-only\">\n        <caption>{title} points</caption>\n        <thead>\n          <tr>\n            <th>Series</th>\n            <th>Label</th>\n            <th>{axisLabels[0]}</th>\n            <th>{axisLabels[1]}</th>\n          </tr>\n        </thead>\n        <tbody>\n          {series.flatMap((item) =>\n            item.points.map((point, index) => (\n              <tr key={`${item.label}-${point.label ?? index}`}>\n                <td>{item.label}</td>\n                <td>{point.label ?? index + 1}</td>\n                <td>{formatX(point.x)}</td>\n                <td>{format(point.y)}</td>\n              </tr>\n            ))\n          )}\n        </tbody>\n      </table>\n    </ChartCard>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:block"
}