{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "heatmap-chart",
  "type": "registry:component",
  "title": "Heatmap Chart",
  "description": "A contribution heatmap with animated cells and configurable level colors",
  "dependencies": [
    "@visx/group@4.0.1-alpha.0",
    "@visx/heatmap@4.0.1-alpha.0",
    "@visx/pattern@4.0.1-alpha.0",
    "@visx/responsive@4.0.1-alpha.0",
    "@visx/scale@4.0.1-alpha.0",
    "motion"
  ],
  "registryDependencies": [
    "@bklit/chart-context",
    "@bklit/chart-animation",
    "@bklit/utils",
    "@bklit/chart-utils",
    "@bklit/chart-tooltip",
    "@bklit/background",
    "@bklit/shimmering-text"
  ],
  "files": [
    {
      "path": "src/charts/heatmap/heatmap-chart.tsx",
      "content": "\"use client\";\n\nimport { ParentSize } from \"@visx/responsive\";\nimport { scaleLinear, scaleTime } from \"@visx/scale\";\nimport type { Transition } from \"motion/react\";\nimport { useReducedMotion } from \"motion/react\";\nimport {\n  type ReactNode,\n  useCallback,\n  useEffect,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport type { Margin } from \"../chart-context\";\nimport { ChartLoadingLabel } from \"../chart-loading-label\";\nimport {\n  type ChartPhase,\n  type ChartStatus,\n  DEFAULT_CHART_STATUS,\n  resolveRestingChartPhase,\n} from \"../chart-phase\";\nimport {\n  HEATMAP_DEFAULT_ENTER_DURATION_MS,\n  HEATMAP_DEFAULT_ENTER_TRANSITION,\n  HEATMAP_DEFAULT_LOADING_CELL_MAX_OPACITY,\n  HEATMAP_DEFAULT_LOADING_CELL_RANDOMNESS,\n  HEATMAP_LOADING_CHART_OPACITY,\n  HEATMAP_LOADING_CONCEAL_MS,\n} from \"./heatmap-animation\";\nimport type { HeatmapLevelColors, HeatmapLevelStyles } from \"./heatmap-colors\";\nimport {\n  buildHeatmapColorScaleFromStyles,\n  buildHeatmapFillScale,\n  resolveHeatmapLevelStyles,\n} from \"./heatmap-colors\";\nimport {\n  type HeatmapColumn,\n  type HeatmapContextValue,\n  HeatmapInteractionRoot,\n  HeatmapProvider,\n  type HeatmapRevealMode,\n  useHeatmap,\n  useHeatmapInteraction,\n} from \"./heatmap-context\";\nimport { HeatmapPatternDefs } from \"./heatmap-pattern-defs\";\nimport { resolveHeatmapSeparatorConfigWithData } from \"./heatmap-resolve-separator\";\nimport {\n  filterHeatmapColumns,\n  getHeatmapColumnXOffset,\n  getHeatmapPlotInnerWidth,\n  getHeatmapSeparatorCount,\n  getHeatmapTimeExtent,\n  type HeatmapSeparatorLayout,\n  type HeatmapSeparatorParsedConfig,\n  type HeatmapWeekStartDay,\n  rotateHeatmapColumnBins,\n} from \"./heatmap-utils\";\n\nexport type HeatmapLayout = \"fluid\" | \"fill\";\n\nexport interface HeatmapChartProps {\n  /** Column data — one entry per week (or category) with row bins inside. */\n  data: HeatmapColumn[];\n  /** Visible time range — filters week columns that overlap the domain. */\n  xDomain?: [Date, Date];\n  /**\n   * Week columns used for cell sizing when `xDomain` is set. Keeps bin height\n   * stable while scrubbing (filtered columns can vary by ±1 at boundaries).\n   */\n  sizingColumnCount?: number;\n  /**\n   * `fluid` — width drives square cells; chart height hugs the grid (GitHub-style).\n   * `fill` — expands cells to fill the parent.\n   */\n  layout?: HeatmapLayout;\n  /** Chart margins */\n  margin?: Partial<Margin>;\n  /** Fixed cell size in pixels. When 0, cells are square and sized to fit the plot. Default: 0 */\n  binSize?: number;\n  /** Gap between cells in pixels. Default: 2 */\n  gap?: number;\n  /** Override the default color scale. */\n  colorScale?: (count: number | null | undefined) => string;\n  /** Per-level colors for the Less → More scale (5 entries). */\n  levelColors?: HeatmapLevelColors;\n  /** Per-level fill styles (color and optional pattern). Takes precedence over `levelColors`. */\n  levelStyles?: HeatmapLevelStyles;\n  /** Optional outer aspect ratio (e.g. \"2 / 1\"). Omit to fill the parent. */\n  aspectRatio?: string;\n  /** Additional class name for the container */\n  className?: string;\n  /** Fetch / display status. Default: `\"ready\"`. */\n  status?: ChartStatus;\n  /** Centered label while loading. */\n  loadingLabel?: string;\n  /** Enter animation duration in milliseconds. Default: 1600 */\n  animationDuration?: number;\n  /** Motion enter transition (spring or cubic-bezier tween). */\n  enterTransition?: Transition;\n  /** Signature of motion URL state — triggers enter replay when it changes. */\n  revealSignature?: string;\n  /** Scales wave stagger delays (1 = default). */\n  enterStaggerScale?: number;\n  /** Play enter fade-in / loading shimmer animations. Default: true */\n  animate?: boolean;\n  /** Chart opacity while loading. Default: 0.5 */\n  loadingOpacity?: number;\n  /** Show loading cell shimmer while loading. Default: true */\n  showLoadingCells?: boolean;\n  /** Max opacity loading cells animate toward (0–1). Default: 0.5 */\n  loadingCellMaxOpacity?: number;\n  /** Share of cells that participate in loading shimmer (0–1). Default: 0.65 */\n  loadingCellRandomness?: number;\n  /**\n   * Inserts horizontal gaps between column groups. Overridden when a\n   * {@link HeatmapSeparator} child sets `every` / `spacing`.\n   */\n  columnSeparators?: HeatmapSeparatorParsedConfig;\n  /**\n   * First row of the grid — `0` = Sunday (GitHub default), `1` = Monday, etc.\n   * Rotates column bins for display without reshaping source data.\n   */\n  weekStartDay?: HeatmapWeekStartDay;\n  /** Child components (HeatmapCells, HeatmapXAxis, HeatmapYAxis) */\n  children: ReactNode;\n}\n\nconst DEFAULT_MARGIN: Margin = { top: 28, right: 16, bottom: 0, left: 40 };\n\nfunction computeHeatmapDimensions({\n  width,\n  parentHeight,\n  margin,\n  columnCount,\n  rowCount,\n  binSize,\n  layout,\n  separator,\n}: {\n  width: number;\n  parentHeight: number;\n  margin: Margin;\n  columnCount: number;\n  rowCount: number;\n  binSize: number;\n  layout: HeatmapLayout;\n  separator: Pick<HeatmapSeparatorLayout, \"spacing\" | \"atColumns\"> | null;\n}) {\n  const innerWidth = Math.max(width - margin.left - margin.right, 0);\n  const availableHeight = Math.max(\n    parentHeight - margin.top - margin.bottom,\n    0\n  );\n  const separatorCount = separator ? getHeatmapSeparatorCount(separator) : 0;\n  const totalSpacing = separatorCount * (separator?.spacing ?? 0);\n\n  let binWidth: number;\n  let binHeight: number;\n\n  if (binSize > 0) {\n    binWidth = binSize;\n    binHeight = binSize;\n  } else if (layout === \"fluid\") {\n    const cellSize = Math.max((innerWidth - totalSpacing) / columnCount, 0);\n    binWidth = cellSize;\n    binHeight = cellSize;\n  } else {\n    const cellSize = Math.min(\n      Math.max((innerWidth - totalSpacing) / columnCount, 0),\n      availableHeight / rowCount\n    );\n    binWidth = cellSize;\n    binHeight = cellSize;\n  }\n\n  const plotInnerWidth = getHeatmapPlotInnerWidth(\n    columnCount,\n    binWidth,\n    separator\n  );\n  const innerHeight = rowCount * binHeight;\n  const height =\n    layout === \"fluid\"\n      ? margin.top + innerHeight + margin.bottom\n      : Math.max(parentHeight, margin.top + innerHeight + margin.bottom);\n  const chartWidth =\n    binSize > 0 && layout === \"fluid\"\n      ? margin.left + plotInnerWidth + margin.right\n      : width;\n\n  return {\n    binWidth,\n    binHeight,\n    innerWidth: plotInnerWidth,\n    innerHeight,\n    height,\n    width: chartWidth,\n  };\n}\n\ninterface HeatmapChartInnerProps {\n  width: number;\n  height: number;\n  data: HeatmapColumn[];\n  xDomain?: [Date, Date];\n  sizingColumnCount?: number;\n  margin: Margin;\n  binSize: number;\n  gap: number;\n  layout: HeatmapLayout;\n  colorScale: (count: number | null | undefined) => string;\n  fillScale: (count: number | null | undefined) => string;\n  levelStyles: HeatmapLevelStyles;\n  chartStatus: ChartStatus;\n  chartPhase: ChartPhase;\n  isLoaded: boolean;\n  revealEpoch: number;\n  animationDuration: number;\n  enterTransition?: Transition;\n  enterStaggerScale: number;\n  animateCells: boolean;\n  loadingOpacity: number;\n  showLoadingCells: boolean;\n  loadingCellMaxOpacity: number;\n  loadingCellRandomness: number;\n  revealMode: HeatmapRevealMode;\n  loadingLabel?: string;\n  showLoadingLabel: boolean;\n  columnSeparators?: HeatmapSeparatorParsedConfig;\n  weekStartDay: HeatmapWeekStartDay;\n  children: ReactNode;\n}\n\nfunction HeatmapChartInner({\n  width,\n  height: parentHeight,\n  data,\n  xDomain,\n  sizingColumnCount: sizingColumnCountProp,\n  margin,\n  binSize,\n  gap,\n  layout,\n  colorScale,\n  fillScale,\n  levelStyles,\n  chartStatus,\n  chartPhase,\n  isLoaded,\n  revealEpoch,\n  animationDuration,\n  enterTransition,\n  enterStaggerScale,\n  animateCells,\n  loadingOpacity,\n  showLoadingCells,\n  loadingCellMaxOpacity,\n  loadingCellRandomness,\n  revealMode,\n  loadingLabel,\n  showLoadingLabel,\n  columnSeparators,\n  weekStartDay,\n  children,\n}: HeatmapChartInnerProps) {\n  const containerRef = useRef<HTMLDivElement>(null);\n\n  const filteredData = useMemo(\n    () => filterHeatmapColumns(data, xDomain),\n    [data, xDomain]\n  );\n\n  const visibleData = useMemo(\n    () => rotateHeatmapColumnBins(filteredData, weekStartDay),\n    [filteredData, weekStartDay]\n  );\n\n  const visibleColumnCount = Math.max(visibleData.length, 1);\n  const columnCount =\n    xDomain && sizingColumnCountProp != null\n      ? Math.max(sizingColumnCountProp, 1)\n      : visibleColumnCount;\n  const rowCount = Math.max(visibleData[0]?.bins.length ?? 7, 1);\n\n  const separatorLayout = useMemo(\n    () =>\n      resolveHeatmapSeparatorConfigWithData(\n        children,\n        visibleData,\n        columnSeparators\n      ),\n    [children, columnSeparators, visibleData]\n  );\n\n  const {\n    binWidth,\n    binHeight,\n    innerWidth,\n    innerHeight,\n    height,\n    width: chartWidth,\n  } = useMemo(\n    () =>\n      computeHeatmapDimensions({\n        width,\n        parentHeight,\n        margin,\n        columnCount,\n        rowCount,\n        binSize,\n        layout,\n        separator: separatorLayout,\n      }),\n    [\n      binSize,\n      columnCount,\n      layout,\n      margin,\n      parentHeight,\n      rowCount,\n      separatorLayout,\n      width,\n    ]\n  );\n\n  const xScale = useMemo(\n    () => (columnIndex: number) =>\n      columnIndex * binWidth +\n      getHeatmapColumnXOffset(columnIndex, separatorLayout),\n    [binWidth, separatorLayout]\n  );\n  const yScale = useMemo(\n    () => (rowIndex: number) => rowIndex * binHeight,\n    [binHeight]\n  );\n\n  const timeExtent = useMemo(() => getHeatmapTimeExtent(data), [data]);\n\n  const timeXScale = useMemo(() => {\n    const domain = timeExtent ?? [new Date(), new Date()];\n    return scaleTime({\n      domain,\n      range: [0, innerWidth],\n    });\n  }, [innerWidth, timeExtent]);\n\n  const brushYScale = useMemo(\n    () =>\n      scaleLinear({\n        domain: [0, 1],\n        range: [innerHeight, 0],\n      }),\n    [innerHeight]\n  );\n\n  const contextValue = useMemo<HeatmapContextValue>(\n    () => ({\n      data: visibleData,\n      width: chartWidth,\n      height,\n      innerWidth,\n      innerHeight,\n      margin,\n      binWidth,\n      binHeight,\n      gap,\n      weekStartDay,\n      xScale,\n      yScale,\n      separatorLayout,\n      timeXScale,\n      brushYScale,\n      isReady: chartWidth >= 10 && height >= 10,\n      colorScale,\n      fillScale,\n      levelStyles,\n      containerRef,\n      chartStatus,\n      chartPhase,\n      isLoaded,\n      revealEpoch,\n      animationDuration,\n      enterTransition,\n      enterStaggerScale,\n      animateCells,\n      loadingOpacity,\n      showLoadingCells,\n      loadingCellMaxOpacity,\n      loadingCellRandomness,\n      revealMode,\n      loadingLabel,\n      showLoadingLabel,\n    }),\n    [\n      animateCells,\n      animationDuration,\n      binHeight,\n      binWidth,\n      brushYScale,\n      chartPhase,\n      chartStatus,\n      chartWidth,\n      colorScale,\n      fillScale,\n      enterStaggerScale,\n      enterTransition,\n      gap,\n      height,\n      innerHeight,\n      innerWidth,\n      isLoaded,\n      loadingCellMaxOpacity,\n      loadingCellRandomness,\n      loadingLabel,\n      levelStyles,\n      loadingOpacity,\n      margin,\n      revealMode,\n      separatorLayout,\n      showLoadingCells,\n      showLoadingLabel,\n      revealEpoch,\n      timeXScale,\n      visibleData,\n      weekStartDay,\n      xScale,\n      yScale,\n    ]\n  );\n\n  if (chartWidth < 10 || height < 10) {\n    return null;\n  }\n\n  return (\n    <HeatmapProvider value={contextValue}>\n      <HeatmapInteractionRoot>\n        <HeatmapChartSurface layout={layout}>{children}</HeatmapChartSurface>\n      </HeatmapInteractionRoot>\n    </HeatmapProvider>\n  );\n}\n\nfunction HeatmapChartSurface({\n  layout,\n  children,\n}: {\n  layout: HeatmapLayout;\n  children: ReactNode;\n}) {\n  const {\n    containerRef,\n    height,\n    width,\n    margin,\n    levelStyles,\n    chartPhase,\n    loadingOpacity,\n    loadingLabel,\n    showLoadingLabel,\n  } = useHeatmap();\n  const { clearInteraction } = useHeatmapInteraction();\n  const reducedOpacity =\n    chartPhase === \"loading\" || chartPhase === \"exitingReady\"\n      ? loadingOpacity\n      : 1;\n\n  return (\n    <div\n      className={cn(\"relative w-full\", layout === \"fill\" && \"h-full\")}\n      onPointerLeave={clearInteraction}\n      ref={containerRef}\n      style={{ opacity: reducedOpacity }}\n    >\n      <svg aria-hidden=\"true\" height={height} width={width}>\n        <HeatmapPatternDefs levelStyles={levelStyles} />\n        <g transform={`translate(${margin.left},${margin.top})`}>{children}</g>\n      </svg>\n      {showLoadingLabel && loadingLabel?.trim() ? (\n        <div\n          className=\"pointer-events-none absolute\"\n          style={{\n            top: margin.top,\n            left: margin.left,\n            width: Math.max(width - margin.left - margin.right, 0),\n            height: Math.max(height - margin.top - margin.bottom, 0),\n          }}\n        >\n          <ChartLoadingLabel\n            exiting={chartPhase !== \"loading\"}\n            text={loadingLabel}\n          />\n        </div>\n      ) : null}\n    </div>\n  );\n}\n\nfunction useHeatmapChartLifecycle({\n  chartStatus,\n  animationDuration,\n  revealSignature = \"\",\n  animate,\n}: {\n  chartStatus: ChartStatus;\n  animationDuration: number;\n  revealSignature?: string;\n  animate: boolean;\n}) {\n  const reducedMotion = useReducedMotion();\n  const [chartPhase, setChartPhase] = useState<ChartPhase>(() =>\n    resolveRestingChartPhase(chartStatus)\n  );\n  const [isLoaded, setIsLoaded] = useState(\n    () => chartStatus === \"ready\" && (!animate || animationDuration <= 0)\n  );\n  const [revealEpoch, setRevealEpoch] = useState(0);\n  const [revealMode, setRevealMode] = useState<HeatmapRevealMode>(null);\n  const prevStatusRef = useRef(chartStatus);\n  const phaseRef = useRef(chartPhase);\n  phaseRef.current = chartPhase;\n\n  const animateCells = animate && !reducedMotion;\n  const animateEnter = animateCells && animationDuration > 0;\n\n  const finishReveal = useCallback(() => {\n    setIsLoaded(true);\n    setChartPhase(\"ready\");\n    setRevealMode(null);\n  }, []);\n\n  const beginReveal = useCallback(() => {\n    setRevealMode(\"enter\");\n    setRevealEpoch((epoch) => epoch + 1);\n    setIsLoaded(false);\n    setChartPhase(\"revealing\");\n    if (!animateEnter) {\n      finishReveal();\n    }\n  }, [animateEnter, finishReveal]);\n\n  useLayoutEffect(() => {\n    const prevStatus = prevStatusRef.current;\n    if (prevStatus === chartStatus) {\n      return;\n    }\n    prevStatusRef.current = chartStatus;\n\n    if (chartStatus === \"ready\" && prevStatus === \"loading\") {\n      setRevealMode(\"fromLoading\");\n      setIsLoaded(false);\n      setChartPhase(\"revealing\");\n      return;\n    }\n\n    if (chartStatus === \"loading\" && prevStatus === \"ready\") {\n      setRevealMode(null);\n      setIsLoaded(false);\n      setChartPhase(\"exitingReady\");\n    }\n  }, [chartStatus]);\n\n  useEffect(() => {\n    if (chartPhase !== \"exitingReady\") {\n      return;\n    }\n\n    const timer = window.setTimeout(() => {\n      setChartPhase(\"loading\");\n    }, HEATMAP_LOADING_CONCEAL_MS);\n\n    return () => window.clearTimeout(timer);\n  }, [chartPhase]);\n\n  // biome-ignore lint/correctness/useExhaustiveDependencies: revealSignature replays enter\n  useEffect(() => {\n    if (!animateEnter) {\n      setIsLoaded(true);\n      setChartPhase(resolveRestingChartPhase(chartStatus));\n      return;\n    }\n    if (chartStatus !== \"ready\") {\n      return;\n    }\n    if (phaseRef.current !== \"ready\") {\n      return;\n    }\n\n    beginReveal();\n  }, [\n    animateEnter,\n    animationDuration,\n    beginReveal,\n    chartStatus,\n    revealSignature,\n  ]);\n\n  // biome-ignore lint/correctness/useExhaustiveDependencies: revealEpoch replays finish timer\n  useEffect(() => {\n    if (!animateEnter || chartPhase !== \"revealing\") {\n      return;\n    }\n\n    const finishTimer = window.setTimeout(finishReveal, animationDuration);\n    return () => window.clearTimeout(finishTimer);\n  }, [animateEnter, animationDuration, chartPhase, finishReveal, revealEpoch]);\n\n  return {\n    chartPhase,\n    isLoaded,\n    revealEpoch,\n    revealMode,\n    animateCells,\n  };\n}\n\nexport function HeatmapChart({\n  data,\n  xDomain,\n  sizingColumnCount,\n  layout = \"fluid\",\n  margin: marginProp,\n  binSize = 0,\n  gap = 2,\n  colorScale: colorScaleProp,\n  levelColors,\n  levelStyles: levelStylesProp,\n  aspectRatio,\n  className = \"\",\n  status = DEFAULT_CHART_STATUS,\n  loadingLabel,\n  animationDuration = HEATMAP_DEFAULT_ENTER_DURATION_MS,\n  enterTransition = HEATMAP_DEFAULT_ENTER_TRANSITION,\n  revealSignature = \"\",\n  enterStaggerScale = 1,\n  animate = true,\n  loadingOpacity = HEATMAP_LOADING_CHART_OPACITY,\n  showLoadingCells = true,\n  loadingCellMaxOpacity = HEATMAP_DEFAULT_LOADING_CELL_MAX_OPACITY,\n  loadingCellRandomness = HEATMAP_DEFAULT_LOADING_CELL_RANDOMNESS,\n  columnSeparators,\n  weekStartDay = 0,\n  children,\n}: HeatmapChartProps) {\n  const margin = { ...DEFAULT_MARGIN, ...marginProp };\n  const levelStyles = useMemo(\n    () => resolveHeatmapLevelStyles(levelColors, levelStylesProp),\n    [levelColors, levelStylesProp]\n  );\n  const colorScale = useMemo(\n    () => colorScaleProp ?? buildHeatmapColorScaleFromStyles(levelStyles),\n    [colorScaleProp, levelStyles]\n  );\n  const fillScale = useMemo(\n    () => buildHeatmapFillScale(levelStyles),\n    [levelStyles]\n  );\n\n  const { chartPhase, isLoaded, revealEpoch, revealMode, animateCells } =\n    useHeatmapChartLifecycle({\n      chartStatus: status,\n      animationDuration,\n      revealSignature,\n      animate,\n    });\n\n  const showLoadingLabel = Boolean(\n    loadingLabel?.trim() &&\n      status === \"loading\" &&\n      (chartPhase === \"loading\" || chartPhase === \"exitingReady\")\n  );\n\n  return (\n    <div\n      className={cn(\n        \"relative w-full\",\n        layout === \"fill\" && \"h-full min-h-0\",\n        className\n      )}\n      style={aspectRatio ? { aspectRatio } : undefined}\n    >\n      <ParentSize>\n        {({ width, height: parentHeight }) => (\n          <HeatmapChartInner\n            animateCells={animateCells}\n            animationDuration={animationDuration}\n            binSize={binSize}\n            chartPhase={chartPhase}\n            chartStatus={status}\n            colorScale={colorScale}\n            columnSeparators={columnSeparators}\n            data={data}\n            enterStaggerScale={enterStaggerScale}\n            enterTransition={enterTransition}\n            fillScale={fillScale}\n            gap={gap}\n            height={parentHeight}\n            isLoaded={isLoaded}\n            layout={layout}\n            levelStyles={levelStyles}\n            loadingCellMaxOpacity={loadingCellMaxOpacity}\n            loadingCellRandomness={loadingCellRandomness}\n            loadingLabel={loadingLabel}\n            loadingOpacity={loadingOpacity}\n            margin={margin}\n            revealEpoch={revealEpoch}\n            revealMode={revealMode}\n            showLoadingCells={showLoadingCells}\n            showLoadingLabel={showLoadingLabel}\n            sizingColumnCount={sizingColumnCount}\n            weekStartDay={weekStartDay}\n            width={width}\n            xDomain={xDomain}\n          >\n            {children}\n          </HeatmapChartInner>\n        )}\n      </ParentSize>\n    </div>\n  );\n}\n\nHeatmapChart.displayName = \"HeatmapChart\";\n\nexport default HeatmapChart;\n",
      "type": "registry:component",
      "target": "components/charts/heatmap/heatmap-chart.tsx"
    },
    {
      "path": "src/charts/heatmap/heatmap-context.tsx",
      "content": "\"use client\";\n\nimport type { scaleLinear, scaleTime } from \"@visx/scale\";\nimport type { Transition } from \"motion/react\";\nimport {\n  createContext,\n  type Dispatch,\n  type ReactNode,\n  type RefObject,\n  type SetStateAction,\n  useCallback,\n  useContext,\n  useMemo,\n  useState,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport type { Margin } from \"../chart-context\";\nimport type { ChartPhase, ChartStatus } from \"../chart-phase\";\nimport {\n  HEATMAP_DEFAULT_LEVEL_COLORS,\n  type HeatmapLevelStyles,\n} from \"./heatmap-colors\";\nimport type {\n  HeatmapSeparatorLayout,\n  HeatmapWeekStartDay,\n} from \"./heatmap-utils\";\n\ntype HeatmapTimeScale = ReturnType<typeof scaleTime<number>>;\ntype HeatmapLinearScale = ReturnType<typeof scaleLinear<number>>;\n\nexport interface HeatmapBin {\n  count: number;\n  bin: number;\n  date: Date;\n}\n\nexport interface HeatmapColumn {\n  bin: number;\n  bins: HeatmapBin[];\n}\n\nexport interface HeatmapTooltipData {\n  column: number;\n  row: number;\n  count: number;\n  date: Date;\n  x: number;\n  y: number;\n}\n\nexport interface HeatmapInteractionContextValue {\n  hoveredCell: { column: number; row: number } | null;\n  hoveredLegendLevel: number | null;\n  tooltipData: HeatmapTooltipData | null;\n  setHoveredCell: (cell: { column: number; row: number } | null) => void;\n  setHoveredLegendLevel: (level: number | null) => void;\n  setTooltipData: Dispatch<SetStateAction<HeatmapTooltipData | null>>;\n  clearInteraction: () => void;\n}\n\nexport interface HeatmapContextValue {\n  data: HeatmapColumn[];\n  width: number;\n  height: number;\n  innerWidth: number;\n  innerHeight: number;\n  margin: Margin;\n  binWidth: number;\n  binHeight: number;\n  gap: number;\n  weekStartDay: HeatmapWeekStartDay;\n  xScale: (columnIndex: number) => number;\n  yScale: (rowIndex: number) => number;\n  separatorLayout: HeatmapSeparatorLayout | null;\n  timeXScale: HeatmapTimeScale;\n  brushYScale: HeatmapLinearScale;\n  isReady: boolean;\n  colorScale: (count: number | null | undefined) => string;\n  fillScale: (count: number | null | undefined) => string;\n  levelStyles: HeatmapLevelStyles;\n  containerRef: RefObject<HTMLDivElement | null>;\n  chartStatus: ChartStatus;\n  chartPhase: ChartPhase;\n  isLoaded: boolean;\n  revealEpoch: number;\n  animationDuration: number;\n  enterTransition?: Transition;\n  enterStaggerScale: number;\n  animateCells: boolean;\n  loadingOpacity: number;\n  showLoadingCells: boolean;\n  loadingCellMaxOpacity: number;\n  loadingCellRandomness: number;\n  revealMode: HeatmapRevealMode;\n  loadingLabel?: string;\n  showLoadingLabel: boolean;\n}\n\n/** How cells transition into view after loading or on first mount. */\nexport type HeatmapRevealMode = \"enter\" | \"fromLoading\" | null;\n\nconst HeatmapContext = createContext<HeatmapContextValue | null>(null);\nconst HeatmapInteractionContext =\n  createContext<HeatmapInteractionContextValue | null>(null);\n\nexport function HeatmapInteractionProvider({\n  children,\n}: {\n  children: ReactNode;\n}) {\n  const [hoveredCell, setHoveredCell] = useState<{\n    column: number;\n    row: number;\n  } | null>(null);\n  const [hoveredLegendLevel, setHoveredLegendLevel] = useState<number | null>(\n    null\n  );\n  const [tooltipData, setTooltipData] = useState<HeatmapTooltipData | null>(\n    null\n  );\n\n  const clearInteraction = useCallback(() => {\n    setHoveredCell(null);\n    setHoveredLegendLevel(null);\n    setTooltipData(null);\n  }, []);\n\n  const value = useMemo<HeatmapInteractionContextValue>(\n    () => ({\n      hoveredCell,\n      hoveredLegendLevel,\n      tooltipData,\n      setHoveredCell,\n      setHoveredLegendLevel,\n      setTooltipData,\n      clearInteraction,\n    }),\n    [clearInteraction, hoveredCell, hoveredLegendLevel, tooltipData]\n  );\n\n  return (\n    <HeatmapInteractionContext.Provider value={value}>\n      {children}\n    </HeatmapInteractionContext.Provider>\n  );\n}\n\nexport function HeatmapProvider({\n  children,\n  value,\n}: {\n  children: ReactNode;\n  value: HeatmapContextValue;\n}) {\n  return (\n    <HeatmapContext.Provider value={value}>{children}</HeatmapContext.Provider>\n  );\n}\n\nexport function useHeatmap(): HeatmapContextValue {\n  const context = useContext(HeatmapContext);\n  if (!context) {\n    throw new Error(\"useHeatmap must be used within a HeatmapProvider\");\n  }\n  return context;\n}\n\nexport function useHeatmapInteraction(): HeatmapInteractionContextValue {\n  const context = useContext(HeatmapInteractionContext);\n  if (!context) {\n    throw new Error(\n      \"useHeatmapInteraction must be used within a HeatmapInteractionProvider\"\n    );\n  }\n  return context;\n}\n\nexport function useHeatmapInteractionOptional(): HeatmapInteractionContextValue | null {\n  return useContext(HeatmapInteractionContext);\n}\n\n/** Clears hover state when the pointer leaves chart + legend. */\nexport function HeatmapInteractionBoundary({\n  children,\n  className,\n}: {\n  children: ReactNode;\n  className?: string;\n}) {\n  const { clearInteraction } = useHeatmapInteraction();\n\n  return (\n    <div\n      className={cn(\"size-full min-h-0 min-w-0\", className)}\n      onPointerLeave={clearInteraction}\n    >\n      {children}\n    </div>\n  );\n}\n\n/** Nests a provider only when one is not already present upstream. */\nexport function HeatmapInteractionRoot({ children }: { children: ReactNode }) {\n  const existing = useContext(HeatmapInteractionContext);\n  if (existing) {\n    return children;\n  }\n  return <HeatmapInteractionProvider>{children}</HeatmapInteractionProvider>;\n}\n\n/** @deprecated Use {@link HEATMAP_DEFAULT_LEVEL_COLORS} */\nexport const heatmapCssVars = {\n  empty: HEATMAP_DEFAULT_LEVEL_COLORS[0],\n  level1: HEATMAP_DEFAULT_LEVEL_COLORS[1],\n  level2: HEATMAP_DEFAULT_LEVEL_COLORS[2],\n  level3: HEATMAP_DEFAULT_LEVEL_COLORS[3],\n  level4: HEATMAP_DEFAULT_LEVEL_COLORS[4],\n} as const;\n",
      "type": "registry:component",
      "target": "components/charts/heatmap/heatmap-context.tsx"
    },
    {
      "path": "src/charts/heatmap/heatmap-cells.tsx",
      "content": "\"use client\";\n\nimport { Group } from \"@visx/group\";\nimport { HeatmapRect } from \"@visx/heatmap\";\nimport {\n  animate,\n  type MotionValue,\n  motion,\n  type Transition,\n  useMotionValue,\n} from \"motion/react\";\nimport { memo, useCallback, useEffect, useMemo, useRef } from \"react\";\nimport type { ChartStatus } from \"../chart-phase\";\nimport { transitionWithDelay } from \"../motion-utils\";\nimport {\n  computeHeatmapEnterFadeDelayMs,\n  HEATMAP_DEFAULT_ENTER_EASE,\n  HEATMAP_LOADING_BASE_CELL_OPACITY,\n  HEATMAP_LOADING_CONCEAL_MS,\n  heatmapLoadingCellParticipates,\n  resolveHeatmapEnterFadeDurationSec,\n} from \"./heatmap-animation\";\nimport { heatmapLevelCellFillOpacity } from \"./heatmap-colors\";\nimport {\n  type HeatmapBin,\n  type HeatmapColumn,\n  useHeatmap,\n  useHeatmapInteraction,\n} from \"./heatmap-context\";\nimport {\n  getHeatmapContributionLevel,\n  isHeatmapGhostBin,\n  isHeatmapHoverEffectEnabled,\n  resolveHeatmapDisplayRange,\n  resolveHeatmapHoverStyle,\n  resolveHeatmapRowOpacity,\n} from \"./heatmap-utils\";\n\nconst HEATMAP_INACTIVE_OPACITY = 0.3;\n/** Smooth tween for inactive opacity + scale on hover. */\nconst HEATMAP_INACTIVE_TRANSITION = {\n  duration: 0.22,\n  ease: [0.4, 0, 0.2, 1] as const,\n};\nconst HEATMAP_CONCEAL_TRANSITION = {\n  duration: HEATMAP_LOADING_CONCEAL_MS / 1000,\n  ease: HEATMAP_DEFAULT_ENTER_EASE,\n};\n\nfunction computeHeatmapCellFaded(\n  isCellHovering: boolean,\n  isLevelHovering: boolean,\n  hoveredCell: { column: number; row: number } | null,\n  hoveredLegendLevel: number | null,\n  cell: { column: number; row: number },\n  count: number\n): { isHighlighted: boolean; isDimmed: boolean } {\n  if (isCellHovering && hoveredCell) {\n    const isHighlighted =\n      hoveredCell.column === cell.column && hoveredCell.row === cell.row;\n    return { isHighlighted, isDimmed: !isHighlighted };\n  }\n  if (isLevelHovering && hoveredLegendLevel !== null) {\n    const isHighlighted =\n      getHeatmapContributionLevel(count) === hoveredLegendLevel;\n    return { isHighlighted, isDimmed: !isHighlighted };\n  }\n  return { isHighlighted: false, isDimmed: false };\n}\n\ninterface SyncCellLayerParams {\n  animateCells: boolean;\n  chartStatus: ChartStatus;\n  dataOpacity: MotionValue<number>;\n  isAwaitingLoadingConceal: boolean;\n  isExitingToLoading: boolean;\n  isLoadingResting: boolean;\n  isReadyResting: boolean;\n  isRevealActive: boolean;\n  participates: boolean;\n  pulseOpacity: MotionValue<number>;\n  readyDataOpacity: number;\n  shimmerOpacity: MotionValue<number>;\n  showLoadingCellsLayer: boolean;\n  showShimmerPulse: boolean;\n  staggeredTransition: Transition;\n}\n\nfunction syncHeatmapCellLayerOpacities(params: SyncCellLayerParams) {\n  const {\n    animateCells,\n    chartStatus,\n    dataOpacity,\n    isAwaitingLoadingConceal,\n    isExitingToLoading,\n    isLoadingResting,\n    isReadyResting,\n    isRevealActive,\n    participates,\n    pulseOpacity,\n    readyDataOpacity,\n    shimmerOpacity,\n    showLoadingCellsLayer,\n    showShimmerPulse,\n    staggeredTransition,\n  } = params;\n\n  if (!animateCells) {\n    if (isReadyResting) {\n      dataOpacity.set(readyDataOpacity);\n      shimmerOpacity.set(0);\n      pulseOpacity.set(0);\n    } else if (chartStatus === \"loading\") {\n      dataOpacity.set(0);\n      pulseOpacity.set(0);\n      shimmerOpacity.set(\n        participates && showLoadingCellsLayer\n          ? 0\n          : HEATMAP_LOADING_BASE_CELL_OPACITY\n      );\n    }\n    return;\n  }\n\n  if (isLoadingResting) {\n    animate(dataOpacity, 0, { duration: 0 });\n    if (!showShimmerPulse) {\n      animate(shimmerOpacity, HEATMAP_LOADING_BASE_CELL_OPACITY, {\n        duration: 0,\n      });\n    }\n    return;\n  }\n\n  if (isRevealActive) {\n    pulseOpacity.set(0);\n    shimmerOpacity.set(0);\n    animate(dataOpacity, readyDataOpacity, staggeredTransition);\n    return;\n  }\n\n  if (isAwaitingLoadingConceal || isExitingToLoading) {\n    animate(dataOpacity, 0, HEATMAP_CONCEAL_TRANSITION);\n    animate(shimmerOpacity, 0, { duration: 0 });\n    return;\n  }\n\n  if (isReadyResting) {\n    animate(dataOpacity, readyDataOpacity, HEATMAP_INACTIVE_TRANSITION);\n    animate(shimmerOpacity, 0, { duration: 0 });\n    pulseOpacity.set(0);\n  }\n}\n\nexport interface HeatmapCellsProps {\n  /** Corner radius for each cell. Default: 2 */\n  cornerRadius?: number;\n  /** Override the default GitHub-style green color scale. */\n  colorScale?: (count: number | null | undefined) => string;\n  /** Opacity for inactive cells while hovering. Default: 0.3 */\n  inactiveOpacity?: number;\n  /** Scale for inactive cells while hovering. Default: 1 */\n  inactiveScale?: number;\n  /** Scale for the highlighted cell while hovering. Default: 1 */\n  activeScale?: number;\n  /** Per-row opacity multiplier (display row index). Default: 1 */\n  rowOpacity?: number | readonly number[];\n  /** Pointer hover + dimming. Default: true */\n  interactive?: boolean;\n  /** Hide out-of-range bins (GitHub-style ghost cells). Default: true */\n  hideGhostCells?: boolean;\n}\n\ninterface HeatmapCellRectProps {\n  cell: {\n    column: number;\n    row: number;\n    x: number;\n    y: number;\n    width: number;\n    height: number;\n    color?: string;\n    opacity?: number;\n  };\n  bin: HeatmapBin;\n  cornerRadius: number;\n  interactive: boolean;\n  inactiveOpacity: number;\n  inactiveScale: number;\n  activeScale: number;\n  rowOpacity: number | readonly number[] | undefined;\n  hoverState: { isHighlighted: boolean; isDimmed: boolean };\n  onEnter: (\n    column: number,\n    row: number,\n    bin: HeatmapBin,\n    x: number,\n    y: number\n  ) => void;\n  onLeave: () => void;\n}\n\nconst HeatmapMotionCell = memo(function HeatmapMotionCell({\n  cell,\n  bin,\n  cornerRadius,\n  fillScale,\n  interactive,\n  inactiveOpacity,\n  inactiveScale,\n  activeScale,\n  rowOpacity,\n  hoverState,\n  onEnter,\n  onLeave,\n}: HeatmapCellRectProps & {\n  fillScale: (count: number | null | undefined) => string;\n}) {\n  const {\n    chartStatus,\n    chartPhase,\n    isLoaded,\n    revealMode,\n    revealEpoch,\n    animationDuration,\n    enterTransition,\n    enterStaggerScale,\n    animateCells,\n    showLoadingCells: showLoadingCellsLayer,\n    loadingCellMaxOpacity,\n    loadingCellRandomness,\n    levelStyles,\n  } = useHeatmap();\n\n  const levelStyle =\n    levelStyles[getHeatmapContributionLevel(bin.count ?? 0)] ?? levelStyles[0];\n  const targetFill = fillScale(bin.count);\n  const emptyFill = fillScale(0);\n  const patternFillOpacity = heatmapLevelCellFillOpacity(levelStyle);\n  const dataOpacity = useMotionValue(0);\n  /** Orchestration layer: conceal, static loading base. */\n  const shimmerOpacity = useMotionValue(0);\n  /** Pulse loop only — kept separate so stagger tweens don't break re-entry. */\n  const pulseOpacity = useMotionValue(0);\n  const wasShimmerPulsingRef = useRef(false);\n\n  const isLoadingResting =\n    chartStatus === \"loading\" && chartPhase === \"loading\";\n  const isAwaitingLoadingConceal =\n    chartStatus === \"loading\" && chartPhase === \"ready\";\n  const isExitingToLoading = chartPhase === \"exitingReady\";\n  const isRevealActive =\n    chartPhase === \"revealing\" &&\n    !isLoaded &&\n    (revealMode === \"fromLoading\" || revealMode === \"enter\");\n  const isReadyResting =\n    chartStatus === \"ready\" && chartPhase === \"ready\" && isLoaded;\n\n  const participates = useMemo(\n    () =>\n      heatmapLoadingCellParticipates(\n        cell.column,\n        cell.row,\n        loadingCellRandomness\n      ),\n    [cell.column, cell.row, loadingCellRandomness]\n  );\n\n  const showShimmerPulse =\n    animateCells && showLoadingCellsLayer && participates && isLoadingResting;\n\n  const fadeDurationSec = resolveHeatmapEnterFadeDurationSec(\n    enterTransition,\n    animationDuration\n  );\n  const delayMs = computeHeatmapEnterFadeDelayMs({\n    column: cell.column,\n    row: cell.row,\n    revealEpoch,\n    animationDurationMs: animationDuration,\n    enterStaggerScale,\n    fadeDurationSec,\n  });\n  const staggeredTransition = transitionWithDelay(\n    enterTransition,\n    delayMs / 1000\n  );\n\n  const readyHoverStyle = resolveHeatmapHoverStyle(\n    hoverState.isHighlighted,\n    hoverState.isDimmed,\n    { inactiveOpacity, inactiveScale, activeScale }\n  );\n  const rowOpacityMultiplier = resolveHeatmapRowOpacity(cell.row, rowOpacity);\n  const readyDataOpacity = readyHoverStyle.opacity;\n  const readyScale = isReadyResting ? readyHoverStyle.scale : 1;\n  const transformOrigin = `${cell.x + cell.width / 2}px ${cell.y + cell.height / 2}px`;\n\n  useEffect(() => {\n    if (!showShimmerPulse) {\n      wasShimmerPulsingRef.current = false;\n      pulseOpacity.set(0);\n      return;\n    }\n\n    const isFreshLoadingPulse =\n      isLoadingResting && !wasShimmerPulsingRef.current;\n    wasShimmerPulsingRef.current = true;\n\n    if (isFreshLoadingPulse) {\n      pulseOpacity.set(0);\n    }\n\n    let cancelled = false;\n    let current: ReturnType<typeof animate> | undefined;\n\n    const pulse = async () => {\n      while (!cancelled) {\n        const target = Math.random() * loadingCellMaxOpacity;\n        const duration = 0.35 + Math.random() * 0.85;\n        current = animate(pulseOpacity, target, {\n          duration,\n          ease: [0.45, 0, 0.55, 1],\n        });\n\n        try {\n          await current;\n        } catch {\n          break;\n        }\n\n        if (cancelled) {\n          break;\n        }\n\n        await new Promise<void>((resolve) => {\n          window.setTimeout(resolve, 80 + Math.random() * 420);\n        });\n      }\n    };\n\n    pulse().catch(() => undefined);\n\n    return () => {\n      cancelled = true;\n      current?.stop();\n    };\n  }, [isLoadingResting, loadingCellMaxOpacity, pulseOpacity, showShimmerPulse]);\n\n  useEffect(() => {\n    syncHeatmapCellLayerOpacities({\n      animateCells,\n      chartStatus,\n      dataOpacity,\n      isAwaitingLoadingConceal,\n      isExitingToLoading,\n      isLoadingResting,\n      isReadyResting,\n      isRevealActive,\n      participates,\n      pulseOpacity,\n      readyDataOpacity,\n      shimmerOpacity,\n      showLoadingCellsLayer,\n      showShimmerPulse,\n      staggeredTransition,\n    });\n  }, [\n    animateCells,\n    chartStatus,\n    dataOpacity,\n    isAwaitingLoadingConceal,\n    isExitingToLoading,\n    isLoadingResting,\n    isReadyResting,\n    isRevealActive,\n    participates,\n    pulseOpacity,\n    readyDataOpacity,\n    shimmerOpacity,\n    showLoadingCellsLayer,\n    showShimmerPulse,\n    staggeredTransition,\n  ]);\n\n  const cellProps = {\n    className: \"visx-heatmap-rect\",\n    height: cell.height,\n    rx: cornerRadius,\n    ry: cornerRadius,\n    width: cell.width,\n    x: cell.x,\n    y: cell.y,\n  };\n\n  return (\n    <motion.g\n      animate={{ scale: readyScale }}\n      style={{ transformOrigin }}\n      transition={HEATMAP_INACTIVE_TRANSITION}\n    >\n      <motion.rect\n        {...cellProps}\n        fill={targetFill}\n        fillOpacity={\n          (cell.opacity ?? 1) * patternFillOpacity * rowOpacityMultiplier\n        }\n        onPointerEnter={() =>\n          onEnter(cell.column, cell.row, bin, cell.x, cell.y)\n        }\n        onPointerLeave={onLeave}\n        style={{\n          cursor: interactive ? \"pointer\" : undefined,\n          opacity: dataOpacity,\n        }}\n      />\n      <motion.rect\n        {...cellProps}\n        fill={emptyFill}\n        pointerEvents=\"none\"\n        style={{\n          opacity: showShimmerPulse ? pulseOpacity : shimmerOpacity,\n        }}\n      />\n    </motion.g>\n  );\n});\n\nexport const HeatmapCells = memo(function HeatmapCells({\n  cornerRadius = 2,\n  colorScale: colorScaleProp,\n  inactiveOpacity = HEATMAP_INACTIVE_OPACITY,\n  inactiveScale = 1,\n  activeScale = 1,\n  rowOpacity,\n  interactive = true,\n  hideGhostCells = true,\n}: HeatmapCellsProps) {\n  const {\n    data,\n    binWidth,\n    binHeight,\n    gap,\n    margin,\n    xScale,\n    yScale,\n    chartStatus,\n    colorScale: contextColorScale,\n    fillScale: contextFillScale,\n  } = useHeatmap();\n  const colorScale = colorScaleProp ?? contextColorScale;\n  const fillScale = contextFillScale;\n  const cellsInteractive = interactive && chartStatus !== \"loading\";\n  const {\n    hoveredCell,\n    hoveredLegendLevel,\n    setHoveredCell,\n    setHoveredLegendLevel,\n    setTooltipData,\n  } = useHeatmapInteraction();\n\n  const handleCellEnter = useCallback(\n    (column: number, row: number, bin: HeatmapBin, x: number, y: number) => {\n      if (!cellsInteractive) {\n        return;\n      }\n\n      setHoveredLegendLevel(null);\n      setHoveredCell({ column, row });\n      setTooltipData({\n        column,\n        row,\n        count: bin.count,\n        date: bin.date,\n        x: margin.left + x + binWidth / 2,\n        y: margin.top + y + binHeight / 2,\n      });\n    },\n    [\n      binHeight,\n      binWidth,\n      cellsInteractive,\n      margin.left,\n      margin.top,\n      setHoveredCell,\n      setHoveredLegendLevel,\n      setTooltipData,\n    ]\n  );\n\n  const handleCellLeave = useCallback(() => {\n    if (!cellsInteractive) {\n      return;\n    }\n\n    setHoveredCell(null);\n    setTooltipData(null);\n  }, [cellsInteractive, setHoveredCell, setTooltipData]);\n\n  const inactiveEnabled = isHeatmapHoverEffectEnabled({\n    inactiveOpacity,\n    inactiveScale,\n    activeScale,\n  });\n  const isCellHovering =\n    cellsInteractive && hoveredCell !== null && inactiveEnabled;\n  const isLevelHovering =\n    cellsInteractive && hoveredLegendLevel !== null && inactiveEnabled;\n\n  const displayRange = useMemo(\n    () => (hideGhostCells ? resolveHeatmapDisplayRange(data) : null),\n    [data, hideGhostCells]\n  );\n\n  return (\n    <HeatmapRect<HeatmapColumn, HeatmapBin>\n      binHeight={binHeight}\n      bins={(column) => column.bins}\n      binWidth={binWidth}\n      colorScale={(count) =>\n        colorScale(typeof count === \"number\" ? count : count?.valueOf())\n      }\n      count={(bin) => bin.count}\n      data={data}\n      gap={gap}\n      xScale={xScale}\n      yScale={yScale}\n    >\n      {(cells) => (\n        <Group className=\"visx-heatmap-rects\">\n          {cells.flatMap((column) =>\n            column.map((cell) => {\n              const bin = data[cell.column]?.bins[cell.row];\n              if (!bin) {\n                return null;\n              }\n\n              if (displayRange && isHeatmapGhostBin(bin, displayRange)) {\n                return null;\n              }\n\n              const hoverState = computeHeatmapCellFaded(\n                isCellHovering,\n                isLevelHovering,\n                hoveredCell,\n                hoveredLegendLevel,\n                cell,\n                bin.count\n              );\n\n              return (\n                <HeatmapMotionCell\n                  activeScale={activeScale}\n                  bin={bin}\n                  cell={cell}\n                  cornerRadius={cornerRadius}\n                  fillScale={fillScale}\n                  hoverState={hoverState}\n                  inactiveOpacity={inactiveOpacity}\n                  inactiveScale={inactiveScale}\n                  interactive={cellsInteractive}\n                  key={`heatmap-cell-${cell.column}-${cell.row}`}\n                  onEnter={handleCellEnter}\n                  onLeave={handleCellLeave}\n                  rowOpacity={rowOpacity}\n                />\n              );\n            })\n          )}\n        </Group>\n      )}\n    </HeatmapRect>\n  );\n});\n\nHeatmapCells.displayName = \"HeatmapCells\";\n\nexport default HeatmapCells;\n",
      "type": "registry:component",
      "target": "components/charts/heatmap/heatmap-cells.tsx"
    },
    {
      "path": "src/charts/heatmap/heatmap-x-axis.tsx",
      "content": "\"use client\";\n\nimport { memo, useEffect, useMemo, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"@/lib/utils\";\nimport { useHeatmap } from \"./heatmap-context\";\nimport { getHeatmapColumnMonthAnchor } from \"./heatmap-utils\";\n\nexport interface HeatmapXAxisProps {\n  /** Additional class name for labels */\n  className?: string;\n}\n\nconst monthFmt = new Intl.DateTimeFormat(\"en-US\", { month: \"short\" });\n\nexport const HeatmapXAxis = memo(function HeatmapXAxis({\n  className,\n}: HeatmapXAxisProps) {\n  const { containerRef, data, margin, xScale } = useHeatmap();\n  const [mounted, setMounted] = useState(false);\n\n  useEffect(() => {\n    setMounted(true);\n  }, []);\n\n  const labels = useMemo(() => {\n    const ticks: { label: string; x: number; key: string }[] = [];\n    let lastMonthKey = \"\";\n\n    for (let columnIndex = 0; columnIndex < data.length; columnIndex++) {\n      const column = data[columnIndex];\n      if (!column) {\n        continue;\n      }\n\n      const monthAnchor = getHeatmapColumnMonthAnchor(column);\n      if (!monthAnchor) {\n        continue;\n      }\n\n      const monthKey = `${monthAnchor.getFullYear()}-${monthAnchor.getMonth()}`;\n      if (monthKey === lastMonthKey) {\n        continue;\n      }\n\n      ticks.push({\n        label: monthFmt.format(monthAnchor),\n        x: margin.left + xScale(columnIndex),\n        key: monthKey,\n      });\n      lastMonthKey = monthKey;\n    }\n\n    return ticks;\n  }, [data, margin.left, xScale]);\n\n  const container = containerRef.current;\n  if (!(mounted && container)) {\n    return null;\n  }\n\n  return createPortal(\n    labels.map((tick) => (\n      <div\n        className=\"pointer-events-none absolute\"\n        key={tick.key}\n        style={{\n          top: 0,\n          left: tick.x,\n          width: 0,\n          display: \"flex\",\n          justifyContent: \"flex-start\",\n        }}\n      >\n        <span\n          className={cn(\n            \"whitespace-nowrap text-chart-label text-xs\",\n            className\n          )}\n        >\n          {tick.label}\n        </span>\n      </div>\n    )),\n    container\n  );\n});\n\nHeatmapXAxis.displayName = \"HeatmapXAxis\";\n\nexport default HeatmapXAxis;\n",
      "type": "registry:component",
      "target": "components/charts/heatmap/heatmap-x-axis.tsx"
    },
    {
      "path": "src/charts/heatmap/heatmap-y-axis.tsx",
      "content": "\"use client\";\n\nimport { memo, useEffect, useMemo, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"@/lib/utils\";\nimport { useHeatmap } from \"./heatmap-context\";\nimport {\n  formatHeatmapYAxisLabel,\n  getHeatmapDayLabels,\n  type HeatmapYAxisLabelFormat,\n  type HeatmapYAxisTickFilter,\n  resolveHeatmapRowOpacity,\n  shouldShowHeatmapYAxisTick,\n} from \"./heatmap-utils\";\n\nexport interface HeatmapYAxisProps {\n  /** Additional class name for labels */\n  className?: string;\n  /** Which row ticks to display. Default: `\"odd\"` (Mon / Wed / Fri). */\n  tickFilter?: HeatmapYAxisTickFilter;\n  /** Label format — `\"initial\"` shows the first letter only (Mon → M). Default: `\"full\"`. */\n  labelFormat?: HeatmapYAxisLabelFormat;\n  /** Per-row label opacity — mirrors {@link HeatmapCells} `rowOpacity`. */\n  rowOpacity?: number | readonly number[];\n}\n\nexport const HeatmapYAxis = memo(function HeatmapYAxis({\n  className,\n  tickFilter = \"odd\",\n  labelFormat = \"full\",\n  rowOpacity,\n}: HeatmapYAxisProps) {\n  const { containerRef, margin, binHeight, gap, yScale, weekStartDay } =\n    useHeatmap();\n  const [mounted, setMounted] = useState(false);\n\n  useEffect(() => {\n    setMounted(true);\n  }, []);\n\n  const labels = useMemo(\n    () =>\n      getHeatmapDayLabels(weekStartDay)\n        .map((label, row) => ({\n          row,\n          label: formatHeatmapYAxisLabel(label, labelFormat),\n          y: margin.top + yScale(row) + (binHeight - gap) / 2,\n        }))\n        .filter((tick) => shouldShowHeatmapYAxisTick(tick.row, tickFilter)),\n    [binHeight, gap, labelFormat, margin.top, tickFilter, weekStartDay, yScale]\n  );\n\n  const container = containerRef.current;\n  if (!(mounted && container)) {\n    return null;\n  }\n\n  return createPortal(\n    labels.map((tick) => (\n      <div\n        className=\"pointer-events-none absolute\"\n        key={tick.row}\n        style={{\n          top: tick.y,\n          left: 4,\n          width: Math.max(margin.left - 12, 0),\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"flex-end\",\n          transform: \"translateY(-50%)\",\n        }}\n      >\n        <span\n          className={cn(\n            \"whitespace-nowrap text-chart-label text-xs\",\n            className\n          )}\n          style={{ opacity: resolveHeatmapRowOpacity(tick.row, rowOpacity) }}\n        >\n          {tick.label}\n        </span>\n      </div>\n    )),\n    container\n  );\n});\n\nHeatmapYAxis.displayName = \"HeatmapYAxis\";\n\nexport default HeatmapYAxis;\n",
      "type": "registry:component",
      "target": "components/charts/heatmap/heatmap-y-axis.tsx"
    },
    {
      "path": "src/charts/heatmap/heatmap-legend.tsx",
      "content": "\"use client\";\n\nimport { motion } from \"motion/react\";\nimport { memo, useCallback } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  defaultHeatmapColorScale,\n  type HeatmapLevelStyles,\n} from \"./heatmap-colors\";\nimport { useHeatmapInteractionOptional } from \"./heatmap-context\";\nimport { HeatmapLegendGradient } from \"./heatmap-legend-gradient\";\nimport { HeatmapLegendSwatch } from \"./heatmap-legend-swatch\";\nimport {\n  getHeatmapContributionLevel,\n  isHeatmapHoverEffectEnabled,\n  resolveHeatmapHoverStyle,\n} from \"./heatmap-utils\";\n\nexport const HEATMAP_LEGEND_LEVELS = [0, 1, 2, 3, 4] as const;\n\nconst HEATMAP_INACTIVE_OPACITY = 0.3;\n/** Smooth tween for inactive opacity + scale on hover. */\nconst HEATMAP_INACTIVE_TRANSITION = {\n  duration: 0.22,\n  ease: [0.4, 0, 0.2, 1] as const,\n};\n\nexport type HeatmapLegendVariant = \"swatches\" | \"gradient\";\n\nexport interface HeatmapLegendProps {\n  /** Side label before the swatches. Default: \"Less\" */\n  lessLabel?: string;\n  /** Side label after the swatches. Default: \"More\" */\n  moreLabel?: string;\n  /** Swatch size in pixels. Default: 11 */\n  cellSize?: number;\n  /** Gap between swatches. Default: 2 */\n  gap?: number;\n  /** Corner radius for swatches. Default: 2 */\n  cornerRadius?: number;\n  /** Horizontal alignment within the legend cell. Default: \"end\" */\n  align?: \"start\" | \"center\" | \"end\";\n  /** Legend layout — discrete swatches or a continuous gradient bar. Default: `\"swatches\"` */\n  variant?: HeatmapLegendVariant;\n  /** Gradient bar width in swatch units when `variant=\"gradient\"`. Default: 5 */\n  gradientSpan?: number;\n  /** Font size in pixels for side labels. */\n  fontSize?: number;\n  /** Additional class name for less/more labels. */\n  labelClassName?: string;\n  /** Shared level styles for cells and legend swatches. */\n  levelStyles?: HeatmapLevelStyles;\n  /** Override the default color scale (used when `levelStyles` is omitted). */\n  colorScale?: (count: number | null | undefined) => string;\n  /** Opacity for inactive swatches while interacting. Default: 0.3 */\n  inactiveOpacity?: number;\n  /** Scale for inactive swatches while interacting. Default: 1 */\n  inactiveScale?: number;\n  /** Scale for the highlighted swatch while interacting. Default: 1 */\n  activeScale?: number;\n  /** Sync dimming with chart hover. Default: true when inside HeatmapInteractionProvider */\n  interactive?: boolean;\n  className?: string;\n}\n\nexport const HeatmapLegend = memo(function HeatmapLegend({\n  lessLabel = \"Less\",\n  moreLabel = \"More\",\n  cellSize = 11,\n  gap = 2,\n  cornerRadius = 2,\n  align = \"end\",\n  variant = \"swatches\",\n  gradientSpan = 5,\n  fontSize,\n  labelClassName,\n  levelStyles: levelStylesProp,\n  colorScale = defaultHeatmapColorScale,\n  inactiveOpacity = HEATMAP_INACTIVE_OPACITY,\n  inactiveScale = 1,\n  activeScale = 1,\n  interactive,\n  className,\n}: HeatmapLegendProps) {\n  const interaction = useHeatmapInteractionOptional();\n  const isInteractive = interactive ?? interaction != null;\n  const levelStyles =\n    levelStylesProp ??\n    (HEATMAP_LEGEND_LEVELS.map((level) => ({\n      color: colorScale(level),\n      fillMode: \"solid\" as const,\n      pattern: \"none\" as const,\n    })) as unknown as HeatmapLevelStyles);\n\n  const {\n    hoveredLegendLevel,\n    tooltipData,\n    setHoveredCell,\n    setHoveredLegendLevel,\n    setTooltipData,\n  } = interaction ?? {\n    hoveredLegendLevel: null,\n    tooltipData: null,\n    setHoveredCell: () => undefined,\n    setHoveredLegendLevel: () => undefined,\n    setTooltipData: () => undefined,\n  };\n\n  const highlightedLevel =\n    hoveredLegendLevel ??\n    (tooltipData ? getHeatmapContributionLevel(tooltipData.count) : null);\n  const hoverParams = { inactiveOpacity, inactiveScale, activeScale };\n  const inactiveEnabled = isHeatmapHoverEffectEnabled(hoverParams);\n  const isDimming =\n    isInteractive && highlightedLevel !== null && inactiveEnabled;\n\n  const handleLegendEnter = useCallback(\n    (level: number) => {\n      if (!isInteractive) {\n        return;\n      }\n\n      setHoveredLegendLevel(level);\n      setHoveredCell(null);\n      setTooltipData(null);\n    },\n    [isInteractive, setHoveredCell, setHoveredLegendLevel, setTooltipData]\n  );\n\n  const handleLegendLeave = useCallback(() => {\n    if (!isInteractive) {\n      return;\n    }\n\n    setHoveredLegendLevel(null);\n  }, [isInteractive, setHoveredLegendLevel]);\n\n  const labelStyle = fontSize == null ? undefined : { fontSize };\n\n  return (\n    <div\n      className={cn(\n        \"flex w-full items-center gap-1.5 text-chart-label text-xs\",\n        align === \"start\" && \"justify-start\",\n        align === \"center\" && \"justify-center\",\n        align === \"end\" && \"justify-end\",\n        className\n      )}\n      style={labelStyle}\n    >\n      <span className={labelClassName}>{lessLabel}</span>\n      {variant === \"gradient\" ? (\n        <HeatmapLegendGradient\n          activeScale={activeScale}\n          cellSize={cellSize}\n          cornerRadius={cornerRadius}\n          gap={gap}\n          gradientSpan={gradientSpan}\n          highlightedLevel={highlightedLevel}\n          inactiveOpacity={inactiveOpacity}\n          inactiveScale={inactiveScale}\n          isDimming={isDimming}\n          isInteractive={isInteractive}\n          levelStyles={levelStyles}\n          levels={HEATMAP_LEGEND_LEVELS}\n          onEnter={handleLegendEnter}\n          onLeave={handleLegendLeave}\n        />\n      ) : (\n        <div className=\"flex items-center\" style={{ gap }}>\n          {HEATMAP_LEGEND_LEVELS.map((level) => {\n            const isHighlighted = highlightedLevel === level;\n            const isDimmed = isDimming && !isHighlighted;\n            const hoverStyle = resolveHeatmapHoverStyle(\n              isHighlighted,\n              isDimmed,\n              hoverParams\n            );\n            const style = levelStyles[level];\n\n            return (\n              <motion.span\n                animate={{\n                  opacity: hoverStyle.opacity,\n                  scale: hoverStyle.scale,\n                }}\n                aria-hidden=\"true\"\n                className=\"block shrink-0 leading-none\"\n                initial={{ opacity: 1, scale: 1 }}\n                key={level}\n                onPointerEnter={() => handleLegendEnter(level)}\n                onPointerLeave={handleLegendLeave}\n                style={{\n                  cursor: isInteractive ? \"pointer\" : undefined,\n                }}\n                transition={HEATMAP_INACTIVE_TRANSITION}\n              >\n                <HeatmapLegendSwatch\n                  cellSize={cellSize}\n                  cornerRadius={cornerRadius}\n                  level={level}\n                  style={style}\n                />\n              </motion.span>\n            );\n          })}\n        </div>\n      )}\n      <span className={labelClassName}>{moreLabel}</span>\n    </div>\n  );\n});\n\nHeatmapLegend.displayName = \"HeatmapLegend\";\n\nexport default HeatmapLegend;\n",
      "type": "registry:component",
      "target": "components/charts/heatmap/heatmap-legend.tsx"
    },
    {
      "path": "src/charts/heatmap/heatmap-legend-swatch.tsx",
      "content": "\"use client\";\n\nimport { memo } from \"react\";\nimport { renderPatternPreset } from \"../pattern-preset\";\nimport {\n  type HeatmapLevelStyle,\n  heatmapLevelPatternId,\n  heatmapLevelPatternRenderOptions,\n  isHeatmapLevelPattern,\n} from \"./heatmap-colors\";\n\nexport const HeatmapLegendSwatch = memo(function HeatmapLegendSwatch({\n  level,\n  style,\n  cellSize,\n  cornerRadius,\n}: {\n  level: number;\n  style: HeatmapLevelStyle;\n  cellSize: number;\n  cornerRadius: number;\n}) {\n  const shellStyle = {\n    width: cellSize,\n    height: cellSize,\n    borderRadius: cornerRadius,\n  };\n\n  if (isHeatmapLevelPattern(style) && style.pattern) {\n    const patternId = heatmapLevelPatternId(level);\n    const patternNode = renderPatternPreset(\n      style.pattern,\n      patternId,\n      heatmapLevelPatternRenderOptions(style)\n    );\n    const opacity = style.patternOpacity ?? 1;\n\n    return (\n      <span\n        aria-hidden=\"true\"\n        className=\"block shrink-0 overflow-hidden\"\n        style={{ ...shellStyle, opacity }}\n      >\n        <svg\n          aria-hidden=\"true\"\n          className=\"block size-full\"\n          viewBox={`0 0 ${cellSize} ${cellSize}`}\n        >\n          {patternNode ? <defs>{patternNode}</defs> : null}\n          <rect\n            fill={patternNode ? `url(#${patternId})` : style.color}\n            height={cellSize}\n            rx={cornerRadius}\n            ry={cornerRadius}\n            width={cellSize}\n          />\n        </svg>\n      </span>\n    );\n  }\n\n  return (\n    <span\n      aria-hidden=\"true\"\n      className=\"block shrink-0\"\n      style={{\n        ...shellStyle,\n        backgroundColor: style.color,\n        border: level === 0 ? `1px solid ${style.color}` : undefined,\n        boxSizing: \"border-box\",\n      }}\n    />\n  );\n});\n\nHeatmapLegendSwatch.displayName = \"HeatmapLegendSwatch\";\n",
      "type": "registry:component",
      "target": "components/charts/heatmap/heatmap-legend-swatch.tsx"
    },
    {
      "path": "src/charts/heatmap/heatmap-legend-gradient.tsx",
      "content": "\"use client\";\n\nimport { motion } from \"motion/react\";\nimport { memo } from \"react\";\nimport type { HeatmapLevelStyles } from \"./heatmap-colors\";\nimport {\n  buildHeatmapLegendGradient,\n  resolveHeatmapHoverStyle,\n} from \"./heatmap-utils\";\n\nconst HEATMAP_INACTIVE_TRANSITION = {\n  duration: 0.22,\n  ease: [0.4, 0, 0.2, 1] as const,\n};\n\nexport interface HeatmapLegendGradientProps {\n  levels: readonly number[];\n  levelStyles: HeatmapLevelStyles;\n  cellSize: number;\n  gap: number;\n  cornerRadius: number;\n  gradientSpan: number;\n  highlightedLevel: number | null;\n  isDimming: boolean;\n  inactiveOpacity: number;\n  inactiveScale: number;\n  activeScale: number;\n  isInteractive: boolean;\n  onEnter: (level: number) => void;\n  onLeave: () => void;\n}\n\nexport const HeatmapLegendGradient = memo(function HeatmapLegendGradient({\n  levels,\n  levelStyles,\n  cellSize,\n  gap,\n  cornerRadius,\n  gradientSpan,\n  highlightedLevel,\n  isDimming,\n  inactiveOpacity,\n  inactiveScale,\n  activeScale,\n  isInteractive,\n  onEnter,\n  onLeave,\n}: HeatmapLegendGradientProps) {\n  const barWidth = gradientSpan * cellSize + (gradientSpan - 1) * gap;\n  const barHeight = cellSize;\n  const pillRadius = Math.min(cornerRadius, barHeight / 2);\n  const segmentWidth = barWidth / levels.length;\n  const gradient = buildHeatmapLegendGradient(levelStyles);\n\n  return (\n    <div\n      className=\"relative shrink-0\"\n      style={{ width: barWidth, height: barHeight }}\n    >\n      <motion.div\n        animate={{\n          opacity: isDimming && highlightedLevel === null ? inactiveOpacity : 1,\n          scale: 1,\n        }}\n        aria-hidden=\"true\"\n        className=\"absolute inset-0\"\n        initial={{ opacity: 1, scale: 1 }}\n        style={{\n          borderRadius: pillRadius,\n          background: gradient,\n        }}\n        transition={HEATMAP_INACTIVE_TRANSITION}\n      />\n      {levels.map((level, index) => {\n        const isHighlighted = highlightedLevel === level;\n        const isDimmed = isDimming && !isHighlighted;\n        const hoverStyle = resolveHeatmapHoverStyle(isHighlighted, isDimmed, {\n          inactiveOpacity,\n          inactiveScale,\n          activeScale,\n        });\n\n        return (\n          <motion.span\n            animate={{\n              opacity: hoverStyle.opacity,\n              scale: hoverStyle.scale,\n            }}\n            className=\"absolute top-0 block\"\n            initial={{ opacity: 1, scale: 1 }}\n            key={level}\n            onPointerEnter={() => onEnter(level)}\n            onPointerLeave={onLeave}\n            style={{\n              left: index * segmentWidth,\n              width: segmentWidth,\n              height: barHeight,\n              cursor: isInteractive ? \"pointer\" : undefined,\n              transformOrigin: \"center center\",\n            }}\n            transition={HEATMAP_INACTIVE_TRANSITION}\n          />\n        );\n      })}\n    </div>\n  );\n});\n\nHeatmapLegendGradient.displayName = \"HeatmapLegendGradient\";\n\nexport default HeatmapLegendGradient;\n",
      "type": "registry:component",
      "target": "components/charts/heatmap/heatmap-legend-gradient.tsx"
    },
    {
      "path": "src/charts/heatmap/heatmap-tooltip.tsx",
      "content": "\"use client\";\n\nimport { memo } from \"react\";\nimport { TooltipBox } from \"../tooltip/tooltip-box\";\nimport { useHeatmap, useHeatmapInteraction } from \"./heatmap-context\";\nimport {\n  formatHeatmapContributionLabel,\n  formatHeatmapTooltipDate,\n  formatHeatmapTooltipWeekday,\n} from \"./heatmap-utils\";\nimport { useDelayedTooltipData } from \"./use-delayed-tooltip-data\";\n\nexport interface HeatmapTooltipProps {\n  /** Custom contribution line (bottom section). Default: `N contribution(s)`. */\n  formatLabel?: (count: number, date: Date) => string;\n  /** Custom class name */\n  className?: string;\n  /** Inline styles for the tooltip panel (background, blur, etc.). */\n  panelStyle?: React.CSSProperties;\n  /**\n   * Tooltip panel background color (CSS variable or color value).\n   * Default: `var(--chart-tooltip-background)`.\n   */\n  backgroundColor?: string;\n  /**\n   * Delay before showing the tooltip on first hover (ms).\n   * Moving between cells updates immediately once visible.\n   */\n  showDelay?: number;\n  /**\n   * Grace period before hiding when the pointer leaves a cell (ms).\n   * Helps avoid flicker when moving quickly between adjacent cells.\n   */\n  hideDelay?: number;\n  /**\n   * When true, the tooltip appears and disappears instantly with no motion.\n   */\n  instant?: boolean;\n}\n\nexport const HeatmapTooltip = memo(function HeatmapTooltip({\n  formatLabel = formatHeatmapContributionLabel,\n  className = \"\",\n  panelStyle,\n  backgroundColor,\n  showDelay = 0,\n  hideDelay = 120,\n  instant = false,\n}: HeatmapTooltipProps) {\n  const { containerRef, width, height } = useHeatmap();\n  const { tooltipData } = useHeatmapInteraction();\n  const displayData = useDelayedTooltipData(tooltipData, showDelay, hideDelay);\n\n  if (!displayData) {\n    return null;\n  }\n\n  const { count, date } = displayData;\n\n  return (\n    <TooltipBox\n      animate={false}\n      backgroundColor={backgroundColor}\n      className={className}\n      containerHeight={height}\n      containerRef={containerRef}\n      containerWidth={width}\n      entrance={!instant}\n      panelStyle={panelStyle}\n      visible\n      x={displayData.x}\n      y={displayData.y}\n    >\n      <div className=\"overflow-hidden\">\n        <div className=\"px-3 py-2.5 text-left\">\n          <div className=\"font-medium text-chart-tooltip-foreground text-xs\">\n            {formatHeatmapTooltipDate(date)}\n          </div>\n          <div className=\"mt-0.5 text-chart-tooltip-muted text-xs\">\n            {formatHeatmapTooltipWeekday(date)}\n          </div>\n          <div className=\"my-2 border-chart-tooltip-muted/30 border-t\" />\n          <div className=\"text-chart-tooltip-foreground text-sm\">\n            {formatLabel(count, date)}\n          </div>\n        </div>\n      </div>\n    </TooltipBox>\n  );\n});\n\nHeatmapTooltip.displayName = \"HeatmapTooltip\";\n\nexport default HeatmapTooltip;\n",
      "type": "registry:component",
      "target": "components/charts/heatmap/heatmap-tooltip.tsx"
    },
    {
      "path": "src/charts/heatmap/use-delayed-tooltip-data.ts",
      "content": "\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport type { HeatmapTooltipData } from \"./heatmap-context\";\n\nexport function useDelayedTooltipData(\n  tooltipData: HeatmapTooltipData | null,\n  showDelay: number,\n  hideDelay: number\n): HeatmapTooltipData | null {\n  const [displayData, setDisplayData] = useState<HeatmapTooltipData | null>(\n    null\n  );\n  const isShowingRef = useRef(false);\n  const showTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(\n    undefined\n  );\n  const hideTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(\n    undefined\n  );\n\n  useEffect(() => {\n    if (showTimerRef.current) {\n      clearTimeout(showTimerRef.current);\n      showTimerRef.current = undefined;\n    }\n    if (hideTimerRef.current) {\n      clearTimeout(hideTimerRef.current);\n      hideTimerRef.current = undefined;\n    }\n\n    if (tooltipData) {\n      if (isShowingRef.current) {\n        setDisplayData(tooltipData);\n        return;\n      }\n\n      if (showDelay === 0) {\n        isShowingRef.current = true;\n        setDisplayData(tooltipData);\n        return;\n      }\n\n      showTimerRef.current = setTimeout(() => {\n        isShowingRef.current = true;\n        setDisplayData(tooltipData);\n      }, showDelay);\n      return;\n    }\n\n    if (hideDelay === 0) {\n      isShowingRef.current = false;\n      setDisplayData(null);\n      return;\n    }\n\n    hideTimerRef.current = setTimeout(() => {\n      isShowingRef.current = false;\n      setDisplayData(null);\n    }, hideDelay);\n  }, [tooltipData, showDelay, hideDelay]);\n\n  useEffect(\n    () => () => {\n      if (showTimerRef.current) {\n        clearTimeout(showTimerRef.current);\n      }\n      if (hideTimerRef.current) {\n        clearTimeout(hideTimerRef.current);\n      }\n    },\n    []\n  );\n\n  return displayData;\n}\n",
      "type": "registry:component",
      "target": "components/charts/heatmap/use-delayed-tooltip-data.ts"
    },
    {
      "path": "src/charts/heatmap/heatmap-separator.tsx",
      "content": "\"use client\";\n\nimport { memo, useEffect, useId, useMemo, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"@/lib/utils\";\nimport { useHeatmap } from \"./heatmap-context\";\nimport {\n  buildHeatmapSeparatorGradientStops,\n  getHeatmapSeparatorLineY,\n  getHeatmapSeparatorX,\n  type HeatmapSeparatorGradient,\n  type HeatmapSeparatorStrokeStyle,\n  resolveHeatmapSeparatorStrokeDasharray,\n} from \"./heatmap-utils\";\n\n/** Marker for reliably identifying {@link HeatmapSeparator} in chart child trees. */\nexport const HEATMAP_SEPARATOR_MARKER = \"__isHeatmapSeparator\" as const;\n\nexport interface HeatmapSeparatorProps {\n  /**\n   * Insert a separator before every Nth column (e.g. 4 = divide every four weeks).\n   * Used when `groupBy=\"every\"`. When omitted, uses `columnSeparators` from\n   * {@link HeatmapChart} or a sibling {@link HeatmapSeparator} parsed for layout.\n   */\n  every?: number;\n  /**\n   * Group columns by fixed interval (`every`) or calendar quarter (`quarter`).\n   * Default: `\"every\"`.\n   */\n  groupBy?: \"every\" | \"quarter\";\n  /** Additional class name for each separator group. */\n  className?: string;\n  /**\n   * Extra horizontal gap between column groups in pixels (shifts cells apart).\n   * Parsed from this component by {@link HeatmapChart} for layout. Default: 0\n   */\n  spacing?: number;\n  /** Half-width of the line band in pixels (line is centered). Default: 0 */\n  paddingX?: number;\n  /** Inset from the bottom of the plot in pixels. Default: 0 */\n  paddingY?: number;\n  /**\n   * Distance from the chart container top to the line start, in pixels.\n   * Use to align with {@link HeatmapXAxis} labels (e.g. `14`). The line always\n   * extends to the plot bottom. Default: plot top (`margin.top`).\n   */\n  startOffset?: number;\n  /**\n   * Distance below the separator line top to place quarter labels.\n   * `0` aligns labels with the line top. Default: 0.\n   */\n  labelOffset?: number;\n  /** Draw Q1–Q4 labels at the start of each quarter group. Default: false */\n  showLabels?: boolean;\n  /** Format quarter group labels. Default: `Q{n}` */\n  labelFormat?: (quarter: number, startDate: Date) => string;\n  /** Additional class name for quarter labels */\n  labelClassName?: string;\n  /** Solid or dashed separator line. Default: `\"solid\"` */\n  strokeStyle?: HeatmapSeparatorStrokeStyle;\n  /** Dash pattern when `strokeStyle=\"dashed\"`. Default: `\"4,4\"` */\n  strokeDasharray?: string;\n  /** Line stroke color when `gradient` is omitted. Default: `var(--border)` */\n  stroke?: string;\n  /**\n   * Vertical stroke gradient aligned to each line's span.\n   * Use matching `from`/`via`/`to` colors with fading opacities to soften the\n   * top and bottom (e.g. `{ fromOpacity: 0, viaOpacity: 1, toOpacity: 0 }`).\n   */\n  gradient?: HeatmapSeparatorGradient;\n  /** Line stroke width in pixels. Default: 1 */\n  strokeWidth?: number;\n  /** Multiplier applied to solid strokes or gradient stop opacities. Default: 1 */\n  strokeOpacity?: number;\n}\n\nexport const HeatmapSeparator = memo(function HeatmapSeparator({\n  className,\n  paddingX = 0,\n  paddingY = 0,\n  startOffset,\n  labelOffset = 0,\n  showLabels = false,\n  labelFormat = (quarter) => `Q${quarter}`,\n  labelClassName,\n  strokeStyle = \"solid\",\n  strokeDasharray,\n  stroke = \"var(--border)\",\n  gradient,\n  strokeWidth = 1,\n  strokeOpacity = 1,\n}: HeatmapSeparatorProps) {\n  const { gap, innerHeight, margin, separatorLayout, xScale, containerRef } =\n    useHeatmap();\n\n  const [mounted, setMounted] = useState(false);\n  const reactId = useId().replace(/:/g, \"\");\n  const gradientId = `heatmap-separator-gradient-${reactId}`;\n\n  useEffect(() => {\n    setMounted(true);\n  }, []);\n\n  const separators = useMemo(() => {\n    if (!separatorLayout) {\n      return [];\n    }\n\n    return separatorLayout.atColumns.map((columnIndex) => ({\n      columnIndex,\n      x: getHeatmapSeparatorX(columnIndex, gap, separatorLayout, xScale),\n    }));\n  }, [gap, separatorLayout, xScale]);\n\n  const labels = useMemo(() => {\n    if (!(showLabels && separatorLayout?.groups.length)) {\n      return [];\n    }\n\n    return separatorLayout.groups.map((group) => ({\n      key: `${group.year}-Q${group.quarter}-${group.startColumnIndex}`,\n      label: labelFormat(group.quarter, group.startDate),\n      x: margin.left + xScale(group.startColumnIndex),\n    }));\n  }, [labelFormat, margin.left, separatorLayout, showLabels, xScale]);\n\n  if (!separatorLayout) {\n    return null;\n  }\n\n  const { y1: lineY1, y2: lineY2 } = getHeatmapSeparatorLineY({\n    innerHeight,\n    marginTop: margin.top,\n    startOffset,\n    paddingY,\n  });\n\n  const separatorTop = startOffset ?? margin.top;\n  const labelTop = separatorTop + labelOffset;\n  const resolvedStroke = gradient ? `url(#${gradientId})` : stroke;\n  const resolvedStrokeDasharray = resolveHeatmapSeparatorStrokeDasharray(\n    strokeStyle,\n    strokeDasharray\n  );\n  const gradientStops = gradient\n    ? buildHeatmapSeparatorGradientStops(gradient, strokeOpacity)\n    : null;\n\n  const container = containerRef.current;\n  const labelPortal =\n    mounted && container && labels.length > 0\n      ? createPortal(\n          labels.map((tick) => (\n            <div\n              className=\"pointer-events-none absolute\"\n              key={tick.key}\n              style={{\n                top: labelTop,\n                left: tick.x,\n                width: 0,\n                display: \"flex\",\n                justifyContent: \"flex-start\",\n              }}\n            >\n              <span\n                className={cn(\n                  \"whitespace-nowrap text-chart-label text-xs\",\n                  labelClassName\n                )}\n              >\n                {tick.label}\n              </span>\n            </div>\n          )),\n          container\n        )\n      : null;\n\n  if (separators.length === 0) {\n    return labelPortal;\n  }\n\n  return (\n    <>\n      {gradientStops ? (\n        <defs>\n          <linearGradient\n            gradientUnits=\"userSpaceOnUse\"\n            id={gradientId}\n            x1={0}\n            x2={0}\n            y1={lineY1}\n            y2={lineY2}\n          >\n            {gradientStops.map((stop) => (\n              <stop\n                key={stop.offset}\n                offset={stop.offset}\n                stopColor={stop.color}\n                stopOpacity={stop.opacity}\n              />\n            ))}\n          </linearGradient>\n        </defs>\n      ) : null}\n      <g>\n        {separators.map((separator) => (\n          <g\n            className={cn(className)}\n            key={separator.columnIndex}\n            transform={`translate(${separator.x}, 0)`}\n          >\n            {paddingX > 0 ? (\n              <rect\n                fill=\"transparent\"\n                height={lineY2 - lineY1}\n                width={paddingX * 2}\n                x={-paddingX}\n                y={lineY1}\n              />\n            ) : null}\n            <line\n              stroke={resolvedStroke}\n              strokeDasharray={resolvedStrokeDasharray}\n              strokeOpacity={gradient ? undefined : strokeOpacity}\n              strokeWidth={strokeWidth}\n              x1={0}\n              x2={0}\n              y1={lineY1}\n              y2={lineY2}\n            />\n          </g>\n        ))}\n      </g>\n      {labelPortal}\n    </>\n  );\n});\n\n(HeatmapSeparator as unknown as Record<string, boolean>)[\n  HEATMAP_SEPARATOR_MARKER\n] = true;\n\nHeatmapSeparator.displayName = \"HeatmapSeparator\";\n\nexport default HeatmapSeparator;\n",
      "type": "registry:component",
      "target": "components/charts/heatmap/heatmap-separator.tsx"
    },
    {
      "path": "src/charts/heatmap/heatmap-resolve-separator.ts",
      "content": "import type { ReactElement, ReactNode } from \"react\";\nimport { Children, isValidElement } from \"react\";\nimport { resolveChartChildElement } from \"../chart-child-passthrough\";\nimport type { HeatmapColumn } from \"./heatmap-context\";\nimport {\n  HEATMAP_SEPARATOR_MARKER,\n  HeatmapSeparator,\n} from \"./heatmap-separator\";\nimport type {\n  HeatmapSeparatorGroupBy,\n  HeatmapSeparatorLayout,\n  HeatmapSeparatorParsedConfig,\n} from \"./heatmap-utils\";\nimport { resolveHeatmapSeparatorLayout } from \"./heatmap-utils\";\n\nfunction getChildComponentName(child: ReactElement): string {\n  const childType = child.type as { displayName?: string; name?: string };\n  return typeof child.type === \"function\"\n    ? childType.displayName || childType.name || \"\"\n    : \"\";\n}\n\nfunction isHeatmapSeparatorElement(child: ReactElement): boolean {\n  const resolved = resolveChartChildElement(child);\n  const type = resolved.type as { [HEATMAP_SEPARATOR_MARKER]?: boolean };\n  return (\n    resolved.type === HeatmapSeparator ||\n    type[HEATMAP_SEPARATOR_MARKER] === true ||\n    getChildComponentName(resolved) === \"HeatmapSeparator\"\n  );\n}\n\nfunction readHeatmapSeparatorConfig(\n  child: ReactElement\n): HeatmapSeparatorParsedConfig | null {\n  const props = resolveChartChildElement(child).props as {\n    every?: number;\n    groupBy?: HeatmapSeparatorGroupBy;\n    spacing?: number;\n  };\n  const groupBy = props.groupBy ?? \"every\";\n\n  if (groupBy === \"quarter\") {\n    return {\n      groupBy: \"quarter\",\n      spacing: props.spacing ?? 0,\n    };\n  }\n\n  if (props.every == null || props.every <= 0) {\n    return null;\n  }\n\n  return {\n    groupBy: \"every\",\n    every: props.every,\n    spacing: props.spacing ?? 0,\n  };\n}\n\n/** Reads the first {@link HeatmapSeparator} child for separator config. */\nexport function resolveHeatmapSeparatorConfig(\n  children: ReactNode,\n  chartSeparators?: HeatmapSeparatorParsedConfig | null\n): HeatmapSeparatorParsedConfig | null {\n  let config: HeatmapSeparatorParsedConfig | null = null;\n\n  const visit = (node: ReactNode) => {\n    if (config) {\n      return;\n    }\n\n    Children.forEach(node, (child) => {\n      if (config || !isValidElement(child)) {\n        return;\n      }\n\n      if (isHeatmapSeparatorElement(child)) {\n        config = readHeatmapSeparatorConfig(child);\n        return;\n      }\n\n      const childProps = child.props as { children?: ReactNode } | undefined;\n      if (childProps?.children) {\n        visit(childProps.children);\n      }\n    });\n  };\n\n  visit(children);\n  return config ?? normalizeHeatmapSeparatorConfig(chartSeparators);\n}\n\nfunction normalizeHeatmapSeparatorConfig(\n  config?: HeatmapSeparatorParsedConfig | null\n): HeatmapSeparatorParsedConfig | null {\n  if (!config) {\n    return null;\n  }\n\n  if (config.groupBy === \"quarter\") {\n    return {\n      groupBy: \"quarter\",\n      spacing: config.spacing ?? 0,\n    };\n  }\n\n  if (!config.every || config.every <= 0) {\n    return null;\n  }\n\n  return {\n    groupBy: \"every\",\n    every: config.every,\n    spacing: config.spacing ?? 0,\n  };\n}\n\n/** Merges chart prop and child-derived config, then resolves column indices from data. */\nexport function resolveHeatmapSeparatorConfigWithData(\n  children: ReactNode,\n  columns: HeatmapColumn[],\n  chartSeparators?: HeatmapSeparatorParsedConfig | null\n): HeatmapSeparatorLayout | null {\n  const config = resolveHeatmapSeparatorConfig(children, chartSeparators);\n  return resolveHeatmapSeparatorLayout(config, columns);\n}\n",
      "type": "registry:component",
      "target": "components/charts/heatmap/heatmap-resolve-separator.ts"
    },
    {
      "path": "src/charts/heatmap/heatmap-colors.ts",
      "content": "import { CHART_SCALE_VARS } from \"../chart-scale\";\nimport type { PatternPresetId } from \"../pattern-preset\";\nimport { getHeatmapContributionLevel } from \"./heatmap-utils\";\n\n/** Default Less → More scale using {@link CHART_SCALE_VARS}. */\nexport const HEATMAP_DEFAULT_LEVEL_COLORS = CHART_SCALE_VARS;\n\nexport type HeatmapLevelColors = readonly [\n  string,\n  string,\n  string,\n  string,\n  string,\n];\n\nexport type HeatmapLevelFillMode = \"solid\" | \"pattern\";\n\nexport interface HeatmapLevelStyle {\n  color: string;\n  fillMode?: HeatmapLevelFillMode;\n  pattern?: PatternPresetId;\n  patternColor?: string;\n  patternScale?: number;\n  patternStrokeWidth?: number;\n  patternRadius?: number;\n  patternComplement?: boolean;\n  patternFill?: string;\n  patternTileBackground?: string;\n  patternOpacity?: number;\n  patternDotsFill?: boolean;\n}\n\nexport type HeatmapLevelStyles = readonly [\n  HeatmapLevelStyle,\n  HeatmapLevelStyle,\n  HeatmapLevelStyle,\n  HeatmapLevelStyle,\n  HeatmapLevelStyle,\n];\n\nexport const HEATMAP_DEFAULT_LEVEL_STYLES = [\n  {\n    color: HEATMAP_DEFAULT_LEVEL_COLORS[0],\n    fillMode: \"solid\",\n    pattern: \"none\",\n  },\n  {\n    color: HEATMAP_DEFAULT_LEVEL_COLORS[1],\n    fillMode: \"solid\",\n    pattern: \"none\",\n  },\n  {\n    color: HEATMAP_DEFAULT_LEVEL_COLORS[2],\n    fillMode: \"solid\",\n    pattern: \"none\",\n  },\n  {\n    color: HEATMAP_DEFAULT_LEVEL_COLORS[3],\n    fillMode: \"solid\",\n    pattern: \"none\",\n  },\n  {\n    color: HEATMAP_DEFAULT_LEVEL_COLORS[4],\n    fillMode: \"solid\",\n    pattern: \"none\",\n  },\n] as const satisfies HeatmapLevelStyles;\n\nexport function heatmapLevelPatternId(level: number): string {\n  return `heatmap-level-${level}`;\n}\n\nexport function isHeatmapLevelPattern(style: HeatmapLevelStyle): boolean {\n  return (\n    style.fillMode === \"pattern\" &&\n    style.pattern != null &&\n    style.pattern !== \"none\"\n  );\n}\n\nexport function heatmapPatternStrokeFallback(color: string): string {\n  return `color-mix(in oklch, ${color} 45%, white)`;\n}\n\nexport function heatmapLevelPatternRenderOptions(style: HeatmapLevelStyle) {\n  const preset = style.pattern ?? \"diagonal\";\n  let defaultScale = 1;\n  if (preset === \"cross\") {\n    defaultScale = 1.33;\n  }\n\n  return {\n    color:\n      style.patternColor?.trim() ||\n      (preset === \"accent\"\n        ? \"#e879f9\"\n        : heatmapPatternStrokeFallback(style.color)),\n    tileBackground: style.patternTileBackground?.trim() || style.color,\n    scale: style.patternScale ?? defaultScale,\n    strokeWidth: style.patternStrokeWidth,\n    radius: style.patternRadius,\n    complement: style.patternComplement,\n    fill: style.patternFill?.trim() || undefined,\n    dotFill: style.patternDotsFill,\n  };\n}\n\nexport function heatmapLevelCellFillOpacity(style: HeatmapLevelStyle): number {\n  if (!isHeatmapLevelPattern(style)) {\n    return 1;\n  }\n  return style.patternOpacity ?? 1;\n}\n\nexport function levelColorsFromStyles(\n  levelStyles: HeatmapLevelStyles\n): HeatmapLevelColors {\n  return [\n    levelStyles[0].color,\n    levelStyles[1].color,\n    levelStyles[2].color,\n    levelStyles[3].color,\n    levelStyles[4].color,\n  ];\n}\n\nexport function levelStylesFromColors(\n  levelColors: HeatmapLevelColors\n): HeatmapLevelStyles {\n  return [\n    { color: levelColors[0], fillMode: \"solid\", pattern: \"none\" },\n    { color: levelColors[1], fillMode: \"solid\", pattern: \"none\" },\n    { color: levelColors[2], fillMode: \"solid\", pattern: \"none\" },\n    { color: levelColors[3], fillMode: \"solid\", pattern: \"none\" },\n    { color: levelColors[4], fillMode: \"solid\", pattern: \"none\" },\n  ];\n}\n\nexport function resolveHeatmapLevelStyles(\n  levelColors?: HeatmapLevelColors,\n  levelStyles?: HeatmapLevelStyles\n): HeatmapLevelStyles {\n  if (levelStyles) {\n    return levelStyles;\n  }\n  if (levelColors) {\n    return levelStylesFromColors(levelColors);\n  }\n  return HEATMAP_DEFAULT_LEVEL_STYLES;\n}\n\nexport function buildHeatmapColorScale(\n  levelColors: HeatmapLevelColors\n): (count: number | null | undefined) => string {\n  return buildHeatmapColorScaleFromStyles(levelStylesFromColors(levelColors));\n}\n\nexport function buildHeatmapColorScaleFromStyles(\n  levelStyles: HeatmapLevelStyles\n): (count: number | null | undefined) => string {\n  return (count: number | null | undefined) => {\n    const level = getHeatmapContributionLevel(count ?? 0);\n    return levelStyles[level]?.color ?? levelStyles[0].color;\n  };\n}\n\nexport function buildHeatmapFillScale(\n  levelStyles: HeatmapLevelStyles\n): (count: number | null | undefined) => string {\n  return (count: number | null | undefined) => {\n    const level = getHeatmapContributionLevel(count ?? 0);\n    const style = levelStyles[level] ?? levelStyles[0];\n\n    if (isHeatmapLevelPattern(style)) {\n      return `url(#${heatmapLevelPatternId(level)})`;\n    }\n\n    return style.color;\n  };\n}\n\nexport const defaultHeatmapColorScale = buildHeatmapColorScale(\n  HEATMAP_DEFAULT_LEVEL_COLORS\n);\n\nexport const defaultHeatmapFillScale = buildHeatmapFillScale(\n  HEATMAP_DEFAULT_LEVEL_STYLES\n);\n",
      "type": "registry:component",
      "target": "components/charts/heatmap/heatmap-colors.ts"
    },
    {
      "path": "src/charts/chart-scale.ts",
      "content": "/** Sequential scale CSS variables for heatmaps, choropleths, and binned data (01 = lowest, 05 = highest). */\nexport const CHART_SCALE_VARS = [\n  \"var(--chart-scale-01)\",\n  \"var(--chart-scale-02)\",\n  \"var(--chart-scale-03)\",\n  \"var(--chart-scale-04)\",\n  \"var(--chart-scale-05)\",\n] as const;\n\nexport type ChartScaleVars = typeof CHART_SCALE_VARS;\n\nexport const chartScaleCssVars = {\n  scale01: CHART_SCALE_VARS[0],\n  scale02: CHART_SCALE_VARS[1],\n  scale03: CHART_SCALE_VARS[2],\n  scale04: CHART_SCALE_VARS[3],\n  scale05: CHART_SCALE_VARS[4],\n  patternColor: \"var(--chart-scale-pattern-color)\",\n} as const;\n",
      "type": "registry:component",
      "target": "components/charts/chart-scale.ts"
    },
    {
      "path": "src/charts/heatmap/heatmap-utils.ts",
      "content": "import type { HeatmapLevelStyles } from \"./heatmap-colors\";\nimport type { HeatmapBin, HeatmapColumn } from \"./heatmap-context\";\n\n/** Calendar months shown in default one-year contribution grids. */\nexport const HEATMAP_MONTHS_ONE_YEAR = 12;\n\n/** Half-year contribution grids (gallery card demos). */\nexport const HEATMAP_MONTHS_SIX = 6;\n\n/** Nominal week count for one year (~52). Default data uses calendar-month math instead. */\nexport const HEATMAP_WEEKS_ONE_YEAR = 52;\n\nconst MS_PER_DAY = 24 * 60 * 60 * 1000;\nconst MS_PER_WEEK = MS_PER_DAY * 7;\n\nexport function getHeatmapCalendarRangeStart(\n  today: Date,\n  months: number\n): Date {\n  const monthOffset = months === HEATMAP_MONTHS_SIX ? months : months - 1;\n  const start = new Date(\n    today.getFullYear(),\n    today.getMonth() - monthOffset,\n    1\n  );\n  start.setHours(0, 0, 0, 0);\n  return start;\n}\n\nexport function getHeatmapYearStartMonth(today: Date): Date {\n  return getHeatmapCalendarRangeStart(today, HEATMAP_MONTHS_ONE_YEAR);\n}\n\nexport function getHeatmapWeekStartSunday(date: Date): Date {\n  const sunday = new Date(date);\n  sunday.setDate(sunday.getDate() - sunday.getDay());\n  sunday.setHours(0, 0, 0, 0);\n  return sunday;\n}\n\nexport function getHeatmapWeekCount(startSunday: Date, endDate: Date): number {\n  const endSunday = getHeatmapWeekStartSunday(endDate);\n  return (\n    Math.floor((endSunday.getTime() - startSunday.getTime()) / MS_PER_WEEK) + 1\n  );\n}\n\n/** Days in a Sun–Sat column on or after `threshold` (for trimming partial lead weeks). */\nexport function countHeatmapWeekDaysOnOrAfter(\n  weekStart: Date,\n  threshold: Date\n): number {\n  const day = new Date(weekStart);\n  day.setHours(0, 0, 0, 0);\n  const cutoff = new Date(threshold);\n  cutoff.setHours(0, 0, 0, 0);\n  let count = 0;\n\n  for (let i = 0; i < 7; i++) {\n    if (day >= cutoff) {\n      count++;\n    }\n    day.setDate(day.getDate() + 1);\n  }\n\n  return count;\n}\n\n/**\n * First Sunday week column where enough days fall on/after `rangeStart`.\n * Skips a lead week that is mostly before the range (e.g. late July before Aug 1).\n */\nexport function getHeatmapWeekStartAlignedToRange(\n  rangeStart: Date,\n  minDaysInFirstWeek = 4\n): Date {\n  const startDate = getHeatmapWeekStartSunday(rangeStart);\n  const weekEnd = new Date(startDate);\n  weekEnd.setDate(weekEnd.getDate() + 6);\n\n  // Keep the Sun–Sat column that contains the 1st (e.g. Jan 1 in a partial lead week).\n  if (rangeStart >= startDate && rangeStart <= weekEnd) {\n    return startDate;\n  }\n\n  while (\n    countHeatmapWeekDaysOnOrAfter(startDate, rangeStart) < minDaysInFirstWeek\n  ) {\n    startDate.setDate(startDate.getDate() + 7);\n  }\n\n  return startDate;\n}\n\n/** Column index for a month label — snaps to separator group start when layout is set. */\nexport function getHeatmapMonthLabelColumnIndex(\n  columnIndex: number,\n  separatorLayout: Pick<HeatmapSeparatorLayout, \"atColumns\"> | null\n): number {\n  if (!separatorLayout?.atColumns.length) {\n    return columnIndex;\n  }\n\n  return getHeatmapSeparatorGroupStartColumn(\n    columnIndex,\n    separatorLayout.atColumns\n  );\n}\n\nexport interface HeatmapWeekRange {\n  startDate: Date;\n  weekCount: number;\n  /** First in-range calendar day; bins before this are empty in default year grids. */\n  rangeStart: Date | null;\n}\n\n/** Default `weeks` uses 12 calendar months; other values use a rolling week window. */\nexport function resolveHeatmapWeekRange(\n  today: Date,\n  weeks: number = HEATMAP_WEEKS_ONE_YEAR\n): HeatmapWeekRange {\n  const endDate = new Date(today);\n  endDate.setHours(0, 0, 0, 0);\n\n  if (weeks === HEATMAP_WEEKS_ONE_YEAR) {\n    const rangeStart = getHeatmapYearStartMonth(endDate);\n    const startDate = getHeatmapWeekStartAlignedToRange(rangeStart);\n    return {\n      startDate,\n      weekCount: getHeatmapWeekCount(startDate, endDate),\n      rangeStart,\n    };\n  }\n\n  const startDate = new Date(endDate);\n  startDate.setDate(startDate.getDate() - (weeks - 1) * 7);\n  startDate.setDate(startDate.getDate() - startDate.getDay());\n  startDate.setHours(0, 0, 0, 0);\n\n  return {\n    startDate,\n    weekCount: weeks,\n    rangeStart: null,\n  };\n}\n\n/** Month label anchor for a week column — prefers the 1st, else the 1st of the first bin's month. */\nexport function getHeatmapColumnMonthAnchor(\n  column: HeatmapColumn\n): Date | null {\n  for (const bin of column.bins) {\n    if (bin.date && bin.date.getDate() === 1) {\n      return bin.date;\n    }\n  }\n\n  const firstDate = column.bins[0]?.date;\n  if (!firstDate) {\n    return null;\n  }\n\n  return new Date(firstDate.getFullYear(), firstDate.getMonth(), 1);\n}\n\nexport function getHeatmapColumnStartDate(column: HeatmapColumn): Date | null {\n  return column.bins[0]?.date ?? null;\n}\n\nexport function getHeatmapColumnEndDate(column: HeatmapColumn): Date | null {\n  const lastBin = column.bins.at(-1);\n  return lastBin?.date ?? null;\n}\n\nexport function getHeatmapTimeExtent(\n  columns: HeatmapColumn[]\n): [Date, Date] | null {\n  if (columns.length === 0) {\n    return null;\n  }\n\n  const firstColumn = columns[0];\n  if (!firstColumn) {\n    return null;\n  }\n  const start = getHeatmapColumnStartDate(firstColumn);\n  const lastColumn = columns.at(-1);\n  if (!lastColumn) {\n    return null;\n  }\n  const end = getHeatmapColumnEndDate(lastColumn);\n  if (!(start && end)) {\n    return null;\n  }\n\n  return [start, end];\n}\n\nexport function filterHeatmapColumns(\n  columns: HeatmapColumn[],\n  xDomain?: [Date, Date]\n): HeatmapColumn[] {\n  if (!xDomain) {\n    return columns;\n  }\n\n  const start = Math.min(xDomain[0].getTime(), xDomain[1].getTime());\n  const end = Math.max(xDomain[0].getTime(), xDomain[1].getTime());\n\n  return columns.filter((column) => {\n    const weekStart = getHeatmapColumnStartDate(column)?.getTime();\n    const weekEnd = getHeatmapColumnEndDate(column)?.getTime();\n    if (weekStart == null || weekEnd == null) {\n      return false;\n    }\n    return weekEnd >= start && weekStart <= end;\n  });\n}\n\nconst heatmapTooltipMonthFmt = new Intl.DateTimeFormat(\"en-US\", {\n  month: \"long\",\n});\n\nconst heatmapTooltipWeekdayFmt = new Intl.DateTimeFormat(\"en-US\", {\n  weekday: \"long\",\n});\n\nfunction formatHeatmapOrdinalDay(day: number): string {\n  if (day >= 11 && day <= 13) {\n    return `${day}th`;\n  }\n\n  switch (day % 10) {\n    case 1:\n      return `${day}st`;\n    case 2:\n      return `${day}nd`;\n    case 3:\n      return `${day}rd`;\n    default:\n      return `${day}th`;\n  }\n}\n\n/** Tooltip header date — e.g. `January 20th 2026`. */\nexport function formatHeatmapTooltipDate(date: Date): string {\n  const month = heatmapTooltipMonthFmt.format(date);\n  const day = formatHeatmapOrdinalDay(date.getDate());\n  return `${month} ${day} ${date.getFullYear()}`;\n}\n\n/** Tooltip weekday line — e.g. `Monday`. */\nexport function formatHeatmapTooltipWeekday(date: Date): string {\n  return heatmapTooltipWeekdayFmt.format(date);\n}\n\n/** Tooltip contribution line — e.g. `3 contributions`. */\nexport function formatHeatmapContributionLabel(\n  count: number,\n  _date?: Date\n): string {\n  const word = count === 1 ? \"contribution\" : \"contributions\";\n  return `${count} ${word}`;\n}\n\n/** Sunday-first day labels for heatmap row bins. */\nexport const HEATMAP_DAY_LABELS = [\n  \"Sun\",\n  \"Mon\",\n  \"Tue\",\n  \"Wed\",\n  \"Thu\",\n  \"Fri\",\n  \"Sat\",\n] as const;\n\n/** First row of the grid — `0` = Sunday (GitHub default). */\nexport type HeatmapWeekStartDay = 0 | 1 | 2 | 3 | 4 | 5 | 6;\n\n/** Day labels with row 0 aligned to `weekStartDay`. */\nexport function getHeatmapDayLabels(\n  weekStartDay: HeatmapWeekStartDay = 0\n): readonly string[] {\n  if (weekStartDay === 0) {\n    return HEATMAP_DAY_LABELS;\n  }\n\n  return [\n    ...HEATMAP_DAY_LABELS.slice(weekStartDay),\n    ...HEATMAP_DAY_LABELS.slice(0, weekStartDay),\n  ];\n}\n\n/** Rotates Sun-first column bins so display row 0 starts on `weekStartDay`. */\nexport function rotateHeatmapColumnBins(\n  columns: HeatmapColumn[],\n  weekStartDay: HeatmapWeekStartDay = 0\n): HeatmapColumn[] {\n  if (weekStartDay === 0) {\n    return columns;\n  }\n\n  return columns.map((column) => ({\n    ...column,\n    bins: [\n      ...column.bins.slice(weekStartDay),\n      ...column.bins.slice(0, weekStartDay),\n    ],\n  }));\n}\n\n/** Which Y-axis row ticks to display. */\nexport type HeatmapYAxisTickFilter = \"all\" | \"odd\" | \"even\";\n\n/** Y-axis label display — `initial` shows the first letter only (Mon → M). */\nexport type HeatmapYAxisLabelFormat = \"full\" | \"initial\";\n\nexport function formatHeatmapYAxisLabel(\n  label: string,\n  labelFormat: HeatmapYAxisLabelFormat\n): string {\n  return labelFormat === \"initial\" ? label.charAt(0) : label;\n}\n\nexport function shouldShowHeatmapYAxisTick(\n  row: number,\n  tickFilter: HeatmapYAxisTickFilter\n): boolean {\n  switch (tickFilter) {\n    case \"all\":\n      return true;\n    case \"odd\":\n      return row % 2 === 1;\n    case \"even\":\n      return row % 2 === 0;\n    default:\n      return row % 2 === 1;\n  }\n}\n\n/** Layout spacing parsed from {@link HeatmapSeparator}. */\nexport type HeatmapSeparatorGroupBy = \"every\" | \"quarter\";\n\n/** Separator config from props — resolved to column indices once data is known. */\nexport interface HeatmapSeparatorParsedConfig {\n  groupBy: HeatmapSeparatorGroupBy;\n  every?: number;\n  spacing: number;\n}\n\nexport interface HeatmapSeparatorGroup {\n  startColumnIndex: number;\n  quarter: number;\n  year: number;\n  startDate: Date;\n  label: string;\n}\n\n/** Resolved separator layout used for column offsets and rendering. */\nexport interface HeatmapSeparatorLayout {\n  spacing: number;\n  atColumns: number[];\n  groups: HeatmapSeparatorGroup[];\n}\n\n/** Separator line style. */\nexport type HeatmapSeparatorStrokeStyle = \"solid\" | \"dashed\";\n\n/** Vertical stroke gradient for separator lines (`from` → optional `via` → `to`). */\nexport interface HeatmapSeparatorGradient {\n  from: string;\n  via?: string;\n  to: string;\n  fromOpacity?: number;\n  viaOpacity?: number;\n  toOpacity?: number;\n}\n\nexport interface HeatmapSeparatorGradientStop {\n  offset: string;\n  color: string;\n  opacity: number;\n}\n\n/** Builds SVG gradient stops for a vertical separator line. */\nexport function buildHeatmapSeparatorGradientStops(\n  gradient: HeatmapSeparatorGradient,\n  strokeOpacity = 1\n): HeatmapSeparatorGradientStop[] {\n  const scaleOpacity = (value: number | undefined, fallback = 1) =>\n    (value ?? fallback) * strokeOpacity;\n\n  if (gradient.via != null) {\n    return [\n      {\n        offset: \"0%\",\n        color: gradient.from,\n        opacity: scaleOpacity(gradient.fromOpacity),\n      },\n      {\n        offset: \"50%\",\n        color: gradient.via,\n        opacity: scaleOpacity(gradient.viaOpacity),\n      },\n      {\n        offset: \"100%\",\n        color: gradient.to,\n        opacity: scaleOpacity(gradient.toOpacity),\n      },\n    ];\n  }\n\n  return [\n    {\n      offset: \"0%\",\n      color: gradient.from,\n      opacity: scaleOpacity(gradient.fromOpacity),\n    },\n    {\n      offset: \"100%\",\n      color: gradient.to,\n      opacity: scaleOpacity(gradient.toOpacity),\n    },\n  ];\n}\n\nexport function resolveHeatmapSeparatorStrokeDasharray(\n  strokeStyle: HeatmapSeparatorStrokeStyle = \"solid\",\n  strokeDasharray?: string\n): string | undefined {\n  if (strokeStyle !== \"dashed\") {\n    return undefined;\n  }\n  return strokeDasharray ?? \"4,4\";\n}\n\n/** Calendar quarter (1–4) for Jan–Mar, Apr–Jun, Jul–Sep, Oct–Dec. */\nexport function getCalendarQuarter(date: Date): number {\n  return Math.floor(date.getMonth() / 3) + 1;\n}\n\nconst CALENDAR_QUARTER_START_MONTHS = [0, 3, 6, 9] as const;\n\n/** Jan/Apr/Jul/Oct 1 dates strictly after `gridStart` and on or before `gridEnd`. */\nexport function getCalendarQuarterStartDatesBetween(\n  gridStart: Date,\n  gridEnd: Date\n): Date[] {\n  const startTime = gridStart.getTime();\n  const endTime = gridEnd.getTime();\n  const dates: Date[] = [];\n\n  for (\n    let year = gridStart.getFullYear();\n    year <= gridEnd.getFullYear();\n    year++\n  ) {\n    for (const month of CALENDAR_QUARTER_START_MONTHS) {\n      const date = new Date(year, month, 1);\n      date.setHours(0, 0, 0, 0);\n      const time = date.getTime();\n      if (time > startTime && time <= endTime) {\n        dates.push(date);\n      }\n    }\n  }\n\n  return dates;\n}\n\n/** Week column index whose Sun–Sat span contains `date`. */\nexport function findHeatmapColumnIndexForDate(\n  columns: HeatmapColumn[],\n  date: Date\n): number | null {\n  const target = new Date(date);\n  target.setHours(0, 0, 0, 0);\n  const targetTime = target.getTime();\n\n  for (let columnIndex = 0; columnIndex < columns.length; columnIndex++) {\n    const column = columns[columnIndex];\n    if (!column) {\n      continue;\n    }\n\n    const weekStart = getHeatmapColumnStartDate(column);\n    const weekEnd = getHeatmapColumnEndDate(column);\n    if (!(weekStart && weekEnd)) {\n      continue;\n    }\n\n    weekStart.setHours(0, 0, 0, 0);\n    weekEnd.setHours(0, 0, 0, 0);\n    if (targetTime >= weekStart.getTime() && targetTime <= weekEnd.getTime()) {\n      return columnIndex;\n    }\n  }\n\n  return null;\n}\n\n/** Quarter anchor for a week column — prefers the 1st of a quarter month, else the quarter of the first bin. */\nexport function getHeatmapColumnQuarterAnchor(column: HeatmapColumn): {\n  quarter: number;\n  year: number;\n  date: Date;\n} | null {\n  for (const bin of column.bins) {\n    if (!bin.date) {\n      continue;\n    }\n    const month = bin.date.getMonth();\n    if (bin.date.getDate() === 1 && month % 3 === 0) {\n      return {\n        quarter: month / 3 + 1,\n        year: bin.date.getFullYear(),\n        date: bin.date,\n      };\n    }\n  }\n\n  const firstDate = column.bins[0]?.date;\n  if (!firstDate) {\n    return null;\n  }\n\n  const quarter = getCalendarQuarter(firstDate);\n  return {\n    quarter,\n    year: firstDate.getFullYear(),\n    date: new Date(firstDate.getFullYear(), (quarter - 1) * 3, 1),\n  };\n}\n\n/** Label column snapped to the start of a separator group. */\nexport function getHeatmapSeparatorGroupStartColumn(\n  columnIndex: number,\n  atColumns: number[]\n): number {\n  let groupStart = 0;\n\n  for (const start of atColumns) {\n    if (start <= columnIndex) {\n      groupStart = start;\n    } else {\n      break;\n    }\n  }\n\n  return groupStart;\n}\n\n/** Column indices (0-based) where a vertical separator is drawn (fixed interval). */\nexport function getHeatmapSeparatorColumnIndices(\n  columnCount: number,\n  every: number\n): number[] {\n  if (every <= 0 || columnCount <= every) {\n    return [];\n  }\n\n  const indices: number[] = [];\n  for (\n    let columnIndex = every;\n    columnIndex < columnCount;\n    columnIndex += every\n  ) {\n    indices.push(columnIndex);\n  }\n  return indices;\n}\n\n/** Matches demo-style calendar grids to their range start (Jan 1, etc.). */\nexport function inferHeatmapCalendarRangeStart(\n  columns: HeatmapColumn[]\n): Date | null {\n  const firstColumn = columns[0];\n  if (!firstColumn) {\n    return null;\n  }\n\n  const gridStart = getHeatmapColumnStartDate(firstColumn);\n  if (!gridStart) {\n    return null;\n  }\n\n  const today = new Date();\n  today.setHours(0, 0, 0, 0);\n\n  for (const months of [HEATMAP_MONTHS_SIX, HEATMAP_MONTHS_ONE_YEAR]) {\n    const rangeStart = getHeatmapCalendarRangeStart(today, months);\n    const alignedStart = getHeatmapWeekStartAlignedToRange(rangeStart);\n    if (gridStart.getTime() === alignedStart.getTime()) {\n      return rangeStart;\n    }\n  }\n\n  return null;\n}\n\nexport function buildHeatmapQuarterSeparatorGroups(\n  columns: HeatmapColumn[]\n): HeatmapSeparatorGroup[] {\n  if (columns.length === 0) {\n    return [];\n  }\n\n  const extent = getHeatmapTimeExtent(columns);\n  if (!extent) {\n    return [];\n  }\n\n  const [extentStart, gridEnd] = extent;\n  const displayRange = resolveHeatmapDisplayRange(columns);\n  const quarterRangeStart =\n    displayRange.start ??\n    inferHeatmapCalendarRangeStart(columns) ??\n    extentStart;\n  const firstQuarter = getCalendarQuarter(quarterRangeStart);\n\n  const groups: HeatmapSeparatorGroup[] = [\n    {\n      startColumnIndex: 0,\n      quarter: firstQuarter,\n      year: quarterRangeStart.getFullYear(),\n      startDate: quarterRangeStart,\n      label: `Q${firstQuarter}`,\n    },\n  ];\n\n  const usedColumns = new Set<number>([0]);\n  const quarterStarts = getCalendarQuarterStartDatesBetween(\n    quarterRangeStart,\n    gridEnd\n  );\n\n  for (const quarterStart of quarterStarts) {\n    const columnIndex = findHeatmapColumnIndexForDate(columns, quarterStart);\n    if (\n      columnIndex == null ||\n      columnIndex === 0 ||\n      usedColumns.has(columnIndex)\n    ) {\n      continue;\n    }\n\n    usedColumns.add(columnIndex);\n    const quarter = getCalendarQuarter(quarterStart);\n    groups.push({\n      startColumnIndex: columnIndex,\n      quarter,\n      year: quarterStart.getFullYear(),\n      startDate: quarterStart,\n      label: `Q${quarter}`,\n    });\n  }\n\n  groups.sort((a, b) => a.startColumnIndex - b.startColumnIndex);\n  return groups;\n}\n\nexport function resolveHeatmapSeparatorLayout(\n  config: HeatmapSeparatorParsedConfig | null,\n  columns: HeatmapColumn[]\n): HeatmapSeparatorLayout | null {\n  if (!config) {\n    return null;\n  }\n\n  if (config.groupBy === \"quarter\") {\n    const groups = buildHeatmapQuarterSeparatorGroups(columns);\n    if (groups.length === 0) {\n      return null;\n    }\n\n    return {\n      spacing: config.spacing,\n      atColumns: groups\n        .map((group) => group.startColumnIndex)\n        .filter((columnIndex) => columnIndex > 0),\n      groups,\n    };\n  }\n\n  if (!config.every || config.every <= 0) {\n    return null;\n  }\n\n  const atColumns = getHeatmapSeparatorColumnIndices(\n    columns.length,\n    config.every\n  );\n\n  return {\n    spacing: config.spacing,\n    atColumns,\n    groups: [],\n  };\n}\n\nexport function getHeatmapSeparatorCount(\n  separator: Pick<HeatmapSeparatorLayout, \"atColumns\"> | null\n): number {\n  return separator?.atColumns.length ?? 0;\n}\n\n/** Extra x-offset for a column when separator spacing is enabled. */\nexport function getHeatmapColumnXOffset(\n  columnIndex: number,\n  separator: Pick<HeatmapSeparatorLayout, \"spacing\" | \"atColumns\"> | null\n): number {\n  if (!separator || separator.spacing <= 0) {\n    return 0;\n  }\n  if (columnIndex <= 0) {\n    return 0;\n  }\n\n  const separatorCount = separator.atColumns.filter(\n    (atColumn) => atColumn <= columnIndex\n  ).length;\n  return separatorCount * separator.spacing;\n}\n\nexport function getHeatmapPlotInnerWidth(\n  columnCount: number,\n  binWidth: number,\n  separator: Pick<HeatmapSeparatorLayout, \"spacing\" | \"atColumns\"> | null\n): number {\n  const separatorCount = separator ? getHeatmapSeparatorCount(separator) : 0;\n  return columnCount * binWidth + separatorCount * (separator?.spacing ?? 0);\n}\n\n/** Vertical span for a separator line in plot coordinates. */\nexport function getHeatmapSeparatorLineY({\n  innerHeight,\n  marginTop,\n  startOffset,\n  paddingY = 0,\n}: {\n  innerHeight: number;\n  marginTop: number;\n  /** Distance from the chart container top to the line start. Default: plot top. */\n  startOffset?: number;\n  paddingY?: number;\n}): { y1: number; y2: number } {\n  const resolvedStart = startOffset ?? marginTop;\n  const y1 = resolvedStart - marginTop + paddingY;\n  const y2 = Math.max(innerHeight - paddingY, y1);\n  return { y1, y2 };\n}\n\n/** X position for a separator line (centered in the gutter when spacing > 0). */\nexport function getHeatmapSeparatorX(\n  columnIndex: number,\n  gap: number,\n  separator: Pick<HeatmapSeparatorLayout, \"spacing\">,\n  xScale: (columnIndex: number) => number\n): number {\n  if (separator.spacing > 0) {\n    return xScale(columnIndex) - separator.spacing / 2;\n  }\n  return xScale(columnIndex) - gap / 2;\n}\n\nexport interface HeatmapDisplayRange {\n  start: Date | null;\n  end: Date | null;\n}\n\n/** Whether a bin falls outside the contribution display window (not merely inactive). */\nexport function isHeatmapGhostBin(\n  bin: HeatmapBin,\n  range: HeatmapDisplayRange\n): boolean {\n  const time = bin.date.getTime();\n  if (range.end && time > range.end.getTime()) {\n    return true;\n  }\n  if (range.start && time < range.start.getTime()) {\n    return true;\n  }\n  return false;\n}\n\n/**\n * Infers GitHub-style display range for calendar-month contribution grids.\n * Custom data that does not match a known grid shape returns null bounds (show all).\n */\nexport function resolveHeatmapDisplayRange(\n  columns: HeatmapColumn[]\n): HeatmapDisplayRange {\n  if (columns.length === 0) {\n    return { start: null, end: null };\n  }\n\n  const extent = getHeatmapTimeExtent(columns);\n  if (!extent) {\n    return { start: null, end: null };\n  }\n\n  const today = new Date();\n  today.setHours(0, 0, 0, 0);\n\n  const firstColumn = columns[0];\n  if (!firstColumn) {\n    return { start: null, end: null };\n  }\n\n  const gridStart = getHeatmapColumnStartDate(firstColumn);\n  if (!gridStart) {\n    return { start: null, end: null };\n  }\n\n  const inferredStart = inferHeatmapCalendarRangeStart(columns);\n\n  if (inferredStart && extent[1].getTime() >= today.getTime()) {\n    return { start: inferredStart, end: today };\n  }\n\n  return { start: null, end: null };\n}\n\n/** Maps a contribution count to the GitHub-style legend level (0–4). */\nexport function getHeatmapContributionLevel(count: number): number {\n  if (count <= 0) {\n    return 0;\n  }\n  if (count === 1) {\n    return 1;\n  }\n  if (count === 2) {\n    return 2;\n  }\n  if (count === 3) {\n    return 3;\n  }\n  return 4;\n}\n\nexport interface HeatmapHoverStyleParams {\n  inactiveOpacity: number;\n  inactiveScale: number;\n  activeScale: number;\n}\n\n/** Whether hover styling runs (disabled when all scale/opacity props are 1). */\nexport function isHeatmapHoverEffectEnabled(\n  params: HeatmapHoverStyleParams\n): boolean {\n  return (\n    params.inactiveOpacity !== 1 ||\n    params.inactiveScale !== 1 ||\n    params.activeScale !== 1\n  );\n}\n\n/** Whether inactive hover styling runs (disabled when both props are 1). */\nexport function isHeatmapInactiveEffectEnabled(\n  inactiveOpacity: number,\n  inactiveScale: number\n): boolean {\n  return isHeatmapHoverEffectEnabled({\n    inactiveOpacity,\n    inactiveScale,\n    activeScale: 1,\n  });\n}\n\n/** Opacity and scale for highlighted vs dimmed cells and legend swatches. */\nexport function resolveHeatmapHoverStyle(\n  isHighlighted: boolean,\n  isDimmed: boolean,\n  params: HeatmapHoverStyleParams\n): { opacity: number; scale: number } {\n  if (isHighlighted && params.activeScale !== 1) {\n    return { opacity: 1, scale: params.activeScale };\n  }\n\n  if (isDimmed) {\n    return {\n      opacity: params.inactiveOpacity,\n      scale: params.inactiveScale,\n    };\n  }\n\n  return { opacity: 1, scale: 1 };\n}\n\n/** Opacity and scale for an inactive cell or legend swatch. */\nexport function resolveHeatmapInactiveStyle(\n  isInactive: boolean,\n  inactiveOpacity: number,\n  inactiveScale: number\n): { opacity: number; scale: number } {\n  return resolveHeatmapHoverStyle(false, isInactive, {\n    inactiveOpacity,\n    inactiveScale,\n    activeScale: 1,\n  });\n}\n\n/** Per-row opacity multiplier for display rows (default 1). */\nexport function resolveHeatmapRowOpacity(\n  row: number,\n  rowOpacity?: number | readonly number[]\n): number {\n  if (rowOpacity == null) {\n    return 1;\n  }\n\n  if (typeof rowOpacity === \"number\") {\n    return rowOpacity;\n  }\n\n  return rowOpacity[row] ?? 1;\n}\n\n/**\n * Builds a per-row opacity map for {@link HeatmapCells} and {@link HeatmapYAxis}.\n * Pass explicit row indices (e.g. `[5, 6]` for display Sat/Sun when `weekStartDay={1}`)\n * or a predicate — `(row) => row >= 5` fades the last two rows.\n */\nexport function buildHeatmapRowOpacity(\n  match: readonly number[] | ((row: number) => boolean),\n  fadedOpacity = 0.35,\n  activeOpacity = 1,\n  rowCount = 7\n): number[] {\n  if (typeof match === \"function\") {\n    return Array.from({ length: rowCount }, (_, row) =>\n      match(row) ? fadedOpacity : activeOpacity\n    );\n  }\n\n  const opacity = new Array<number>(rowCount).fill(activeOpacity);\n  for (const row of match) {\n    if (row >= 0 && row < rowCount) {\n      opacity[row] = fadedOpacity;\n    }\n  }\n  return opacity;\n}\n\n/** CSS `linear-gradient` for a continuous legend bar from level styles. */\nexport function buildHeatmapLegendGradient(\n  levelStyles: HeatmapLevelStyles\n): string {\n  const lastIndex = levelStyles.length - 1;\n  const stops = levelStyles.map((style, index) => {\n    const offset = lastIndex === 0 ? 0 : (index / lastIndex) * 100;\n    return `${style.color} ${offset}%`;\n  });\n\n  return `linear-gradient(to right, ${stops.join(\", \")})`;\n}\n",
      "type": "registry:component",
      "target": "components/charts/heatmap/heatmap-utils.ts"
    },
    {
      "path": "src/charts/heatmap/heatmap-animation.ts",
      "content": "import type { Transition } from \"motion/react\";\nimport type { HeatmapColumn } from \"./heatmap-context\";\nimport { getHeatmapContributionLevel } from \"./heatmap-utils\";\n\n/** Default enter duration when no motion props are passed (ms). */\nexport const HEATMAP_DEFAULT_ENTER_DURATION_MS = 1600;\n\n/** Default cubic-bezier for heatmap cell enter / loading transitions. */\nexport const HEATMAP_DEFAULT_ENTER_EASE = [0.85, 0, 0.916, 0.282] as const;\n\n/** Default enter transition for {@link HeatmapChart}. */\nexport const HEATMAP_DEFAULT_ENTER_TRANSITION: Transition = {\n  type: \"tween\",\n  duration: HEATMAP_DEFAULT_ENTER_DURATION_MS / 1000,\n  ease: HEATMAP_DEFAULT_ENTER_EASE,\n};\n\n/** Chart opacity while `status=\"loading\"`. */\nexport const HEATMAP_LOADING_CHART_OPACITY = 1;\n\n/** Default max per-cell opacity during loading shimmer. */\nexport const HEATMAP_DEFAULT_LOADING_CELL_MAX_OPACITY = 0.85;\n\n/** Default share of cells that participate in loading shimmer (0–1). */\nexport const HEATMAP_DEFAULT_LOADING_CELL_RANDOMNESS = 1;\n\n/** Opacity for loading cells that do not participate in shimmer. */\nexport const HEATMAP_LOADING_BASE_CELL_OPACITY = 0.2;\n\n/** Duration for ready → loading cell conceal before shimmer resumes (ms). */\nexport const HEATMAP_LOADING_CONCEAL_MS = 450;\n\n/** Share of the enter window used for random fade-in delays. */\nexport const HEATMAP_ENTER_STAGGER_SPREAD = 0.6;\n\nfunction seededRandom(seed: number): () => number {\n  let state = seed % 2_147_483_647;\n  if (state <= 0) {\n    state += 2_147_483_646;\n  }\n  return () => {\n    state = (state * 16_807) % 2_147_483_647;\n    return (state - 1) / 2_147_483_646;\n  };\n}\n\nexport function heatmapCellSeed(column: number, row: number): number {\n  return column * 1009 + row * 9176;\n}\n\nexport interface HeatmapLevelRange {\n  min: number;\n  max: number;\n}\n\n/** Min/max contribution levels present in the dataset. */\nexport function computeHeatmapLevelRange(\n  data: HeatmapColumn[]\n): HeatmapLevelRange {\n  let min = Number.POSITIVE_INFINITY;\n  let max = Number.NEGATIVE_INFINITY;\n\n  for (const column of data) {\n    for (const bin of column.bins) {\n      const level = getHeatmapContributionLevel(bin.count);\n      if (level < min) {\n        min = level;\n      }\n      if (level > max) {\n        max = level;\n      }\n    }\n  }\n\n  if (!(Number.isFinite(min) && Number.isFinite(max))) {\n    return { min: 0, max: 4 };\n  }\n\n  return { min, max };\n}\n\n/** Per-cell fade duration derived from the motion enter transition. */\nexport function resolveHeatmapEnterFadeDurationSec(\n  enterTransition: Transition | undefined,\n  animationDurationMs: number\n): number {\n  if (\n    enterTransition &&\n    \"duration\" in enterTransition &&\n    typeof enterTransition.duration === \"number\"\n  ) {\n    return enterTransition.duration;\n  }\n\n  return Math.min(0.45, (animationDurationMs / 1000) * 0.3);\n}\n\n/** Random delay so all cells finish fading in within `animationDurationMs`. */\nexport function computeHeatmapEnterFadeDelayMs({\n  column,\n  row,\n  revealEpoch,\n  animationDurationMs,\n  enterStaggerScale,\n  fadeDurationSec,\n}: {\n  column: number;\n  row: number;\n  revealEpoch: number;\n  animationDurationMs: number;\n  enterStaggerScale: number;\n  fadeDurationSec: number;\n}): number {\n  const random = seededRandom(\n    heatmapCellSeed(column, row) + revealEpoch * 524_287\n  );\n  const fadeMs = fadeDurationSec * 1000;\n  const maxDelayMs = Math.max(0, animationDurationMs - fadeMs);\n  const spreadMs =\n    maxDelayMs *\n    HEATMAP_ENTER_STAGGER_SPREAD *\n    Math.max(enterStaggerScale, 0.25);\n\n  return random() * spreadMs;\n}\n\n/** Whether a cell participates in loading shimmer for a given randomness (0–1). */\nexport function heatmapLoadingCellParticipates(\n  column: number,\n  row: number,\n  randomness: number\n): boolean {\n  if (randomness >= 1) {\n    return true;\n  }\n  if (randomness <= 0) {\n    return false;\n  }\n\n  const random = seededRandom(heatmapCellSeed(column, row) + 73_133);\n  return random() < randomness;\n}\n",
      "type": "registry:component",
      "target": "components/charts/heatmap/heatmap-animation.ts"
    },
    {
      "path": "src/charts/heatmap/heatmap-pattern-defs.tsx",
      "content": "\"use client\";\n\nimport { Fragment, memo } from \"react\";\nimport { renderPatternPreset } from \"../pattern-preset\";\nimport {\n  type HeatmapLevelStyles,\n  heatmapLevelPatternId,\n  heatmapLevelPatternRenderOptions,\n  isHeatmapLevelPattern,\n} from \"./heatmap-colors\";\n\nexport const HeatmapPatternDefs = memo(function HeatmapPatternDefs({\n  levelStyles,\n}: {\n  levelStyles: HeatmapLevelStyles;\n}) {\n  const nodes = levelStyles.flatMap((style, level) => {\n    if (!isHeatmapLevelPattern(style)) {\n      return [];\n    }\n\n    const id = heatmapLevelPatternId(level);\n    const pattern = style.pattern;\n    if (!pattern) {\n      return [];\n    }\n\n    const node = renderPatternPreset(\n      pattern,\n      id,\n      heatmapLevelPatternRenderOptions(style)\n    );\n\n    return node ? [<Fragment key={id}>{node}</Fragment>] : [];\n  });\n\n  if (nodes.length === 0) {\n    return null;\n  }\n\n  return <defs>{nodes}</defs>;\n});\n\nHeatmapPatternDefs.displayName = \"HeatmapPatternDefs\";\n",
      "type": "registry:component",
      "target": "components/charts/heatmap/heatmap-pattern-defs.tsx"
    },
    {
      "path": "src/charts/heatmap/heatmap-chart-loading.tsx",
      "content": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport type { Margin } from \"../chart-context\";\nimport { generateHeatmapSkeletonFromTarget } from \"./generate-heatmap-skeleton-data\";\nimport { HeatmapCells } from \"./heatmap-cells\";\nimport { HeatmapChart } from \"./heatmap-chart\";\nimport type { HeatmapColumn } from \"./heatmap-context\";\nimport { HeatmapXAxis } from \"./heatmap-x-axis\";\nimport { HeatmapYAxis } from \"./heatmap-y-axis\";\n\nexport interface HeatmapChartLoadingProps {\n  /** Target column data used to size the skeleton grid. */\n  data: HeatmapColumn[];\n  /** Visible time range — filters week columns that overlap the domain. */\n  xDomain?: [Date, Date];\n  /** Chart margins */\n  margin?: Partial<Margin>;\n  /** Gap between cells in pixels. Default: 2 */\n  gap?: number;\n  /** Corner radius for each cell. Default: 2 */\n  cornerRadius?: number;\n  /** Centered shimmer label text. Default: \"Loading\" */\n  label?: string;\n  /** Additional class name for the container */\n  className?: string;\n}\n\nexport function HeatmapChartLoading({\n  data,\n  xDomain,\n  margin,\n  gap = 2,\n  cornerRadius = 2,\n  label = \"Loading\",\n  className = \"\",\n}: HeatmapChartLoadingProps) {\n  const skeletonData = useMemo(\n    () => generateHeatmapSkeletonFromTarget(data),\n    [data]\n  );\n\n  return (\n    <HeatmapChart\n      className={className}\n      data={skeletonData}\n      gap={gap}\n      loadingLabel={label}\n      margin={margin}\n      status=\"loading\"\n      xDomain={xDomain}\n    >\n      <HeatmapCells cornerRadius={cornerRadius} interactive={false} />\n      <HeatmapXAxis />\n      <HeatmapYAxis />\n    </HeatmapChart>\n  );\n}\n\nexport default HeatmapChartLoading;\n",
      "type": "registry:component",
      "target": "components/charts/heatmap/heatmap-chart-loading.tsx"
    },
    {
      "path": "src/charts/heatmap/generate-heatmap-skeleton-data.ts",
      "content": "import type { HeatmapColumn } from \"./heatmap-context\";\n\n/** Placeholder grid with the same week/day shape as target data (all empty cells). */\nexport function generateHeatmapSkeletonFromTarget(\n  target: HeatmapColumn[]\n): HeatmapColumn[] {\n  return target.map((column) => ({\n    bin: column.bin,\n    bins: column.bins.map((bin) => ({\n      bin: bin.bin,\n      count: 0,\n      date: bin.date,\n    })),\n  }));\n}\n",
      "type": "registry:component",
      "target": "components/charts/heatmap/generate-heatmap-skeleton-data.ts"
    },
    {
      "path": "src/charts/heatmap/index.ts",
      "content": "export { generateHeatmapSkeletonFromTarget } from \"./generate-heatmap-skeleton-data\";\nexport {\n  computeHeatmapEnterFadeDelayMs,\n  computeHeatmapLevelRange,\n  HEATMAP_DEFAULT_ENTER_DURATION_MS,\n  HEATMAP_DEFAULT_ENTER_EASE,\n  HEATMAP_DEFAULT_ENTER_TRANSITION,\n  HEATMAP_DEFAULT_LOADING_CELL_MAX_OPACITY,\n  HEATMAP_DEFAULT_LOADING_CELL_RANDOMNESS,\n  HEATMAP_ENTER_STAGGER_SPREAD,\n  HEATMAP_LOADING_BASE_CELL_OPACITY,\n  HEATMAP_LOADING_CHART_OPACITY,\n  HEATMAP_LOADING_CONCEAL_MS,\n  type HeatmapLevelRange,\n  heatmapLoadingCellParticipates,\n  resolveHeatmapEnterFadeDurationSec,\n} from \"./heatmap-animation\";\nexport { HeatmapCells, type HeatmapCellsProps } from \"./heatmap-cells\";\nexport {\n  HeatmapChart,\n  type HeatmapChartProps,\n  type HeatmapLayout,\n} from \"./heatmap-chart\";\nexport {\n  HeatmapChartLoading,\n  type HeatmapChartLoadingProps,\n} from \"./heatmap-chart-loading\";\nexport {\n  buildHeatmapColorScale,\n  buildHeatmapColorScaleFromStyles,\n  buildHeatmapFillScale,\n  defaultHeatmapColorScale,\n  defaultHeatmapFillScale,\n  HEATMAP_DEFAULT_LEVEL_COLORS,\n  HEATMAP_DEFAULT_LEVEL_STYLES,\n  type HeatmapLevelColors,\n  type HeatmapLevelFillMode,\n  type HeatmapLevelStyle,\n  type HeatmapLevelStyles,\n  heatmapLevelPatternId,\n  isHeatmapLevelPattern,\n  levelColorsFromStyles,\n  levelStylesFromColors,\n  resolveHeatmapLevelStyles,\n} from \"./heatmap-colors\";\nexport {\n  type HeatmapBin,\n  type HeatmapColumn,\n  type HeatmapContextValue,\n  HeatmapInteractionBoundary,\n  HeatmapInteractionProvider,\n  HeatmapInteractionRoot,\n  HeatmapProvider,\n  type HeatmapRevealMode,\n  heatmapCssVars,\n  useHeatmap,\n  useHeatmapInteraction,\n  useHeatmapInteractionOptional,\n} from \"./heatmap-context\";\nexport {\n  HEATMAP_LEGEND_LEVELS,\n  HeatmapLegend,\n  type HeatmapLegendProps,\n  type HeatmapLegendVariant,\n} from \"./heatmap-legend\";\nexport {\n  resolveHeatmapSeparatorConfig,\n  resolveHeatmapSeparatorConfigWithData,\n} from \"./heatmap-resolve-separator\";\nexport {\n  HeatmapSeparator,\n  type HeatmapSeparatorProps,\n} from \"./heatmap-separator\";\nexport { HeatmapTooltip, type HeatmapTooltipProps } from \"./heatmap-tooltip\";\nexport {\n  buildHeatmapLegendGradient,\n  buildHeatmapQuarterSeparatorGroups,\n  buildHeatmapRowOpacity,\n  buildHeatmapSeparatorGradientStops,\n  countHeatmapWeekDaysOnOrAfter,\n  filterHeatmapColumns,\n  findHeatmapColumnIndexForDate,\n  formatHeatmapContributionLabel,\n  formatHeatmapTooltipDate,\n  formatHeatmapTooltipWeekday,\n  formatHeatmapYAxisLabel,\n  getCalendarQuarter,\n  getCalendarQuarterStartDatesBetween,\n  getHeatmapCalendarRangeStart,\n  getHeatmapColumnMonthAnchor,\n  getHeatmapColumnQuarterAnchor,\n  getHeatmapColumnXOffset,\n  getHeatmapContributionLevel,\n  getHeatmapDayLabels,\n  getHeatmapMonthLabelColumnIndex,\n  getHeatmapPlotInnerWidth,\n  getHeatmapSeparatorColumnIndices,\n  getHeatmapSeparatorCount,\n  getHeatmapSeparatorGroupStartColumn,\n  getHeatmapSeparatorLineY,\n  getHeatmapSeparatorX,\n  getHeatmapTimeExtent,\n  getHeatmapWeekCount,\n  getHeatmapWeekStartAlignedToRange,\n  getHeatmapWeekStartSunday,\n  getHeatmapYearStartMonth,\n  HEATMAP_DAY_LABELS,\n  HEATMAP_MONTHS_ONE_YEAR,\n  HEATMAP_MONTHS_SIX,\n  HEATMAP_WEEKS_ONE_YEAR,\n  type HeatmapDisplayRange,\n  type HeatmapHoverStyleParams,\n  type HeatmapSeparatorGradient,\n  type HeatmapSeparatorGradientStop,\n  type HeatmapSeparatorGroup,\n  type HeatmapSeparatorGroupBy,\n  type HeatmapSeparatorLayout,\n  type HeatmapSeparatorParsedConfig,\n  type HeatmapSeparatorStrokeStyle,\n  type HeatmapWeekRange,\n  type HeatmapWeekStartDay,\n  type HeatmapYAxisLabelFormat,\n  type HeatmapYAxisTickFilter,\n  inferHeatmapCalendarRangeStart,\n  isHeatmapGhostBin,\n  isHeatmapHoverEffectEnabled,\n  isHeatmapInactiveEffectEnabled,\n  resolveHeatmapDisplayRange,\n  resolveHeatmapHoverStyle,\n  resolveHeatmapInactiveStyle,\n  resolveHeatmapRowOpacity,\n  resolveHeatmapSeparatorLayout,\n  resolveHeatmapSeparatorStrokeDasharray,\n  resolveHeatmapWeekRange,\n  rotateHeatmapColumnBins,\n  shouldShowHeatmapYAxisTick,\n} from \"./heatmap-utils\";\nexport { HeatmapXAxis, type HeatmapXAxisProps } from \"./heatmap-x-axis\";\nexport { HeatmapYAxis, type HeatmapYAxisProps } from \"./heatmap-y-axis\";\n",
      "type": "registry:component",
      "target": "components/charts/heatmap/index.ts"
    },
    {
      "path": "src/charts/chart-loading-label.tsx",
      "content": "\"use client\";\n\nimport { motion } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\nimport { ShimmeringText } from \"../components/shimmering-text\";\nimport {\n  LINE_LOADING_PULSE_EASE,\n  LOADING_LABEL_EXIT_S,\n  LOADING_LABEL_EXIT_Y_PX,\n} from \"./line-loading-timing\";\n\nexport interface ChartLoadingLabelProps {\n  /** Label shown centered over the chart. */\n  text?: string;\n  className?: string;\n  /** Animate down, fade, and blur during loading → ready handoff. */\n  exiting?: boolean;\n}\n\nexport function ChartLoadingLabel({\n  text = \"Loading\",\n  className,\n  exiting = false,\n}: ChartLoadingLabelProps) {\n  if (!text.trim()) {\n    return null;\n  }\n\n  return (\n    <motion.div\n      animate={{\n        y: exiting ? LOADING_LABEL_EXIT_Y_PX : 0,\n        opacity: exiting ? 0 : 1,\n        filter: exiting ? \"blur(2px)\" : \"blur(0px)\",\n      }}\n      aria-live=\"polite\"\n      className={cn(\n        \"pointer-events-none absolute inset-0 flex items-center justify-center\",\n        className\n      )}\n      initial={false}\n      role=\"status\"\n      transition={{\n        duration: LOADING_LABEL_EXIT_S,\n        ease: [...LINE_LOADING_PULSE_EASE],\n      }}\n    >\n      <ShimmeringText\n        className=\"font-medium text-sm tracking-wide [--color:var(--muted-foreground)] [--shimmering-color:var(--foreground)]\"\n        text={text}\n      />\n    </motion.div>\n  );\n}\n\nexport default ChartLoadingLabel;\n",
      "type": "registry:component",
      "target": "components/charts/chart-loading-label.tsx"
    },
    {
      "path": "src/charts/chart-child-passthrough.ts",
      "content": "import {\n  Children,\n  cloneElement,\n  Fragment,\n  isValidElement,\n  type ReactElement,\n  type ReactNode,\n} from \"react\";\n\n/** Marker on wrapper components whose single child should inherit clip classification. */\nexport const CHART_CLIP_PASSTHROUGH = \"__chartClipPassthrough\" as const;\n\nexport function isChartClipPassthrough(type: unknown): boolean {\n  return (\n    typeof type === \"function\" &&\n    (type as { [CHART_CLIP_PASSTHROUGH]?: boolean })[CHART_CLIP_PASSTHROUGH] ===\n      true\n  );\n}\n\n/** Unwrap visibility wrappers so `Grid` / axes stay outside the series clip. */\nexport function resolveChartChildElement(child: ReactElement): ReactElement {\n  if (isChartClipPassthrough(child.type)) {\n    const inner = (child.props as { children?: unknown }).children;\n    if (isValidElement(inner)) {\n      return resolveChartChildElement(inner);\n    }\n  }\n  return child;\n}\n\n/** Walk chart children, flattening React fragments (studio often groups layers in `<>...</>`). */\nexport function forEachChartChild(\n  children: ReactNode,\n  callback: (child: ReactElement, index: number) => void\n) {\n  let index = 0;\n  const visit = (nodes: ReactNode) => {\n    Children.forEach(nodes, (child) => {\n      if (!isValidElement(child)) {\n        return;\n      }\n      if (child.type === Fragment) {\n        visit((child.props as { children?: ReactNode }).children);\n        return;\n      }\n      callback(child, index);\n      index += 1;\n    });\n  };\n  visit(children);\n}\n\nconst CLIP_EXCLUDED_COMPONENT_NAMES = new Set([\n  \"Background\",\n  \"Grid\",\n  \"XAxis\",\n  \"YAxis\",\n  \"BarXAxis\",\n  \"BarYAxis\",\n  \"LiveXAxis\",\n  \"LiveYAxis\",\n]);\n\nconst UNDERLAY_COMPONENT_NAMES = new Set([\"ReferenceArea\", \"BarColumnTrack\"]);\n\n/** Markers render after the interaction overlay so they stay clickable. */\nexport function isPostOverlayComponent(child: ReactElement): boolean {\n  const childType = child.type as {\n    displayName?: string;\n    name?: string;\n    __isChartMarkers?: boolean;\n    __isPostOverlay?: boolean;\n  };\n\n  if (childType.__isChartMarkers || childType.__isPostOverlay) {\n    return true;\n  }\n\n  const componentName =\n    typeof child.type === \"function\"\n      ? childType.displayName || childType.name || \"\"\n      : \"\";\n\n  return (\n    componentName === \"ChartMarkers\" ||\n    componentName === \"MarkerGroup\" ||\n    componentName === \"ChartBrush\"\n  );\n}\n\n/** Renders above grid/axes but below series; excluded from grow-clip reveal. */\nexport function isUnderlayComponent(child: ReactElement): boolean {\n  const childType = child.type as { displayName?: string; name?: string };\n  const componentName =\n    typeof child.type === \"function\"\n      ? childType.displayName || childType.name || \"\"\n      : \"\";\n  return UNDERLAY_COMPONENT_NAMES.has(componentName);\n}\n\n/** Grid and axes stay visible during series clip reveal (e.g. loading → ready). */\nexport function isClipExcludedComponent(child: ReactElement): boolean {\n  const childType = child.type as { displayName?: string; name?: string };\n  const componentName =\n    typeof child.type === \"function\"\n      ? childType.displayName || childType.name || \"\"\n      : \"\";\n  return CLIP_EXCLUDED_COMPONENT_NAMES.has(componentName);\n}\n\n/** SVG layer lists from chart shells need stable keys when rendered as arrays. */\nexport function renderKeyedChartLayers(children: ReactElement[]) {\n  return children.map((child, index) =>\n    cloneElement(child, { key: child.key ?? `chart-layer-${index}` })\n  );\n}\n",
      "type": "registry:component",
      "target": "components/charts/chart-child-passthrough.ts"
    }
  ]
}