{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "recharts-heatmap-chart",
  "description": "Calendar heatmap component rendered with Recharts — GitHub contribution graph style",
  "dependencies": [
    "recharts",
    "motion"
  ],
  "registryDependencies": [
    "@evilcharts/recharts-chart",
    "@evilcharts/recharts-tooltip"
  ],
  "files": [
    {
      "path": "src/registry/charts/recharts-heatmap-chart.tsx",
      "content": "\"use client\";\n\nimport {\n  Children,\n  createContext,\n  isValidElement,\n  memo,\n  use,\n  useCallback,\n  useEffect,\n  useId,\n  useMemo,\n  useState,\n  useSyncExternalStore,\n  type ComponentProps,\n  type FC,\n  type KeyboardEvent,\n  type ReactNode,\n} from \"react\";\nimport {\n  Scatter,\n  ScatterChart,\n  XAxis,\n  YAxis,\n  useChartHeight,\n  useChartWidth,\n  type ScatterShapeProps,\n  type XAxisTickContentProps,\n  type YAxisTickContentProps,\n} from \"recharts\";\nimport {\n  ChartContainer,\n  LoadingIndicator,\n  getColorsCount,\n  type ChartConfig,\n} from \"@/registry/ui/recharts-chart\";\nimport {\n  ChartTooltip,\n  type TooltipRoundness,\n  type TooltipVariant,\n} from \"@/registry/ui/recharts-tooltip\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type { ChartConfig, TooltipRoundness, TooltipVariant };\n\nconst DAY_MS = 86_400_000;\nconst DAYS_PER_WEEK = 7;\nconst TRAILING_YEAR_DAYS = 365; // default range — the trailing year, GitHub style\n\nconst INTRO_COLUMN_STAGGER = 0.014; // seconds between one week column and the next\nconst INTRO_CELL_GROW = 0.32; // seconds for a single cell to scale up + fade in\nconst INTRO_CELL_SCALE_FROM = 0.4; // a cell opens from this fraction of its size\nconst LOADING_ANIMATION_DURATION = 2000; // shimmer loop, in milliseconds\n\nconst DEFAULT_CELL_RADIUS = 2;\nconst DEFAULT_CELL_GAP = 3;\nconst MIN_CELL_SIZE = 4; // never fit cells smaller than this\nconst FALLBACK_CELL_SIZE = 12; // used before the container has a measured size\n\nconst DAY_LABEL_WIDTH = 32;\nconst MONTH_LABEL_HEIGHT = 22;\nconst EDGE_PADDING = 2; // keeps the outermost cells' hover ring inside the plot\nconst LABEL_MARGIN = 8; // gap between the month labels' baseline and the grid\nconst MIN_MONTH_LABEL_COLUMNS = 3; // a month label is dropped when the next one is closer than this\n// Module-level so the margin keeps its identity across renders: a new object\n// makes Recharts recompute the layout and remount every cell.\nconst CHART_MARGIN = {\n  top: EDGE_PADDING,\n  right: EDGE_PADDING,\n  bottom: EDGE_PADDING,\n  left: EDGE_PADDING,\n};\n\nconst CELL_DIM_OPACITY = 0.55; // cells outside the current selection — soft, so the ramp still reads\nconst HOVER_RING_WIDTH = 1; // outline drawn around the hovered cell, in pixels\nconst SELECTED_RING_WIDTH = 1.5; // outline drawn around the selected cell, in pixels\n\nconst LOADING_CELL_FLOOR = 0.07; // cell fill outside the sweep, × foreground alpha\nconst LOADING_CELL_PEAK = 0.22; // cell fill inside the sweep, × foreground alpha\nconst LOADING_SHIMMER_BAND = 0.22; // sweep half-width, fraction of chart width\n\nconst DEFAULT_DAY_LABELS = [\"\", \"Mon\", \"\", \"Wed\", \"\", \"Fri\", \"\"]; // Sunday first, like GitHub\nconst DEFAULT_MONTH_LABELS = [\n  \"Jan\",\n  \"Feb\",\n  \"Mar\",\n  \"Apr\",\n  \"May\",\n  \"Jun\",\n  \"Jul\",\n  \"Aug\",\n  \"Sep\",\n  \"Oct\",\n  \"Nov\",\n  \"Dec\",\n];\n\nconst roundnessMap: Record<TooltipRoundness, string> = {\n  sm: \"rounded-sm\",\n  md: \"rounded-md\",\n  lg: \"rounded-lg\",\n  xl: \"rounded-xl\",\n};\n\nconst variantMap: Record<TooltipVariant, string> = {\n  default: \"bg-background\",\n  \"frosted-glass\": \"bg-background/70 backdrop-blur-sm\",\n};\n\nexport type HeatmapDatum = {\n  date: string;\n  value: number;\n};\n\nexport type HeatmapRange = number | string | [string, string];\n\nexport type WeekStart = \"sunday\" | \"monday\";\nexport type HeatmapScale = \"quantile\" | \"linear\";\nexport type HeatmapAnimationType = \"none\" | \"default\";\nexport type TooltipPosition = \"fixed\" | \"variable\";\n\nexport type HeatmapSelection = { date: string; value: number };\n\ntype ScatterChartProps = ComponentProps<typeof ScatterChart>;\n\nexport interface EvilHeatmapChartProps {\n  data: HeatmapDatum[]; // one entry per day that has a value\n  config: ChartConfig; // the series key's label + color ramp (one color per intensity level)\n  children: ReactNode; // composed parts — <Cell>, <MonthLabel>, <DayLabel>, <Tooltip>, <Legend>\n  className?: string; // extra classes for the chart container\n  chartProps?: Omit<ScatterChartProps, \"data\" | \"children\" | \"margin\">; // escape hatch for the raw Recharts ScatterChart\n  range?: HeatmapRange; // calendar window; defaults to the trailing year\n  weekStart?: WeekStart; // which day starts each column\n  cellSize?: number; // fixed cell size in pixels; omit to fit the container width\n  scale?: HeatmapScale; // how values map onto the color ramp when no thresholds are given\n  thresholds?: number[]; // explicit ascending level boundaries; values below the first are level 1\n  defaultSelectedDate?: string | null; // cell selected on first render\n  onSelectionChange?: (selection: HeatmapSelection | null) => void; // fires when the selected cell changes\n  isLoading?: boolean; // shows the animated loading skeleton\n  animation?: boolean; // master switch for the intro sweep — false renders instantly\n  animationType?: HeatmapAnimationType; // \"none\" disables the intro sweep\n}\n\nexport interface CellProps {\n  dataKey: string; // config key whose colors form the intensity ramp\n  radius?: number; // corner radius of each cell in pixels\n  gap?: number; // space between cells in pixels\n  isClickable?: boolean; // lets cells be selected by clicking them\n}\n\n/**\n * Configures the heatmap cells. A configuration slot — the root reads its props\n * and wires them into the Recharts scatter that paints the grid, so it renders\n * nothing itself. `dataKey` picks the config entry whose color array becomes the\n * intensity ramp: the first color is the lightest level, the last the strongest.\n */\nconst Cell: FC<CellProps> = () => null;\n\nexport interface MonthLabelProps {\n  labels?: string[]; // twelve month names, January first\n}\n\n/** Presence shows month names above the grid. A configuration slot — renders nothing itself. */\nconst MonthLabel: FC<MonthLabelProps> = () => null;\n\nexport interface DayLabelProps {\n  labels?: string[]; // seven day names, Sunday first; an empty string hides that row's label\n}\n\n/** Presence shows weekday names beside the grid. A configuration slot — renders nothing itself. */\nconst DayLabel: FC<DayLabelProps> = () => null;\n\nexport interface LegendProps {\n  align?: \"left\" | \"right\"; // which side of the chart the ramp sits on\n  lessLabel?: string; // text before the ramp\n  moreLabel?: string; // text after the ramp\n}\n\n/** Presence renders the \"Less → More\" ramp under the grid. A configuration slot — renders nothing itself. */\nconst Legend: FC<LegendProps> = () => null;\n\ntype CellSlot = {\n  dataKey: string;\n  radius: number;\n  gap: number;\n  isClickable: boolean;\n};\ntype MonthLabelSlot = { labels: string[] };\ntype DayLabelSlot = { labels: string[] };\ntype LegendSlot = {\n  align: \"left\" | \"right\";\n  lessLabel: string;\n  moreLabel: string;\n};\n\ntype CollectedSlots = {\n  cell: CellSlot;\n  monthLabel: MonthLabelSlot | null;\n  dayLabel: DayLabelSlot | null;\n  legend: LegendSlot | null;\n};\n\nfunction collectSlots(children: ReactNode): CollectedSlots {\n  let cell: CellSlot = {\n    dataKey: \"\",\n    radius: DEFAULT_CELL_RADIUS,\n    gap: DEFAULT_CELL_GAP,\n    isClickable: false,\n  };\n  let monthLabel: MonthLabelSlot | null = null;\n  let dayLabel: DayLabelSlot | null = null;\n  let legend: LegendSlot | null = null;\n\n  Children.forEach(children, (child) => {\n    if (!isValidElement(child)) return;\n    const type = child.type;\n\n    if (type === Cell) {\n      const props = child.props as CellProps;\n      cell = {\n        dataKey: props.dataKey,\n        radius: props.radius ?? DEFAULT_CELL_RADIUS,\n        gap: props.gap ?? DEFAULT_CELL_GAP,\n        isClickable: props.isClickable ?? false,\n      };\n    } else if (type === MonthLabel) {\n      const props = child.props as MonthLabelProps;\n      monthLabel = { labels: props.labels ?? DEFAULT_MONTH_LABELS };\n    } else if (type === DayLabel) {\n      const props = child.props as DayLabelProps;\n      dayLabel = { labels: props.labels ?? DEFAULT_DAY_LABELS };\n    } else if (type === Legend) {\n      const props = child.props as LegendProps;\n      legend = {\n        align: props.align ?? \"right\",\n        lessLabel: props.lessLabel ?? \"Less\",\n        moreLabel: props.moreLabel ?? \"More\",\n      };\n    }\n  });\n\n  return { cell, monthLabel, dayLabel, legend };\n}\n\nfunction defaultDateFormatter(date: string): string {\n  const ms = parseIsoDate(date);\n  if (Number.isNaN(ms)) return date;\n  return new Date(ms).toLocaleDateString(\"en-US\", {\n    month: \"short\",\n    day: \"numeric\",\n    year: \"numeric\",\n    timeZone: \"UTC\",\n  });\n}\n\nfunction defaultValueFormatter(value: number): string {\n  return value.toLocaleString();\n}\n\nfunction parseIsoDate(iso: string): number {\n  const [y, m, d] = iso.split(\"-\").map(Number);\n  if (!y || !m || !d) return Number.NaN;\n  return Date.UTC(y, m - 1, d);\n}\n\nfunction toIsoDate(ms: number): string {\n  return new Date(ms).toISOString().slice(0, 10);\n}\n\nfunction todayUtc(): number {\n  const now = new Date();\n  return Date.UTC(now.getFullYear(), now.getMonth(), now.getDate());\n}\n\ntype ResolvedRange = { start: number; end: number }; // inclusive UTC midnights\n\nfunction resolveRange(range: HeatmapRange | undefined, data: HeatmapDatum[]): ResolvedRange {\n  if (typeof range === \"number\") {\n    return { start: Date.UTC(range, 0, 1), end: Date.UTC(range, 11, 31) };\n  }\n  if (typeof range === \"string\") {\n    const [y, m] = range.split(\"-\").map(Number);\n    if (y && m) return { start: Date.UTC(y, m - 1, 1), end: Date.UTC(y, m, 0) };\n    if (y) return { start: Date.UTC(y, 0, 1), end: Date.UTC(y, 11, 31) };\n  }\n  if (Array.isArray(range)) {\n    const start = parseIsoDate(range[0]);\n    const end = parseIsoDate(range[1]);\n    if (!Number.isNaN(start) && !Number.isNaN(end) && start <= end) return { start, end };\n  }\n\n  let latest = Number.NEGATIVE_INFINITY;\n  for (const datum of data) {\n    const ms = parseIsoDate(datum.date);\n    if (!Number.isNaN(ms) && ms > latest) latest = ms;\n  }\n  const end = Number.isFinite(latest) ? latest : todayUtc();\n  return { start: end - (TRAILING_YEAR_DAYS - 1) * DAY_MS, end };\n}\n\ntype GridCell = {\n  date: string; // ISO calendar date\n  value: number; // 0 for days absent from the data\n  level: number; // 0 = empty, 1..levels = ramp intensity\n  column: number; // week column, 0-based from the range start\n  row: number; // weekday row, 0-based from `weekStart`\n};\n\ntype MonthTick = { column: number; month: number }; // month label anchored at this week column\n\ntype GridModel = {\n  cells: GridCell[];\n  weeks: number; // number of week columns\n  levels: number; // number of non-empty intensity levels\n  monthTicks: MonthTick[]; // month starts inside the range, thinned so labels never overlap\n  range: ResolvedRange;\n};\n\nfunction computeThresholds(values: number[], levels: number, scale: HeatmapScale): number[] {\n  const cuts = levels - 1;\n  if (cuts <= 0) return [];\n  const nonZero = values.filter((value) => value > 0).sort((a, b) => a - b);\n  if (nonZero.length === 0) return Array.from({ length: cuts }, (_, i) => i + 1);\n\n  if (scale === \"linear\") {\n    const min = nonZero[0];\n    const max = nonZero[nonZero.length - 1];\n    if (max === min) return Array.from({ length: cuts }, () => max + 1);\n    return Array.from({ length: cuts }, (_, i) => min + ((max - min) * (i + 1)) / levels);\n  }\n\n  return Array.from({ length: cuts }, (_, i) => {\n    const position = ((i + 1) / levels) * (nonZero.length - 1);\n    const lower = Math.floor(position);\n    const upper = Math.min(nonZero.length - 1, lower + 1);\n    const fraction = position - lower;\n    return nonZero[lower] + (nonZero[upper] - nonZero[lower]) * fraction;\n  });\n}\n\nfunction levelOf(value: number, thresholds: number[]): number {\n  if (value <= 0) return 0;\n  let level = 1;\n  for (const threshold of thresholds) {\n    if (value >= threshold) level += 1;\n  }\n  return level;\n}\n\nfunction buildGrid(\n  data: HeatmapDatum[],\n  range: ResolvedRange,\n  weekStart: WeekStart,\n  levels: number,\n  scale: HeatmapScale,\n  thresholds: number[] | undefined,\n): GridModel {\n  const firstDay = weekStart === \"monday\" ? 1 : 0;\n  const valueByDate = new Map<string, number>();\n  for (const datum of data) {\n    const ms = parseIsoDate(datum.date);\n    if (Number.isNaN(ms) || ms < range.start || ms > range.end) continue;\n    valueByDate.set(toIsoDate(ms), (valueByDate.get(toIsoDate(ms)) ?? 0) + datum.value);\n  }\n\n  const cuts =\n    thresholds && thresholds.length > 0\n      ? [...thresholds].sort((a, b) => a - b)\n      : computeThresholds([...valueByDate.values()], levels, scale);\n  const effectiveLevels = cuts.length + 1;\n\n  const startOffset =\n    (new Date(range.start).getUTCDay() - firstDay + DAYS_PER_WEEK) % DAYS_PER_WEEK;\n  const days = Math.round((range.end - range.start) / DAY_MS) + 1;\n  const weeks = Math.floor((days + startOffset + DAYS_PER_WEEK - 1) / DAYS_PER_WEEK);\n\n  const cells: GridCell[] = [];\n  const monthStarts: MonthTick[] = [];\n  for (let i = 0; i < days; i++) {\n    const ms = range.start + i * DAY_MS;\n    const day = new Date(ms);\n    const date = toIsoDate(ms);\n    const value = valueByDate.get(date) ?? 0;\n    const column = Math.floor((i + startOffset) / DAYS_PER_WEEK);\n    cells.push({\n      date,\n      value,\n      level: Math.min(effectiveLevels, levelOf(value, cuts)),\n      column,\n      row: (i + startOffset) % DAYS_PER_WEEK,\n    });\n    // The range's first day names its month too, so a window opening mid-month\n    // still starts with a label — GitHub does the same.\n    if (i === 0 || day.getUTCDate() === 1) {\n      monthStarts.push({ column, month: day.getUTCMonth() });\n    }\n  }\n\n  // Two labels closer than a few columns would overlap — keep the later one.\n  const monthTicks = monthStarts.filter(\n    (tick, index) =>\n      index === monthStarts.length - 1 ||\n      monthStarts[index + 1].column - tick.column >= MIN_MONTH_LABEL_COLUMNS,\n  );\n\n  return { cells, weeks, levels: effectiveLevels, monthTicks, range };\n}\n\n/** Index into the config's color slots for a non-empty level. */\nfunction rampIndex(level: number, levels: number, colorsCount: number): number {\n  if (levels <= 1 || colorsCount <= 1) return colorsCount - 1;\n  const index = Math.round(((level - 1) / (levels - 1)) * (colorsCount - 1));\n  return Math.max(0, Math.min(colorsCount - 1, index));\n}\n\ntype Layout = {\n  cellSize: number; // pixels per week column and per weekday row\n  gridTop: number; // pixel y of the first row's top edge, from the top of the chart\n  xDomain: [number, number]; // week columns visible across the plot, in cell units\n  yDomain: [number, number]; // weekday rows visible down the plot, in cell units\n};\n\n/**\n * Sizes square cells to the plot and expresses the result as axis domains: one\n * domain unit is exactly one cell, so Recharts' linear scales place every cell\n * without any further math. Columns fill from the left; the seven rows sit\n * centered vertically in whatever height is left.\n */\nfunction computeLayout(\n  chartWidth: number,\n  chartHeight: number,\n  weeks: number,\n  fixedCellSize: number | undefined,\n  hasDayLabels: boolean,\n  hasMonthLabels: boolean,\n): Layout {\n  const plotWidth = Math.max(\n    0,\n    chartWidth - 2 * EDGE_PADDING - (hasDayLabels ? DAY_LABEL_WIDTH : 0),\n  );\n  const plotHeight = Math.max(\n    0,\n    chartHeight - 2 * EDGE_PADDING - (hasMonthLabels ? MONTH_LABEL_HEIGHT : 0),\n  );\n\n  let cellSize = fixedCellSize ?? FALLBACK_CELL_SIZE;\n  if (fixedCellSize === undefined && plotWidth > 0 && weeks > 0) {\n    const fitWidth = Math.floor(plotWidth / weeks);\n    const fitHeight =\n      plotHeight > 0 ? Math.floor(plotHeight / DAYS_PER_WEEK) : Number.POSITIVE_INFINITY;\n    cellSize = Math.max(MIN_CELL_SIZE, Math.min(fitWidth, fitHeight));\n  }\n\n  const columnsAcross = plotWidth > 0 ? plotWidth / cellSize : weeks;\n  const rowsDown = plotHeight > 0 ? plotHeight / cellSize : DAYS_PER_WEEK;\n  const spare = Math.max(0, plotHeight - cellSize * DAYS_PER_WEEK);\n  const rowOffset = Math.floor(spare / 2) / cellSize; // whole pixels keep the cells crisp\n\n  return {\n    cellSize,\n    gridTop: EDGE_PADDING + (hasMonthLabels ? MONTH_LABEL_HEIGHT : 0) + rowOffset * cellSize,\n    xDomain: [-0.5, columnsAcross - 0.5],\n    yDomain: [-0.5 - rowOffset, rowsDown - 0.5 - rowOffset],\n  };\n}\n\ntype CellStatus = \"idle\" | \"selected\" | \"dimmed\"; // a cell's look relative to the selection\n\n/**\n * Selection and intro state, kept outside React state on purpose. Recharts\n * remounts every scatter symbol whenever the <Scatter> element receives new\n * props, so a selection held in React state would rebuild all ~365 cells (and\n * replay their intro) on each click. Instead each cell subscribes to its own\n * status and only the cells whose status flips re-render.\n */\ntype HeatmapStore = {\n  getSelected: () => string | null; // currently selected ISO date, or null when none\n  subscribe: (listener: () => void) => () => void; // useSyncExternalStore contract\n  toggle: (date: string, value: number) => void; // selects a day, or clears it when already selected\n  setOnSelectionChange: (callback: EvilHeatmapChartProps[\"onSelectionChange\"]) => void; // synced by the root\n  isRevealPending: () => boolean; // true until the first real cells mount — later remounts skip the intro\n  armReveal: () => void; // the next cells to mount play the intro again (after a loading state)\n  settleReveal: () => void; // called once cells have mounted\n  getTabStop: () => string | null; // day that last took keyboard focus, or null for the default stop\n  setTabStop: (date: string | null) => void; // moves the grid's single Tab stop (roving tabindex)\n};\n\nfunction createHeatmapStore(initialSelected: string | null): HeatmapStore {\n  let selected = initialSelected;\n  let tabStop: string | null = null;\n  let onSelectionChange: EvilHeatmapChartProps[\"onSelectionChange\"];\n  let revealPending = true;\n  const listeners = new Set<() => void>();\n  const notify = () => {\n    for (const listener of listeners) listener();\n  };\n\n  return {\n    getSelected: () => selected,\n    subscribe: (listener) => {\n      listeners.add(listener);\n      return () => {\n        listeners.delete(listener);\n      };\n    },\n    toggle: (date, value) => {\n      selected = selected === date ? null : date;\n      notify();\n      onSelectionChange?.(selected === null ? null : { date: selected, value });\n    },\n    getTabStop: () => tabStop,\n    setTabStop: (date) => {\n      if (tabStop === date) return;\n      tabStop = date;\n      notify();\n    },\n    setOnSelectionChange: (callback) => {\n      onSelectionChange = callback;\n    },\n    isRevealPending: () => revealPending,\n    armReveal: () => {\n      revealPending = true;\n    },\n    settleReveal: () => {\n      revealPending = false;\n    },\n  };\n}\n\n/** The cell's look relative to the selection; re-renders only when that look changes. */\nfunction useCellStatus(store: HeatmapStore, date: string): CellStatus {\n  const getStatus = (): CellStatus => {\n    const selected = store.getSelected();\n    if (selected === null) return \"idle\";\n    return selected === date ? \"selected\" : \"dimmed\";\n  };\n  return useSyncExternalStore(store.subscribe, getStatus, getStatus);\n}\n\n/**\n * Whether the cell is the grid's single Tab stop: the day that last had focus,\n * else the selected day, else the most recent day in the range. A stored day\n * with no cell (outside the range, or gone after a data change) is skipped, so\n * some cell always keeps the stop and the grid stays reachable by keyboard.\n */\nfunction useIsTabStop(\n  store: HeatmapStore,\n  date: string,\n  isLast: boolean,\n  hasDate: (date: string) => boolean,\n): boolean {\n  const getIsTabStop = () => {\n    const tabStop = store.getTabStop();\n    const selected = store.getSelected();\n    const stop =\n      tabStop !== null && hasDate(tabStop)\n        ? tabStop\n        : selected !== null && hasDate(selected)\n          ? selected\n          : null;\n    return stop === null ? isLast : stop === date;\n  };\n  return useSyncExternalStore(store.subscribe, getIsTabStop, getIsTabStop);\n}\n\n/** Day index a key press moves to: rows are weekdays, columns are weeks. */\nfunction keyTarget(key: string, index: number, count: number): number | null {\n  switch (key) {\n    case \"ArrowUp\":\n      return index - 1;\n    case \"ArrowDown\":\n      return index + 1;\n    case \"ArrowLeft\":\n      return index - DAYS_PER_WEEK;\n    case \"ArrowRight\":\n      return index + DAYS_PER_WEEK;\n    case \"Home\":\n      return 0;\n    case \"End\":\n      return count - 1;\n    default:\n      return null;\n  }\n}\n\n/**\n * Shared state for every part of the chart. Lifted into <EvilHeatmapChart /> so\n * the cells, axes, tooltip, and legend can read it without prop drilling.\n */\ntype HeatmapChartContextValue = {\n  config: ChartConfig; // colors + label for the series named by <Cell dataKey>\n  cell: CellSlot; // resolved <Cell /> props\n  grid: GridModel; // the date window laid out as week columns\n  colorsCount: number; // number of color slots the config provides for the ramp\n  fixedCellSize: number | undefined; // the root's `cellSize` prop\n  hasDayLabels: boolean; // whether <DayLabel /> is composed\n  hasMonthLabels: boolean; // whether <MonthLabel /> is composed\n  weekStart: WeekStart; // which weekday sits on the top row\n  isLoading: boolean; // whether the chart shows its loading skeleton\n  revealEnabled: boolean; // whether cells play the intro sweep when they mount\n  store: HeatmapStore; // selection + intro state, stable for the chart's lifetime\n};\n\nconst HeatmapChartContext = createContext<HeatmapChartContextValue | null>(null);\n\nfunction useHeatmapChart() {\n  const context = use(HeatmapChartContext);\n\n  if (!context) {\n    throw new Error(\n      \"Heatmap chart parts (<Cell />, <Tooltip />, <Legend />, …) must be used within <EvilHeatmapChart />\",\n    );\n  }\n\n  return context;\n}\n\n/** The current cell layout, derived from the live chart size. Only valid inside the Recharts tree. */\nfunction useHeatmapLayout(): Layout {\n  const width = useChartWidth() ?? 0;\n  const height = useChartHeight() ?? 0;\n  const { grid, fixedCellSize, hasDayLabels, hasMonthLabels } = useHeatmapChart();\n\n  return useMemo(\n    () => computeLayout(width, height, grid.weeks, fixedCellSize, hasDayLabels, hasMonthLabels),\n    [width, height, grid.weeks, fixedCellSize, hasDayLabels, hasMonthLabels],\n  );\n}\n\n/**\n * Calendar heatmap rendered with Recharts — the GitHub contribution graph shape:\n * one square per day, weeks as columns, color intensity from the config's color\n * ramp. The root owns the date window, the value → level mapping, selection\n * state, and the loading skeleton. The visual parts — `<Cell>`, `<MonthLabel>`,\n * `<DayLabel>`, `<Tooltip>`, `<Legend>` — are composed as children. Under the\n * hood the grid is a Recharts scatter on two numeric axes measured in cells, so\n * the cells stay square at any container size.\n */\nexport function EvilHeatmapChart({\n  data,\n  config,\n  children,\n  className,\n  chartProps,\n  range,\n  weekStart = \"sunday\",\n  cellSize,\n  scale = \"quantile\",\n  thresholds,\n  defaultSelectedDate = null,\n  onSelectionChange,\n  isLoading = false,\n  animation = true,\n  animationType = \"default\",\n}: EvilHeatmapChartProps) {\n  const chartId = useId().replace(/:/g, \"\"); // colon-free id keeps CSS/SVG selectors valid\n  const shouldReduceMotion = useReducedMotion();\n  const [store] = useState(() => createHeatmapStore(defaultSelectedDate));\n\n  const slots = useMemo(() => collectSlots(children), [children]);\n  const { legend } = slots;\n  const monthLabels = slots.monthLabel?.labels;\n  const dayLabels = slots.dayLabel?.labels;\n  // `children` is a new element tree on every parent render, so the <Cell />\n  // slot is re-keyed on its values — a fresh object would ripple down into the\n  // Recharts tree and remount every cell.\n  const { dataKey, radius, gap, isClickable } = slots.cell;\n  const cell = useMemo(\n    () => ({ dataKey, radius, gap, isClickable }),\n    [dataKey, radius, gap, isClickable],\n  );\n\n  const colorsCount = config[cell.dataKey] ? getColorsCount(config[cell.dataKey]) : 1;\n\n  const grid = useMemo(\n    () => buildGrid(data, resolveRange(range, data), weekStart, colorsCount, scale, thresholds),\n    [data, range, weekStart, colorsCount, scale, thresholds],\n  );\n\n  useEffect(() => {\n    store.setOnSelectionChange(onSelectionChange);\n  }, [store, onSelectionChange]);\n\n  // The intro is a mount-time animation on each cell. It plays when the first\n  // real cells mount and again after each loading state; resizes and data\n  // updates remount the Recharts symbols but skip it (see HeatmapStore).\n  useEffect(() => {\n    if (isLoading) store.armReveal();\n  }, [store, isLoading]);\n\n  const revealEnabled = animation && animationType !== \"none\" && !shouldReduceMotion;\n\n  const contextValue = useMemo<HeatmapChartContextValue>(\n    () => ({\n      config,\n      cell,\n      grid,\n      colorsCount,\n      fixedCellSize: cellSize,\n      hasDayLabels: dayLabels !== undefined,\n      hasMonthLabels: monthLabels !== undefined,\n      weekStart,\n      isLoading,\n      revealEnabled,\n      store,\n    }),\n    [\n      config,\n      cell,\n      grid,\n      colorsCount,\n      cellSize,\n      dayLabels,\n      monthLabels,\n      weekStart,\n      isLoading,\n      revealEnabled,\n      store,\n    ],\n  );\n\n  const shimmerId = `${chartId}-heatmap-shimmer`;\n\n  return (\n    <HeatmapChartContext value={contextValue}>\n      <ChartContainer\n        className={className}\n        config={config}\n        footer={legend && !isLoading && <HeatmapLegend legend={legend} />}\n      >\n        <LoadingIndicator isLoading={isLoading} />\n        <ScatterChart id={chartId} margin={CHART_MARGIN} {...chartProps}>\n          <HeatmapPlot\n            monthLabels={monthLabels}\n            dayLabels={dayLabels}\n            shimmerId={shimmerId}\n            animateShimmer={!shouldReduceMotion}\n          />\n          {children}\n        </ScatterChart>\n      </ChartContainer>\n    </HeatmapChartContext>\n  );\n}\n\ntype HeatmapPlotProps = {\n  monthLabels: string[] | undefined; // twelve month names, or undefined to hide the axis\n  dayLabels: string[] | undefined; // seven day names, Sunday first, or undefined to hide the axis\n  shimmerId: string; // gradient id shared by the skeleton cells\n  animateShimmer: boolean; // false parks the loading sweep (reduced motion)\n};\n\n/**\n * Everything drawn inside the Recharts tree: the axes, the cells, and the\n * loading gradient. The layout is measured once here and handed down. The\n * <Scatter> element is memoized on what actually moves the cells — Recharts\n * remounts every symbol when it gets new props — and the cells read their\n * selection status from the store, so a click never rebuilds the grid.\n */\nconst HeatmapPlot = memo(function HeatmapPlot({\n  monthLabels,\n  dayLabels,\n  shimmerId,\n  animateShimmer,\n}: HeatmapPlotProps) {\n  const { config, cell, grid, colorsCount, isLoading, revealEnabled, store } = useHeatmapChart();\n  const layout = useHeatmapLayout();\n\n  const gap = Math.min(cell.gap, layout.cellSize - 1);\n  const size = Math.max(1, layout.cellSize - gap);\n  const radius = Math.min(cell.radius, size / 2);\n  const { dataKey, isClickable } = cell;\n  const { cells, levels, range } = grid;\n  const configLabel = config[dataKey]?.label;\n  const seriesLabel = typeof configLabel === \"string\" ? configLabel : dataKey;\n\n  // Cells are consecutive days, so a day's index is its offset from the range start.\n  const navigate = useCallback(\n    (date: string, key: string) => {\n      const index = Math.round((parseIsoDate(date) - range.start) / DAY_MS);\n      const target = keyTarget(key, index, cells.length);\n      if (target === null) return undefined;\n      return cells[Math.max(0, Math.min(cells.length - 1, target))].date;\n    },\n    [cells, range.start],\n  );\n  const lastDate = cells.at(-1)?.date;\n  const dates = useMemo(() => new Set(cells.map((day) => day.date)), [cells]);\n  const hasDate = useCallback((date: string) => dates.has(date), [dates]);\n\n  const renderCell = useCallback(\n    ({ cx, cy, payload }: ScatterShapeProps) => {\n      const day = payload as GridCell | undefined;\n      if (isLoading) {\n        return <LoadingCell cx={cx} cy={cy} size={size} radius={radius} gradientId={shimmerId} />;\n      }\n      if (!day) return <g />;\n      return (\n        <HeatmapCell\n          cx={cx}\n          cy={cy}\n          size={size}\n          radius={radius}\n          date={day.date}\n          value={day.value}\n          column={day.column}\n          fill={\n            day.level === 0\n              ? undefined\n              : `var(--color-${dataKey}-${rampIndex(day.level, levels, colorsCount)})`\n          }\n          isClickable={isClickable}\n          isLast={day.date === lastDate}\n          hasDate={hasDate}\n          seriesLabel={seriesLabel}\n          navigate={navigate}\n          reveal={revealEnabled}\n          store={store}\n        />\n      );\n    },\n    [\n      isLoading,\n      size,\n      radius,\n      shimmerId,\n      dataKey,\n      levels,\n      colorsCount,\n      isClickable,\n      lastDate,\n      hasDate,\n      seriesLabel,\n      navigate,\n      revealEnabled,\n      store,\n    ],\n  );\n\n  const scatter = useMemo(\n    () => <Scatter data={grid.cells} isAnimationActive={false} shape={renderCell} />,\n    [grid.cells, renderCell],\n  );\n\n  return (\n    <>\n      <HeatmapAxes layout={layout} monthLabels={monthLabels} dayLabels={dayLabels} />\n      {isLoading && <ShimmerGradient id={shimmerId} animated={animateShimmer} />}\n      {scatter}\n    </>\n  );\n});\n\ntype HeatmapAxesProps = {\n  layout: Layout; // the measured cell layout\n  monthLabels: string[] | undefined; // twelve month names, or undefined to hide the axis\n  dayLabels: string[] | undefined; // seven day names, Sunday first, or undefined to hide the axis\n};\n\n/**\n * The two numeric axes that position the grid. Their domains are measured in\n * cells, so one unit along either axis is exactly one cell; the axes double as\n * the month labels (top) and weekday labels (left).\n */\nfunction HeatmapAxes({ layout, monthLabels, dayLabels }: HeatmapAxesProps) {\n  const { grid, weekStart } = useHeatmapChart();\n  const firstDay = weekStart === \"monday\" ? 1 : 0;\n\n  const monthByColumn = useMemo(\n    () => new Map(grid.monthTicks.map((tick) => [tick.column, tick.month])),\n    [grid.monthTicks],\n  );\n  const monthTicks = useMemo(() => grid.monthTicks.map((tick) => tick.column), [grid.monthTicks]);\n  const dayTicks = useMemo(() => Array.from({ length: DAYS_PER_WEEK }, (_, row) => row), []);\n\n  return (\n    <>\n      <XAxis\n        type=\"number\"\n        dataKey=\"column\"\n        orientation=\"top\"\n        domain={layout.xDomain}\n        allowDataOverflow\n        allowDecimals\n        ticks={monthTicks}\n        interval={0}\n        hide={monthLabels === undefined}\n        height={MONTH_LABEL_HEIGHT}\n        axisLine={false}\n        tickLine={false}\n        tickMargin={0}\n        tick={(props: XAxisTickContentProps) => {\n          const month = monthByColumn.get(Number(props.payload.value));\n          if (month === undefined) return null;\n          // The rows sit centered in the plot, so the labels follow the grid's\n          // top edge instead of the axis line.\n          return (\n            <text\n              x={Number(props.x) - layout.cellSize / 2}\n              y={layout.gridTop - LABEL_MARGIN}\n              textAnchor=\"start\"\n              className=\"fill-muted-foreground text-[11px]\"\n            >\n              {monthLabels?.[month] ?? DEFAULT_MONTH_LABELS[month]}\n            </text>\n          );\n        }}\n      />\n      <YAxis\n        type=\"number\"\n        dataKey=\"row\"\n        reversed\n        domain={layout.yDomain}\n        allowDataOverflow\n        allowDecimals\n        ticks={dayTicks}\n        interval={0}\n        hide={dayLabels === undefined}\n        width={DAY_LABEL_WIDTH}\n        axisLine={false}\n        tickLine={false}\n        tickMargin={0}\n        tick={(props: YAxisTickContentProps) => {\n          const row = Number(props.payload.value);\n          const label = dayLabels?.[(row + firstDay) % DAYS_PER_WEEK];\n          if (!label) return null;\n          return (\n            <text\n              x={Number(props.x)}\n              y={Number(props.y)}\n              textAnchor=\"end\"\n              dominantBaseline=\"central\"\n              className=\"fill-muted-foreground text-[10px]\"\n            >\n              {label}\n            </text>\n          );\n        }}\n      />\n    </>\n  );\n}\n\ntype CellGeometry = {\n  cx: number | undefined; // pixel center of the cell along the columns axis\n  cy: number | undefined; // pixel center of the cell along the rows axis\n  size: number; // side of the painted square, gap already removed\n  radius: number; // corner radius, clamped to the square\n};\n\ntype HeatmapCellProps = CellGeometry & {\n  date: string; // the day this cell stands for\n  value: number; // that day's value\n  column: number; // week column, drives the intro stagger\n  fill: string | undefined; // ramp color, or undefined for an empty day\n  isClickable: boolean; // lets the cell be selected by clicking it or from the keyboard\n  isLast: boolean; // the most recent day — the default Tab stop when nothing is selected\n  hasDate: (date: string) => boolean; // whether a day has a cell in the current grid\n  seriesLabel: string; // series name read out with the value, e.g. \"Contributions\"\n  navigate: (date: string, key: string) => string | undefined; // day a key press moves focus to\n  reveal: boolean; // whether intro animations are enabled at all\n  store: HeatmapStore; // selection + intro state\n};\n\n/**\n * Renders a single day. Memoized on primitive props, because Recharts re-runs\n * the shape function on every hover; the selection comes from the store, so\n * only the cells whose status flips re-render on a click.\n *\n * Clickable cells are toggle buttons behind a single Tab stop: arrow keys move\n * by day (up/down) and by week (left/right), Home/End jump to the range ends,\n * and Enter or Space toggles the focused day.\n */\nconst HeatmapCell = memo(function HeatmapCell({\n  cx,\n  cy,\n  size,\n  radius,\n  date,\n  value,\n  column,\n  fill,\n  isClickable,\n  isLast,\n  hasDate,\n  seriesLabel,\n  navigate,\n  reveal,\n  store,\n}: HeatmapCellProps) {\n  const status = useCellStatus(store, date);\n  const isTabStop = useIsTabStop(store, date, isLast, hasDate);\n  // Read once at mount: the first real cells play the intro, later remounts don't.\n  const [playIntro] = useState(() => reveal && store.isRevealPending());\n  useEffect(() => {\n    store.settleReveal();\n  }, [store]);\n\n  if (cx === undefined || cy === undefined) return null;\n\n  const selected = status === \"selected\";\n\n  const handleKeyDown = (event: KeyboardEvent<SVGRectElement>) => {\n    if (event.key === \"Enter\" || event.key === \" \") {\n      event.preventDefault();\n      store.toggle(date, value);\n      return;\n    }\n    const target = navigate(date, event.key);\n    if (target === undefined) return;\n    // Keep the key from scrolling the page or reaching Recharts' own keyboard layer.\n    event.preventDefault();\n    event.stopPropagation();\n    event.currentTarget.ownerSVGElement\n      ?.querySelector<SVGRectElement>(`[data-date=\"${target}\"]`)\n      ?.focus();\n  };\n\n  return (\n    <motion.rect\n      x={cx - size / 2}\n      y={cy - size / 2}\n      width={size}\n      height={size}\n      rx={radius}\n      ry={radius}\n      className={cn(\n        \"fill-foreground/[0.08] transition-[fill-opacity,stroke] duration-200\",\n        selected ? \"stroke-foreground/90\" : \"hover:stroke-foreground/60 stroke-transparent\",\n        isClickable && \"focus-visible:stroke-foreground cursor-pointer outline-none\",\n      )}\n      style={{ fill, originX: 0.5, originY: 0.5 }}\n      fillOpacity={status === \"dimmed\" ? CELL_DIM_OPACITY : 1}\n      strokeWidth={selected ? SELECTED_RING_WIDTH : HOVER_RING_WIDTH}\n      initial={playIntro ? { opacity: 0, scale: INTRO_CELL_SCALE_FROM } : false}\n      animate={{ opacity: 1, scale: 1 }}\n      transition={{\n        delay: column * INTRO_COLUMN_STAGGER,\n        duration: INTRO_CELL_GROW,\n        ease: \"easeOut\",\n      }}\n      onClick={isClickable ? () => store.toggle(date, value) : undefined}\n      {...(isClickable && {\n        \"data-date\": date,\n        role: \"button\",\n        \"aria-pressed\": selected,\n        \"aria-label\": `${defaultDateFormatter(date)}: ${defaultValueFormatter(value)} ${seriesLabel}`,\n        tabIndex: isTabStop ? 0 : -1,\n        onKeyDown: handleKeyDown,\n        onFocus: () => store.setTabStop(date),\n      })}\n    />\n  );\n});\n\ntype LoadingCellProps = CellGeometry & {\n  gradientId: string; // the shimmer gradient shared by every skeleton cell\n};\n\n/** A skeleton cell — the same footprint as a real one, painted by the shared shimmer. */\nconst LoadingCell = memo(function LoadingCell({\n  cx,\n  cy,\n  size,\n  radius,\n  gradientId,\n}: LoadingCellProps) {\n  if (cx === undefined || cy === undefined) return null;\n\n  return (\n    <rect\n      x={cx - size / 2}\n      y={cy - size / 2}\n      width={size}\n      height={size}\n      rx={radius}\n      ry={radius}\n      fill={`url(#${gradientId})`}\n      pointerEvents=\"none\"\n    />\n  );\n});\n\ntype ShimmerGradientProps = {\n  id: string; // gradient id referenced by the skeleton cells\n  animated: boolean; // false leaves the band parked in the middle (reduced motion)\n};\n\n/**\n * One gradient shared by every skeleton cell, in chart pixel space so a whole\n * column lights up together as the band passes. The sweep is a native SVG\n * animation, so it never touches React while it loops.\n */\nfunction ShimmerGradient({ id, animated }: ShimmerGradientProps) {\n  const width = useChartWidth() ?? 0;\n  const bandStart = 0.5 - LOADING_SHIMMER_BAND;\n  const bandEnd = 0.5 + LOADING_SHIMMER_BAND;\n\n  return (\n    <defs>\n      <linearGradient\n        id={id}\n        gradientUnits=\"userSpaceOnUse\"\n        x1={0}\n        y1={0}\n        x2={width}\n        y2={0}\n        gradientTransform=\"skewX(-20)\"\n      >\n        <stop offset={0} stopColor=\"currentColor\" stopOpacity={LOADING_CELL_FLOOR} />\n        <stop offset={bandStart} stopColor=\"currentColor\" stopOpacity={LOADING_CELL_FLOOR} />\n        <stop offset={0.5} stopColor=\"currentColor\" stopOpacity={LOADING_CELL_PEAK} />\n        <stop offset={bandEnd} stopColor=\"currentColor\" stopOpacity={LOADING_CELL_FLOOR} />\n        <stop offset={1} stopColor=\"currentColor\" stopOpacity={LOADING_CELL_FLOOR} />\n        {animated && width > 0 && (\n          <animateTransform\n            attributeName=\"gradientTransform\"\n            type=\"translate\"\n            additive=\"sum\"\n            from={`${-width} 0`}\n            to={`${width} 0`}\n            dur={`${LOADING_ANIMATION_DURATION}ms`}\n            repeatCount=\"indefinite\"\n          />\n        )}\n      </linearGradient>\n    </defs>\n  );\n}\n\nexport interface TooltipProps {\n  variant?: TooltipVariant; // visual style of the tooltip surface\n  roundness?: TooltipRoundness; // border-radius of the tooltip\n  position?: TooltipPosition; // \"variable\" follows the hovered cell (default); \"fixed\" pins the tooltip to the top and tracks the cell's X\n  dateFormatter?: (date: string) => string; // formats the hovered day for the tooltip title\n  valueFormatter?: (value: number) => string; // formats the hovered day's value\n}\n\n/**\n * The hover tooltip. Reads the chart's loading state from context and is hidden\n * automatically while the chart shows its skeleton.\n */\nfunction Tooltip({\n  variant = \"default\",\n  roundness = \"lg\",\n  position = \"variable\",\n  dateFormatter = defaultDateFormatter,\n  valueFormatter = defaultValueFormatter,\n}: TooltipProps) {\n  const { isLoading } = useHeatmapChart();\n\n  if (isLoading) return null;\n\n  return (\n    <ChartTooltip\n      cursor={false}\n      position={position === \"fixed\" ? { y: 0 } : undefined}\n      content={\n        <HeatmapTooltipContent\n          variant={variant}\n          roundness={roundness}\n          dateFormatter={dateFormatter}\n          valueFormatter={valueFormatter}\n        />\n      }\n    />\n  );\n}\n\ntype HeatmapTooltipContentProps = {\n  active?: boolean;\n  payload?: ReadonlyArray<{ payload?: unknown }>;\n  variant: TooltipVariant;\n  roundness: TooltipRoundness;\n  dateFormatter: (date: string) => string;\n  valueFormatter: (value: number) => string;\n};\n\n/**\n * Tooltip body for a hovered day: the formatted date as the title, then the\n * series label and value beside a swatch of that day's ramp color. A scatter's\n * default payload is one entry per axis, so this reads the day off the first\n * entry instead of listing the axes.\n */\nfunction HeatmapTooltipContent({\n  active,\n  payload,\n  variant,\n  roundness,\n  dateFormatter,\n  valueFormatter,\n}: HeatmapTooltipContentProps) {\n  const { config, cell, grid, colorsCount } = useHeatmapChart();\n  const item = payload?.[0]?.payload as GridCell | undefined;\n\n  if (!active || !item) {\n    // Empty tooltip — keeps the position from snapping back to 0,0 between hovers\n    return <span className=\"p-4\" />;\n  }\n\n  const label = config[cell.dataKey]?.label ?? cell.dataKey;\n  const swatch =\n    item.level === 0\n      ? undefined\n      : `var(--color-${cell.dataKey}-${rampIndex(item.level, grid.levels, colorsCount)})`;\n\n  return (\n    <div\n      className={cn(\n        \"border-border/50 grid min-w-32 items-start gap-1.5 border px-2.5 py-1.5 text-xs shadow-xl\",\n        roundnessMap[roundness],\n        variantMap[variant],\n      )}\n    >\n      <div className=\"font-medium\">{dateFormatter(item.date)}</div>\n      <div className=\"flex w-full items-center gap-2\">\n        <div\n          className=\"bg-foreground/10 h-2.5 w-2.5 shrink-0 rounded-[2px]\"\n          style={{ background: swatch }}\n        />\n        <div className=\"flex flex-1 items-center justify-between gap-4 leading-none\">\n          <span className=\"text-muted-foreground\">{label}</span>\n          <span className=\"text-foreground font-mono font-medium tabular-nums\">\n            {valueFormatter(item.value)}\n          </span>\n        </div>\n      </div>\n    </div>\n  );\n}\n\n/** The \"Less → More\" ramp under the grid: one swatch per level, plus the empty cell. */\nfunction HeatmapLegend({ legend }: { legend: LegendSlot }) {\n  const { cell, colorsCount } = useHeatmapChart();\n  const swatchStyle = { borderRadius: Math.min(cell.radius, 5) };\n\n  return (\n    <div\n      className={cn(\n        \"text-muted-foreground flex shrink-0 items-center gap-1 pt-2 text-[11px] leading-none\",\n        legend.align === \"left\" ? \"justify-start\" : \"justify-end\",\n      )}\n    >\n      <span className=\"mr-0.5\">{legend.lessLabel}</span>\n      <span className=\"bg-foreground/[0.08] h-2.5 w-2.5 shrink-0\" style={swatchStyle} />\n      {Array.from({ length: colorsCount }, (_, index) => (\n        <span\n          key={index}\n          className=\"h-2.5 w-2.5 shrink-0\"\n          style={{ ...swatchStyle, background: `var(--color-${cell.dataKey}-${index})` }}\n        />\n      ))}\n      <span className=\"ml-0.5\">{legend.moreLabel}</span>\n    </div>\n  );\n}\n\nEvilHeatmapChart.Cell = Cell;\nEvilHeatmapChart.MonthLabel = MonthLabel;\nEvilHeatmapChart.DayLabel = DayLabel;\nEvilHeatmapChart.Tooltip = Tooltip;\nEvilHeatmapChart.Legend = Legend;\n",
      "type": "registry:component",
      "target": "components/evilcharts/charts/recharts-heatmap-chart.tsx"
    }
  ],
  "type": "registry:component"
}