{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "heatmap-chart",
  "title": "Heatmap Chart",
  "description": "Heatmap grid for correlation and intensity data - custom CSS Grid implementation",
  "dependencies": [],
  "registryDependencies": [
    "@boldkit/utils",
    "@boldkit/chart"
  ],
  "files": [
    {
      "path": "registry/default/ui/heatmap-chart.tsx",
      "content": "import * as React from 'react'\nimport { cn } from '@/lib/utils'\nimport { ChartEmpty } from './chart'\n\nexport interface HeatmapCellData {\n  row: string\n  col: string\n  value: number\n}\n\nexport interface HeatmapChartProps extends React.HTMLAttributes<HTMLDivElement> {\n  data: HeatmapCellData[]\n  rows: string[]\n  cols: string[]\n  /** CSS color at 0 intensity (default: transparent primary) */\n  colorLow?: string\n  /** CSS color at max intensity (default: primary) */\n  colorHigh?: string\n  showLabels?: boolean\n  showTooltip?: boolean\n  cellSize?: number\n  /** Accessible label for screen readers (default: \"Heatmap chart\") */\n  ariaLabel?: string\n  /** Fires when a cell is clicked or activated via keyboard (Enter/Space). */\n  onCellClick?: (cell: HeatmapCellData) => void\n  emptyState?: React.ReactNode\n}\n\nfunction interpolateOpacity(value: number, min: number, max: number): number {\n  if (max === min) return 0.5\n  return (value - min) / (max - min)\n}\n\n/**\n * Compute cell background color from colorLow → colorHigh using CSS color-mix(),\n * which works with any valid CSS color string including hsl(var(--*)) tokens.\n * Falls back to primary-opacity when custom colors are not provided.\n */\nfunction getCellColor(\n  intensity: number,\n  colorLow: string | undefined,\n  colorHigh: string | undefined\n): string {\n  if (colorLow && colorHigh) {\n    // color-mix interpolates in srgb space: at t=0 we want colorLow, at t=1 we want colorHigh.\n    // color-mix(in srgb, colorHigh <t*100>%, colorLow) gives us colorHigh at t=1 and colorLow at t=0.\n    const pct = Math.round(Math.max(0, Math.min(1, intensity)) * 100)\n    return `color-mix(in srgb, ${colorHigh} ${pct}%, ${colorLow})`\n  }\n  // Default: vary opacity of primary from 0.08 (low) to 1 (high)\n  return `hsl(var(--primary) / ${Math.max(0.08, intensity)})`\n}\n\nconst HeatmapChart = React.forwardRef<HTMLDivElement, HeatmapChartProps>(\n  (\n    {\n      data,\n      rows,\n      cols,\n      colorLow,\n      colorHigh,\n      showLabels = true,\n      showTooltip = true,\n      cellSize = 40,\n      ariaLabel = 'Heatmap chart',\n      onCellClick,\n      emptyState,\n      className,\n      ...props\n    },\n    ref\n  ) => {\n    const [tooltip, setTooltip] = React.useState<{ x: number; y: number; row: string; col: string; value: number } | null>(null)\n    const isInteractive = !!onCellClick\n    const cellTabIndex = showTooltip || isInteractive ? 0 : undefined\n\n    const valueMap = React.useMemo(() => {\n      const map = new Map<string, number>()\n      data.forEach(d => map.set(`${d.row}__${d.col}`, d.value))\n      return map\n    }, [data])\n\n    const { min, max } = React.useMemo(() => {\n      if (data.length === 0) return { min: 0, max: 1 }\n      // Reduce instead of Math.min(...vals) spread: the spread passes every\n      // value as a function argument and throws RangeError on very large datasets.\n      let min = data[0].value\n      let max = data[0].value\n      for (const d of data) {\n        if (d.value < min) min = d.value\n        if (d.value > max) max = d.value\n      }\n      return { min, max }\n    }, [data])\n\n    const labelWidth = showLabels ? 72 : 8\n    const headerHeight = showLabels ? 32 : 8\n\n    if (!data || data.length === 0) {\n      return <ChartEmpty ref={ref} message={emptyState} className={className} {...props} />\n    }\n\n    return (\n      <div\n        ref={ref}\n        role=\"img\"\n        aria-label={ariaLabel}\n        className={cn('relative w-full overflow-x-auto', className)}\n        {...props}\n      >\n        <div\n          style={{\n            display: 'grid',\n            gridTemplateColumns: `${labelWidth}px repeat(${cols.length}, ${cellSize}px)`,\n            gridTemplateRows: `${headerHeight}px repeat(${rows.length}, ${cellSize}px)`,\n            width: 'fit-content',\n          }}\n        >\n          {/* Top-left empty corner */}\n          <div />\n\n          {/* Column headers */}\n          {cols.map(col =>\n            showLabels ? (\n              <div\n                key={col}\n                className=\"flex items-end justify-center pb-1\"\n                style={{ fontSize: 10, fontFamily: \"'DM Mono', monospace\", fontWeight: 700 }}\n              >\n                <span style={{ transform: 'rotate(-45deg)', transformOrigin: 'bottom center', whiteSpace: 'nowrap' }}>\n                  {col}\n                </span>\n              </div>\n            ) : (\n              <div key={col} />\n            )\n          )}\n\n          {/* Rows */}\n          {rows.map(row => (\n            <React.Fragment key={row}>\n              {/* Row label */}\n              {showLabels ? (\n                <div\n                  className=\"flex items-center pr-2 text-right\"\n                  style={{ fontSize: 10, fontFamily: \"'DM Mono', monospace\", fontWeight: 700, justifyContent: 'flex-end' }}\n                >\n                  {row}\n                </div>\n              ) : (\n                <div />\n              )}\n\n              {/* Cells */}\n              {cols.map(col => {\n                const value = valueMap.get(`${row}__${col}`) ?? 0\n                const intensity = interpolateOpacity(value, min, max)\n\n                return (\n                  <div\n                    key={col}\n                    role={isInteractive ? 'button' : undefined}\n                    aria-label={isInteractive ? `${row}, ${col}: ${value}` : undefined}\n                    tabIndex={cellTabIndex}\n                    className={cn(\n                      'border border-foreground/30 transition duration-100 hover:border-foreground hover:border-2 hover:z-10 focus:border-foreground focus:border-2 focus:z-10 focus:outline-none',\n                      isInteractive ? 'cursor-pointer' : 'cursor-default'\n                    )}\n                    style={{\n                      backgroundColor: getCellColor(intensity, colorLow, colorHigh),\n                    }}\n                    onMouseEnter={(e) => {\n                      if (showTooltip) {\n                        const rect = e.currentTarget.getBoundingClientRect()\n                        setTooltip({ x: rect.left + rect.width / 2, y: rect.top, row, col, value })\n                      }\n                    }}\n                    onMouseLeave={() => setTooltip(null)}\n                    onFocus={(e) => {\n                      if (showTooltip) {\n                        const rect = e.currentTarget.getBoundingClientRect()\n                        setTooltip({ x: rect.left + rect.width / 2, y: rect.top, row, col, value })\n                      }\n                    }}\n                    onBlur={() => setTooltip(null)}\n                    onClick={onCellClick ? () => onCellClick({ row, col, value }) : undefined}\n                    onKeyDown={\n                      onCellClick\n                        ? (e) => {\n                            if (e.key === 'Enter' || e.key === ' ') {\n                              e.preventDefault()\n                              onCellClick({ row, col, value })\n                            }\n                          }\n                        : undefined\n                    }\n                  />\n                )\n              })}\n            </React.Fragment>\n          ))}\n        </div>\n\n        {/* Tooltip */}\n        {showTooltip && tooltip && (\n          <div\n            className=\"fixed z-50 pointer-events-none border-3 border-foreground bg-background px-3 py-2 text-xs font-mono shadow-[4px_4px_0px_hsl(var(--foreground))]\"\n            style={{\n              left: Math.min(\n                Math.max(tooltip.x, 100),\n                (typeof window !== 'undefined' ? window.innerWidth : 1024) - 100\n              ),\n              top: Math.min(\n                Math.max(tooltip.y - 64 < 0 ? tooltip.y + 10 : tooltip.y - 64, 10),\n                (typeof window !== 'undefined' ? window.innerHeight : 768) - 60\n              ),\n              transform: 'translateX(-50%)',\n              maxWidth: 200,\n            }}\n          >\n            <p className=\"font-black\">{tooltip.row} × {tooltip.col}</p>\n            <p className=\"text-muted-foreground\">{tooltip.value}</p>\n          </div>\n        )}\n      </div>\n    )\n  }\n)\nHeatmapChart.displayName = 'HeatmapChart'\n\nexport { HeatmapChart }\n",
      "type": "registry:ui",
      "target": "components/ui/heatmap-chart.tsx"
    }
  ],
  "type": "registry:ui"
}