{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "calendar-heatmap",
  "type": "registry:component",
  "title": "CalendarHeatmap",
  "description": "GitHub-style yearly contribution heatmap with multi-year, weekStart, and i18n support.",
  "dependencies": [
    "clsx",
    "tailwind-merge",
    "date-fns"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "src/components/heatmap/calendar-heatmap.tsx",
      "type": "registry:component",
      "target": "components/heatmap/calendar-heatmap.tsx",
      "content": "import type { Locale, Day as WeekDay } from \"date-fns\";\nimport {\n  differenceInCalendarDays,\n  eachDayOfInterval,\n  format,\n  formatISO,\n  getDay,\n  getMonth,\n  getYear,\n  nextDay,\n  parseISO,\n  subWeeks,\n} from \"date-fns\";\nimport type { CSSProperties, HTMLAttributes, ReactNode } from \"react\";\nimport { createContext, Fragment, use, useMemo } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type Activity = {\n  date: string;\n  value: number;\n};\n\ntype ActivityWithLevel = Activity & {\n  level: number;\n};\n\ntype Week = Array<ActivityWithLevel | undefined>;\n\ntype YearRow = {\n  year: number;\n  startMonth: number;\n  weeks: Week[];\n};\n\nexport type Labels = {\n  months?: string[];\n  weekdays?: string[];\n  stat?: string; // Stat text template. Placeholders: {{value}}, {{year}}\n  cellLabel?: string; // aria-label template. Placeholders: {{date}}, {{value}}\n  heatmapLabel?: string; // aria-label for the heatmap SVG. Placeholder: {{year}}\n  legendLabel?: string; // aria-label for the legend fieldset\n  legendLevelLabel?: string; // aria-label template for legend swatches. Placeholder: {{level}}\n};\n\nexport type ColorConfig = {\n  empty?: string;\n  scale?: string;\n};\n\ntype MonthLabel = {\n  weekIndex: number;\n  label: string;\n  year?: number;\n};\n\n// Non-normalized: 1 empty + (levels-1) colored steps. Normalized: levels colored steps, no empty.\nconst colorStepCount = (levels: number, isNormalized: boolean) =>\n  Math.max(1, isNormalized ? levels : levels - 1);\n\nconst getLevelFill = (\n  level: number,\n  levels: number,\n  isNormalized: boolean,\n  highlighted = false,\n  colors?: ColorConfig,\n): string => {\n  const emptyColor = colors?.empty ?? \"var(--color-secondary)\";\n  const scaleColor = colors?.scale ?? \"var(--color-chart-1)\";\n\n  if (level === 0) return emptyColor;\n  const steps = colorStepCount(levels, isNormalized);\n  const opacity =\n    steps === 1 ? 100 : Math.round(20 + ((level - 1) * 80) / (steps - 1));\n  const finalOpacity = highlighted ? Math.round(opacity * 0.6) : opacity;\n  return `color-mix(in oklch, ${scaleColor} ${finalOpacity}%, transparent)`;\n};\n\nconst calculateLevel = (\n  value: number,\n  minValue: number,\n  maxValue: number,\n  levels: number,\n  isNormalized: boolean,\n): number => {\n  const steps = colorStepCount(levels, isNormalized);\n  if (!Number.isFinite(value)) return isNormalized ? 1 : 0;\n  if (isNormalized) {\n    if (maxValue <= minValue) return 1;\n    const percentage = (value - minValue) / (maxValue - minValue);\n    return Math.max(1, Math.min(steps, Math.ceil(percentage * steps)));\n  }\n  if (value <= 0 || maxValue <= 0) return 0;\n  const percentage = value / maxValue;\n  return Math.max(1, Math.min(steps, Math.ceil(percentage * steps)));\n};\n\nconst generateMonthLabels = (locale?: Locale): string[] => {\n  return Array.from({ length: 12 }, (_, i) => {\n    const date = new Date(2000, i, 1);\n    if (locale) return format(date, \"LLL\", { locale });\n    return date.toLocaleString(\"en-US\", { month: \"short\" });\n  });\n};\n\nconst generateWeekdayLabels = (locale?: Locale): string[] => {\n  return Array.from({ length: 7 }, (_, i) => {\n    const date = new Date(2000, 0, 2 + i);\n    if (locale) return format(date, \"EEE\", { locale });\n    return date.toLocaleString(\"en-US\", { weekday: \"short\" });\n  });\n};\n\ntype CalendarHeatmapContextType = {\n  data: ActivityWithLevel[];\n  weeks: Week[];\n  yearRows: YearRow[];\n  blockMargin: number;\n  blockRadius: number;\n  blockSize: number;\n  blockAspectRatio: number;\n  blockWidth: number;\n  fontSize: number;\n  labels: Labels;\n  labelHeight: number;\n  levels: number;\n  isNormalized: boolean;\n  totalCount: number;\n  weekStart: WeekDay;\n  year: number;\n  width: number;\n  height: number;\n  hasEmptyColumn: boolean;\n  continuousMonths: boolean;\n  locale?: Locale;\n  colors?: ColorConfig;\n};\n\nconst EMPTY_STYLE: CSSProperties = {};\nconst LABEL_MARGIN = 8;\n\nconst CalendarHeatmapContext = createContext<CalendarHeatmapContextType | null>(\n  null,\n);\n\nconst useCalendarHeatmap = () => {\n  const context = use(CalendarHeatmapContext);\n\n  if (!context) {\n    throw new Error(\n      \"CalendarHeatmap components must be used within a CalendarHeatmap\",\n    );\n  }\n\n  return context;\n};\n\nconst fillHoles = (activities: ActivityWithLevel[]): ActivityWithLevel[] => {\n  if (activities.length === 0) {\n    return [];\n  }\n\n  const sortedActivities = [...activities].sort((a, b) =>\n    a.date.localeCompare(b.date),\n  );\n\n  const calendar = new Map<string, ActivityWithLevel>(\n    activities.map((a) => [a.date, a]),\n  );\n\n  const firstActivity = sortedActivities[0] as ActivityWithLevel;\n  const lastActivity = sortedActivities.at(-1);\n\n  if (!lastActivity) {\n    return [];\n  }\n\n  return eachDayOfInterval({\n    start: parseISO(firstActivity.date),\n    end: parseISO(lastActivity.date),\n  }).map((day) => {\n    const date = formatISO(day, { representation: \"date\" });\n\n    if (calendar.has(date)) {\n      return calendar.get(date) as ActivityWithLevel;\n    }\n\n    return {\n      date,\n      value: 0,\n      level: 0,\n    };\n  });\n};\n\nconst groupByYearAndMonth = (\n  activities: ActivityWithLevel[],\n  weekStart: WeekDay = 0,\n  hasEmptyColumn = false,\n): YearRow[] => {\n  if (activities.length === 0) {\n    return [];\n  }\n\n  const normalizedActivities = fillHoles(activities);\n\n  const activitiesByYearMonth = new Map<string, ActivityWithLevel[]>();\n\n  for (const activity of normalizedActivities) {\n    const date = parseISO(activity.date);\n    const year = getYear(date);\n    const month = getMonth(date);\n    const key = `${year}-${String(month).padStart(2, \"0\")}`;\n\n    if (!activitiesByYearMonth.has(key)) {\n      activitiesByYearMonth.set(key, []);\n    }\n    activitiesByYearMonth.get(key)?.push(activity);\n  }\n\n  const sortedKeys = Array.from(activitiesByYearMonth.keys()).sort();\n\n  if (sortedKeys.length === 0) {\n    return [];\n  }\n\n  const firstKey = sortedKeys[0];\n  const [firstYearStr, firstMonthStr] = firstKey.split(\"-\");\n  const startYear = parseInt(firstYearStr, 10);\n  const startMonth = parseInt(firstMonthStr, 10);\n\n  const yearRows: YearRow[] = [];\n  let currentYear = startYear;\n  let currentMonth = startMonth;\n\n  while (true) {\n    const rowWeeks: Week[] = [];\n    let hasData = false;\n\n    for (let monthOffset = 0; monthOffset < 12; monthOffset++) {\n      const monthIndex = (currentMonth + monthOffset) % 12;\n      const yearOffset = Math.floor((currentMonth + monthOffset) / 12);\n      const year = currentYear + yearOffset;\n      const key = `${year}-${String(monthIndex).padStart(2, \"0\")}`;\n\n      const monthActivities = activitiesByYearMonth.get(key) || [];\n\n      if (monthActivities.length > 0) {\n        hasData = true;\n\n        const firstActivity = monthActivities[0] as ActivityWithLevel;\n        const firstDate = parseISO(firstActivity.date);\n        const firstCalendarDate =\n          getDay(firstDate) === weekStart\n            ? firstDate\n            : subWeeks(nextDay(firstDate, weekStart), 1);\n\n        const paddedActivities: Array<ActivityWithLevel | undefined> = [\n          ...new Array(\n            differenceInCalendarDays(firstDate, firstCalendarDate),\n          ).fill(undefined),\n          ...monthActivities,\n        ];\n\n        const numberOfWeeks = Math.ceil(paddedActivities.length / 7);\n        const monthWeeks: Week[] = new Array(numberOfWeeks)\n          .fill(undefined)\n          .map((_, weekIndex) =>\n            paddedActivities.slice(weekIndex * 7, weekIndex * 7 + 7),\n          );\n\n        if (hasEmptyColumn && rowWeeks.length > 0) {\n          rowWeeks.push(new Array(7).fill(undefined) as Week);\n        }\n\n        rowWeeks.push(...monthWeeks);\n      }\n    }\n\n    if (!hasData) {\n      break;\n    }\n\n    yearRows.push({\n      year: currentYear,\n      startMonth: currentMonth,\n      weeks: rowWeeks,\n    });\n\n    currentYear++;\n    currentMonth = startMonth;\n\n    const hasNextYearData = sortedKeys.some((key) => {\n      const [yearStr] = key.split(\"-\");\n      return parseInt(yearStr, 10) >= currentYear;\n    });\n\n    if (!hasNextYearData) {\n      break;\n    }\n  }\n\n  return yearRows;\n};\n\nconst groupContinuous = (\n  activities: ActivityWithLevel[],\n  weekStart: WeekDay = 0,\n): YearRow[] => {\n  if (activities.length === 0) return [];\n\n  const normalizedActivities = fillHoles(activities);\n\n  const first = normalizedActivities[0] as ActivityWithLevel;\n  const firstDate = parseISO(first.date);\n  const firstCalendarDate =\n    getDay(firstDate) === weekStart\n      ? firstDate\n      : subWeeks(nextDay(firstDate, weekStart), 1);\n\n  const leadingPad = differenceInCalendarDays(firstDate, firstCalendarDate);\n\n  const padded: Array<ActivityWithLevel | undefined> = [\n    ...new Array(leadingPad).fill(undefined),\n    ...normalizedActivities,\n  ];\n\n  const numberOfWeeks = Math.ceil(padded.length / 7);\n  const allWeeks: Week[] = new Array(numberOfWeeks)\n    .fill(undefined)\n    .map((_, i) => padded.slice(i * 7, i * 7 + 7));\n\n  const yearMap = new Map<number, Week[]>();\n  for (const week of allWeeks) {\n    const firstActivity = week.find((a) => a !== undefined);\n    const year = firstActivity\n      ? getYear(parseISO(firstActivity.date))\n      : undefined;\n    if (year !== undefined) {\n      if (!yearMap.has(year)) yearMap.set(year, []);\n      yearMap.get(year)?.push(week);\n    }\n  }\n\n  return Array.from(yearMap.entries())\n    .sort(([a], [b]) => a - b)\n    .map(([year, weeks]) => ({\n      year,\n      startMonth: 0,\n      weeks,\n    }));\n};\n\nconst getMonthLabels = (\n  weeks: Week[],\n  monthNames: string[] = generateMonthLabels(),\n): MonthLabel[] => {\n  return weeks\n    .reduce<MonthLabel[]>((labels, week, weekIndex) => {\n      const firstActivity = week.find((activity) => activity !== undefined);\n\n      if (!firstActivity) {\n        return labels;\n      }\n\n      const month = monthNames[getMonth(parseISO(firstActivity.date))];\n\n      if (!month) {\n        const monthName = new Date(firstActivity.date).toLocaleString(\"en-US\", {\n          month: \"short\",\n        });\n        throw new Error(\n          `Unexpected error: undefined month label for ${monthName}.`,\n        );\n      }\n\n      const prevLabel = labels.at(-1);\n\n      if (weekIndex === 0 || !prevLabel || prevLabel.label !== month) {\n        return labels.concat({ weekIndex, label: month });\n      }\n\n      return labels;\n    }, [])\n    .filter(({ weekIndex }, index, labels) => {\n      const minWeeks = 3;\n\n      if (index === 0) {\n        return labels[1] && labels[1].weekIndex - weekIndex >= minWeeks;\n      }\n\n      if (index === labels.length - 1) {\n        return weeks.slice(weekIndex).length >= minWeeks;\n      }\n\n      return true;\n    });\n};\n\nexport type CalendarHeatmapProps = HTMLAttributes<HTMLDivElement> & {\n  data: Activity[];\n  weekStart?: WeekDay;\n  continuousMonths?: boolean;\n  hasEmptyColumn?: boolean;\n  blockSize?: number;\n  blockMargin?: number;\n  blockRadius?: number;\n  blockAspectRatio?: number;\n  levels?: number;\n  isNormalized?: boolean;\n  colors?: ColorConfig;\n  locale?: Locale;\n  labels?: Labels;\n  fontSize?: number;\n  emptyState?: ReactNode;\n  totalCount?: number;\n  style?: CSSProperties;\n  className?: string;\n  children: ReactNode;\n};\n\n/**\n * Calendar Heatmap\n *\n * A GitHub-style contribution calendar showing daily activity over months and years.\n * Each cell represents one day, arranged in weeks (rows) and months (columns).\n *\n * @example\n * ```tsx\n * <CalendarHeatmap data={data} weekStart={1} continuousMonths>\n *   <CalendarHeatmapBody>\n *     {({ activity, dayIndex, weekIndex }) => (\n *       <CalendarHeatmapBlock\n *         activity={activity}\n *         dayIndex={dayIndex}\n *         weekIndex={weekIndex}\n *       />\n *     )}\n *   </CalendarHeatmapBody>\n *   <CalendarHeatmapFooter>\n *     <CalendarHeatmapStat />\n *     <CalendarHeatmapLegend />\n *   </CalendarHeatmapFooter>\n * </CalendarHeatmap>\n * ```\n *\n * @param data - Array of activities with date (YYYY-MM-DD) and value\n * @param weekStart - First day of week (0=Sunday, 1=Monday). Default: 0\n * @param continuousMonths - Display months continuously vs. grouped by year. Default: true\n * @param hasEmptyColumn - Add empty column between months. Default: false\n * @param blockAspectRatio - Width/height ratio of blocks. Default: 1\n * @param levels - Total number of legend cells (including empty when not normalized). Default: 5\n * @param isNormalized - When true, uses min-max normalization across the dataset (suitable for signed values). When false (default), treats 0 as empty and scales from 0 to max.\n */\nexport const CalendarHeatmap = ({\n  data,\n  weekStart = 0,\n  continuousMonths = true,\n  hasEmptyColumn = false,\n  blockSize = 12,\n  blockMargin = 4,\n  blockRadius = 2,\n  blockAspectRatio = 1,\n  levels: levelsProp = 5,\n  isNormalized = false,\n  colors,\n  locale,\n  labels: labelsProp,\n  fontSize = 14,\n  emptyState,\n  totalCount: totalCountProp,\n  style = EMPTY_STYLE,\n  className,\n  children,\n  ...props\n}: CalendarHeatmapProps) => {\n  const levels = Math.max(1, levelsProp);\n\n  const dataWithLevels = useMemo((): ActivityWithLevel[] => {\n    if (data.length === 0) return [];\n\n    const maxCount = data.reduce((max, d) => Math.max(max, d.value), 1);\n    const minCount = isNormalized\n      ? data.reduce((min, d) => Math.min(min, d.value), Infinity)\n      : 0;\n\n    return data.map((activity) => ({\n      ...activity,\n      level: calculateLevel(\n        activity.value,\n        minCount,\n        maxCount,\n        levels,\n        isNormalized,\n      ),\n    }));\n  }, [data, levels, isNormalized]);\n\n  const yearRows = useMemo(\n    () =>\n      continuousMonths\n        ? groupContinuous(dataWithLevels, weekStart)\n        : groupByYearAndMonth(dataWithLevels, weekStart, hasEmptyColumn),\n    [dataWithLevels, weekStart, hasEmptyColumn, continuousMonths],\n  );\n  const weeks = useMemo(() => yearRows.flatMap((r) => r.weeks), [yearRows]);\n\n  const labels = useMemo(\n    () => ({\n      months: generateMonthLabels(locale),\n      weekdays: generateWeekdayLabels(locale),\n      cellLabel: \"{{date}}: {{value}} contributions\",\n      heatmapLabel: \"Contribution heatmap for {{year}}\",\n      legendLabel: \"Activity intensity legend\",\n      legendLevelLabel: \"{{level}} contributions\",\n      ...labelsProp,\n    }),\n    [locale, labelsProp],\n  );\n  const labelHeight = fontSize + LABEL_MARGIN;\n\n  const year =\n    data.length > 0\n      ? getYear(parseISO(data[0].date))\n      : new Date().getFullYear();\n\n  const totalCount =\n    typeof totalCountProp === \"number\"\n      ? totalCountProp\n      : dataWithLevels.reduce((sum, activity) => sum + activity.value, 0);\n\n  const blockWidth = blockSize * blockAspectRatio;\n  const width = weeks.length * (blockWidth + blockMargin) - blockMargin;\n  const height = labelHeight + (blockSize + blockMargin) * 7 - blockMargin;\n\n  const contextValue = useMemo<CalendarHeatmapContextType>(\n    () => ({\n      data: dataWithLevels,\n      weeks,\n      yearRows,\n      blockMargin,\n      blockRadius,\n      blockSize,\n      blockAspectRatio,\n      blockWidth,\n      fontSize,\n      labels,\n      labelHeight,\n      levels,\n      isNormalized,\n      totalCount,\n      weekStart,\n      year,\n      width,\n      height,\n      hasEmptyColumn,\n      continuousMonths,\n      locale,\n      colors,\n    }),\n    [\n      dataWithLevels,\n      weeks,\n      yearRows,\n      blockMargin,\n      blockRadius,\n      blockSize,\n      blockAspectRatio,\n      blockWidth,\n      fontSize,\n      labels,\n      labelHeight,\n      levels,\n      isNormalized,\n      totalCount,\n      weekStart,\n      year,\n      width,\n      height,\n      hasEmptyColumn,\n      continuousMonths,\n      locale,\n      colors,\n    ],\n  );\n\n  if (data.length === 0) {\n    return emptyState ? emptyState : null;\n  }\n\n  return (\n    <CalendarHeatmapContext value={contextValue}>\n      <div\n        data-slot=\"calendar-heatmap\"\n        className={cn(\"flex w-max max-w-full flex-col gap-2 p-4\", className)}\n        style={{ fontSize, ...style }}\n        {...props}\n      >\n        {children}\n      </div>\n    </CalendarHeatmapContext>\n  );\n};\n\nexport type CalendarHeatmapBlockProps = HTMLAttributes<SVGRectElement> & {\n  activity: ActivityWithLevel;\n  dayIndex: number;\n  weekIndex: number;\n  highlighted?: boolean;\n  onCellClick?: (activity: ActivityWithLevel) => void;\n  onCellHover?: (activity: ActivityWithLevel | null) => void;\n};\n\nexport const CalendarHeatmapBlock = ({\n  ref,\n  activity,\n  dayIndex,\n  weekIndex,\n  highlighted = false,\n  onCellClick,\n  onCellHover,\n  onClick,\n  onKeyDown,\n  onMouseEnter,\n  onMouseLeave,\n  className,\n  style: styleProp,\n  ...props\n}: CalendarHeatmapBlockProps & {\n  ref?: React.RefObject<SVGRectElement | null>;\n}) => {\n  const {\n    blockSize,\n    blockWidth,\n    blockMargin,\n    blockRadius,\n    labelHeight,\n    labels,\n    levels,\n    isNormalized,\n    colors,\n  } = useCalendarHeatmap();\n\n  const level = Math.max(\n    0,\n    Math.min(colorStepCount(levels, isNormalized), activity.level),\n  );\n\n  const ariaLabel = (labels.cellLabel ?? \"{{date}}: {{value}} contributions\")\n    .replace(\"{{date}}\", activity.date)\n    .replace(\"{{value}}\", String(activity.value));\n\n  return (\n    <rect\n      ref={ref}\n      data-slot=\"calendar-heatmap-block\"\n      role={onCellClick ? \"button\" : \"img\"}\n      tabIndex={onCellClick ? 0 : -1}\n      aria-label={ariaLabel}\n      className={cn(\n        \"motion-safe:transition-opacity motion-safe:hover:opacity-70\",\n        onCellClick &&\n          \"cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n        className,\n      )}\n      data-value={activity.value}\n      data-date={activity.date}\n      data-level={level}\n      data-highlighted={highlighted || undefined}\n      height={blockSize}\n      rx={blockRadius}\n      ry={blockRadius}\n      width={blockWidth}\n      x={(blockWidth + blockMargin) * weekIndex}\n      y={labelHeight + (blockSize + blockMargin) * dayIndex}\n      style={{\n        fill: getLevelFill(level, levels, isNormalized, highlighted, colors),\n        ...styleProp,\n      }}\n      onClick={(event) => {\n        onCellClick?.(activity);\n        onClick?.(event);\n      }}\n      onKeyDown={(event) => {\n        if (onCellClick && (event.key === \"Enter\" || event.key === \" \")) {\n          event.preventDefault();\n          onCellClick(activity);\n        }\n        onKeyDown?.(event);\n      }}\n      onMouseEnter={(event) => {\n        onCellHover?.(activity);\n        onMouseEnter?.(event);\n      }}\n      onMouseLeave={(event) => {\n        onCellHover?.(null);\n        onMouseLeave?.(event);\n      }}\n      {...props}\n    />\n  );\n};\nCalendarHeatmapBlock.displayName = \"CalendarHeatmapBlock\";\n\nexport type CalendarHeatmapBodyProps = Omit<\n  HTMLAttributes<HTMLDivElement>,\n  \"children\"\n> & {\n  hideMonthLabels?: boolean;\n  hideWeekdayLabels?: boolean;\n  hideYearLabels?: boolean;\n  className?: string;\n  labelClassName?: string;\n  yearClassName?: string;\n  children: (props: {\n    activity: ActivityWithLevel;\n    dayIndex: number;\n    weekIndex: number;\n  }) => ReactNode;\n  renderYearFooter?: (props: { year: number; totalCount: number }) => ReactNode;\n};\n\nexport const CalendarHeatmapBody = ({\n  hideMonthLabels = false,\n  hideWeekdayLabels = false,\n  hideYearLabels = false,\n  className,\n  labelClassName,\n  yearClassName,\n  children,\n  renderYearFooter,\n  ...props\n}: CalendarHeatmapBodyProps) => {\n  const {\n    yearRows,\n    blockSize,\n    blockWidth,\n    blockMargin,\n    labels,\n    labelHeight,\n    fontSize,\n    weekStart,\n    data,\n  } = useCalendarHeatmap();\n\n  const weekdayLabelWidth = hideWeekdayLabels ? 0 : 40;\n  const strokePadding = 3;\n\n  const rowData = useMemo(() => {\n    return yearRows.map((yearRow) => {\n      const width =\n        yearRow.weeks.length * (blockWidth + blockMargin) - blockMargin;\n      const height = labelHeight + (blockSize + blockMargin) * 7 - blockMargin;\n      const monthLabels = getMonthLabels(yearRow.weeks, labels.months);\n      const yearTotalCount = data\n        .filter((activity) => getYear(parseISO(activity.date)) === yearRow.year)\n        .reduce((sum, activity) => sum + activity.value, 0);\n\n      return {\n        yearRow,\n        width,\n        height,\n        monthLabels,\n        yearTotalCount,\n      };\n    });\n  }, [\n    yearRows,\n    blockSize,\n    blockWidth,\n    blockMargin,\n    labelHeight,\n    labels.months,\n    data,\n  ]);\n\n  const maxWidth = Math.max(...rowData.map((r) => r.width));\n  const totalWidth = weekdayLabelWidth + maxWidth + strokePadding * 2;\n\n  return (\n    <div\n      data-slot=\"calendar-heatmap-body\"\n      className={cn(\n        \"flex max-w-full flex-col gap-6 overflow-x-auto overflow-y-hidden py-4\",\n        className,\n      )}\n      {...props}\n    >\n      {rowData.map(({ yearRow, height, monthLabels, yearTotalCount }) => (\n        <div key={`year-row-${yearRow.year}`}>\n          {!hideYearLabels && (\n            <div className={cn(\"mb-2 text-muted-foreground\", yearClassName)}>\n              {yearRow.year}\n            </div>\n          )}\n          <svg\n            role=\"img\"\n            aria-label={(\n              labels.heatmapLabel ?? \"Contribution heatmap for {{year}}\"\n            ).replace(\"{{year}}\", String(yearRow.year))}\n            className=\"block overflow-visible rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n            height={height + strokePadding * 2}\n            viewBox={`0 0 ${totalWidth} ${height + strokePadding * 2}`}\n            width={totalWidth}\n          >\n            <g transform={`translate(0, ${strokePadding})`}>\n              {!hideMonthLabels && (\n                <g className={cn(\"fill-current font-mono\", labelClassName)}>\n                  {monthLabels.map(({ label, weekIndex }) => (\n                    <text\n                      dominantBaseline=\"hanging\"\n                      key={`${yearRow.year}-${weekIndex}`}\n                      x={\n                        weekdayLabelWidth +\n                        strokePadding +\n                        (blockWidth + blockMargin) * weekIndex\n                      }\n                      style={{ fontSize: `${fontSize * 0.75}px` }}\n                    >\n                      {label}\n                    </text>\n                  ))}\n                </g>\n              )}\n              {!hideWeekdayLabels && (\n                <g\n                  className={cn(\n                    \"fill-current font-mono text-xs\",\n                    labelClassName,\n                  )}\n                >\n                  {labels.weekdays?.map((label, dayIndex) => {\n                    const adjustedIndex = (dayIndex + weekStart) % 7;\n                    const adjustedLabel =\n                      labels.weekdays?.[adjustedIndex] || label;\n\n                    return (\n                      <text\n                        key={`weekday-${yearRow.year}-${label}`}\n                        x={0}\n                        y={\n                          labelHeight +\n                          (blockSize + blockMargin) * dayIndex +\n                          blockSize / 2\n                        }\n                        dominantBaseline=\"middle\"\n                        textAnchor=\"start\"\n                        style={{ fontSize: `${fontSize * 0.75}px` }}\n                      >\n                        {adjustedLabel}\n                      </text>\n                    );\n                  })}\n                </g>\n              )}\n              <g\n                transform={`translate(${weekdayLabelWidth + strokePadding}, 0)`}\n              >\n                {yearRow.weeks.map((week, weekIndex) =>\n                  week.map((activity, dayIndex) => {\n                    if (!activity) {\n                      return null;\n                    }\n\n                    return (\n                      <Fragment key={`${yearRow.year}-${activity.date}`}>\n                        {children({ activity, dayIndex, weekIndex })}\n                      </Fragment>\n                    );\n                  }),\n                )}\n              </g>\n            </g>\n          </svg>\n          {renderYearFooter?.({\n            year: yearRow.year,\n            totalCount: yearTotalCount,\n          })}\n        </div>\n      ))}\n    </div>\n  );\n};\n\nexport type CalendarHeatmapFooterProps = HTMLAttributes<HTMLDivElement>;\n\nexport const CalendarHeatmapFooter = ({\n  className,\n  ...props\n}: CalendarHeatmapFooterProps) => (\n  <div\n    data-slot=\"calendar-heatmap-footer\"\n    className={cn(\n      \"flex flex-wrap gap-1 whitespace-nowrap sm:gap-x-4\",\n      className,\n    )}\n    {...props}\n  />\n);\n\nexport type CalendarHeatmapStatProps = Omit<\n  HTMLAttributes<HTMLDivElement>,\n  \"children\"\n> & {\n  compute?: (data: ActivityWithLevel[]) => number | string;\n  label?: string; // Template overriding labels.stat. Placeholders: {{value}}, {{year}}\n  children?: (result: {\n    value: number | string;\n    data: ActivityWithLevel[];\n    year: number;\n  }) => ReactNode;\n};\n\nexport const CalendarHeatmapStat = ({\n  compute,\n  label,\n  className,\n  children,\n  ...props\n}: CalendarHeatmapStatProps) => {\n  const { data, totalCount, year, labels } = useCalendarHeatmap();\n\n  const value = compute ? compute(data) : totalCount;\n\n  if (children) {\n    return <>{children({ value, data, year })}</>;\n  }\n\n  const template =\n    label ?? labels.stat ?? \"{{value}} contributions in {{year}}\";\n\n  return (\n    <div\n      data-slot=\"calendar-heatmap-stat\"\n      className={cn(\"text-muted-foreground tabular-nums\", className)}\n      {...props}\n    >\n      {template\n        .replace(\"{{value}}\", String(value))\n        .replace(\"{{year}}\", String(year))}\n    </div>\n  );\n};\n\nexport type CalendarHeatmapLegendProps = Omit<\n  HTMLAttributes<HTMLFieldSetElement>,\n  \"children\"\n> & {\n  labels?: { less?: string; more?: string };\n  children?: (props: { level: number }) => ReactNode;\n};\n\nexport const CalendarHeatmapLegend = ({\n  labels: labelsProp,\n  className,\n  children,\n  ...props\n}: CalendarHeatmapLegendProps) => {\n  const {\n    levels,\n    isNormalized,\n    blockSize,\n    blockWidth,\n    blockRadius,\n    colors,\n    labels,\n  } = useCalendarHeatmap();\n\n  const lessLabel = labelsProp?.less ?? \"Less\";\n  const moreLabel = labelsProp?.more ?? \"More\";\n\n  const legendLevels = Array.from({ length: levels }, (_, i) =>\n    isNormalized ? i + 1 : i,\n  );\n\n  return (\n    <fieldset\n      data-slot=\"calendar-heatmap-legend\"\n      aria-label={labels.legendLabel ?? \"Activity intensity legend\"}\n      className={cn(\n        \"ml-auto flex items-center gap-1 text-muted-foreground\",\n        className,\n      )}\n      {...props}\n    >\n      <span className=\"mr-1 font-medium text-xs\">{lessLabel}</span>\n      {legendLevels.map((level) =>\n        children ? (\n          <Fragment key={`legend-level-${level}`}>\n            {children({ level })}\n          </Fragment>\n        ) : (\n          <svg\n            role=\"img\"\n            aria-label={(\n              labels.legendLevelLabel ?? \"{{level}} contributions\"\n            ).replace(\"{{level}}\", String(level))}\n            height={blockSize}\n            key={`legend-level-${level}`}\n            width={blockWidth}\n          >\n            <rect\n              data-level={level}\n              height={blockSize}\n              rx={blockRadius}\n              ry={blockRadius}\n              width={blockWidth}\n              style={{\n                fill: getLevelFill(level, levels, isNormalized, false, colors),\n              }}\n            />\n          </svg>\n        ),\n      )}\n      <span className=\"ml-1 font-medium text-xs\">{moreLabel}</span>\n    </fieldset>\n  );\n};\n"
    }
  ],
  "cssVars": {
    "theme": {
      "secondary": "oklch(96.7% 0.001 286.4)",
      "chart-1": "oklch(64.6% 0.222 41.1)",
      "muted-foreground": "oklch(55.2% 0.014 285.9)"
    }
  }
}
