{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "date-heatmap",
  "type": "registry:component",
  "title": "DateHeatmap",
  "description": "Date × hour matrix for zooming into a narrow time window with daily Sum column.",
  "dependencies": [
    "clsx",
    "tailwind-merge",
    "date-fns"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "src/components/heatmap/date-heatmap.tsx",
      "type": "registry:component",
      "target": "components/heatmap/date-heatmap.tsx",
      "content": "import type { Locale } from \"date-fns\";\nimport { format } from \"date-fns\";\nimport type { CSSProperties, HTMLAttributes, ReactNode } from \"react\";\nimport { createContext, Fragment, use, useMemo } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nfunction formatDateWithWeekday(\n  date: Date | string,\n  dateFormat: string = \"EEE, MMM dd, yyyy\",\n  locale?: Locale,\n): string {\n  const d = typeof date === \"string\" ? new Date(date) : date;\n  return format(d, dateFormat, locale ? { locale } : undefined);\n}\n\nexport type DateHour =\n  | 0\n  | 1\n  | 2\n  | 3\n  | 4\n  | 5\n  | 6\n  | 7\n  | 8\n  | 9\n  | 10\n  | 11\n  | 12\n  | 13\n  | 14\n  | 15\n  | 16\n  | 17\n  | 18\n  | 19\n  | 20\n  | 21\n  | 22\n  | 23;\n\nexport type DateHourlyActivity = {\n  date: string; // YYYY-MM-DD\n  hour: number; // 0–23\n  value: number;\n};\n\ntype DateHourlyActivityWithLevel = DateHourlyActivity & {\n  level: number;\n};\n\nexport type DateExtraRowSpec = {\n  label: ReactNode;\n  compute: (data: DateHourlyActivity[]) => number[]; // length 24\n};\n\nexport type DateExtraColumnSpec = {\n  label: ReactNode;\n  compute: (data: DateHourlyActivity[], dates: string[]) => number[]; // length = dates.length\n};\n\ntype ResolvedDateExtraRow = {\n  label: ReactNode;\n  values: { hour: number; value: number; level: number }[];\n};\n\ntype ResolvedDateExtraColumn = {\n  label: ReactNode;\n  values: { date: string; value: number; level: number }[];\n};\n\ntype DateHeatmapContextType = {\n  data: DateHourlyActivityWithLevel[];\n  dates: string[]; // Sorted list of unique dates\n  extraRow: ResolvedDateExtraRow | null;\n  extraColumn: ResolvedDateExtraColumn | null;\n  blockMargin: number;\n  blockRadius: number;\n  blockSize: number;\n  blockAspectRatio: number;\n  blockWidth: number;\n  fontSize: number;\n  labels: DateHeatmapLabels;\n  labelWidth: number;\n  labelHeight: number;\n  levels: number;\n  isNormalized: boolean;\n  totalCount: number;\n  width: number;\n  height: number;\n  dateFormat: string;\n  locale?: Locale;\n  colors?: ColorConfig;\n};\n\nexport type DateHeatmapLabels = {\n  hours?: string[];\n  endHour?: string | null; // null = hide the end hour label\n  stat?: string; // Stat text template. Placeholder: {{value}}\n  cellLabel?: string; // aria-label template. Placeholders: {{date}}, {{hour}}, {{value}}\n  heatmapLabel?: string; // aria-label for the heatmap SVG\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\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 DEFAULT_HOUR_LABELS = Array.from({ length: 24 }, (_, i) =>\n  i.toString().padStart(2, \"0\"),\n);\n\nconst TWELVE_HOUR_LABELS = Array.from({ length: 24 }, (_, i) => {\n  if (i === 0) return \"AM\";\n  if (i < 12) return String(i);\n  if (i === 12) return \"PM\";\n  return String(i - 12);\n});\n\nconst HOURS = Array.from({ length: 24 }, (_, i) => i);\n\nconst EMPTY_STYLE: CSSProperties = {};\nconst LABEL_MARGIN = 8;\nconst PADDING = 20;\n\nconst DateHeatmapContext = createContext<DateHeatmapContextType | null>(null);\n\nconst useDateHeatmap = () => {\n  const context = use(DateHeatmapContext);\n\n  if (!context) {\n    throw new Error(\"DateHeatmap components must be used within a DateHeatmap\");\n  }\n\n  return context;\n};\n\nexport type DateHeatmapProps = HTMLAttributes<HTMLDivElement> & {\n  data: DateHourlyActivity[];\n  use12Hour?: boolean;\n  dateFormat?: string;\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?: DateHeatmapLabels;\n  fontSize?: number;\n  emptyState?: ReactNode;\n  totalCount?: number;\n  extraRow?: DateExtraRowSpec;\n  extraColumn?: DateExtraColumnSpec;\n  style?: CSSProperties;\n  className?: string;\n  children: ReactNode;\n};\n\n/**\n * Date Heatmap\n *\n * A time-range analysis heatmap showing hourly activity across specific dates.\n * Each row represents a date (rows auto-derived from unique `date` values in data),\n * columns represent hours (0–23), with optional aggregated row (per hour) and column (per date).\n *\n * @example\n * ```tsx\n * <DateHeatmap data={data} use12Hour dateFormat=\"MMM dd, yyyy\">\n *   <DateHeatmapBody>\n *     {({ activity, dateIndex }) => (\n *       <DateHeatmapBlock\n *         activity={activity}\n *         dateIndex={dateIndex}\n *       />\n *     )}\n *   </DateHeatmapBody>\n *   <DateHeatmapFooter>\n *     <DateHeatmapStat />\n *     <DateHeatmapLegend />\n *   </DateHeatmapFooter>\n * </DateHeatmap>\n * ```\n *\n * @param data - Array of activities with date (YYYY-MM-DD), hour (0–23), and numeric value\n * @param use12Hour - Use 12-hour format for hour labels. Default: false\n * @param dateFormat - date-fns format string for date labels. Default: \"MMM dd, yyyy\"\n * @param blockSize - Block height in pixels. Default: 24\n * @param blockAspectRatio - Block width/height ratio. Default: 1 (square)\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 * @param extraRow - Appends an aggregated row below the grid. `compute` receives all activities and must return 24 values (one per hour). Rendered through `renderExtraRow` on the body, or falls back to `children`.\n * @param extraColumn - Appends an aggregated column to the right of the grid. `compute` receives all activities plus the sorted date list and must return one value per date. Rendered through `renderExtraColumn` on the body, or falls back to `children`.\n */\nexport const DateHeatmap = ({\n  data,\n  use12Hour = false,\n  dateFormat = \"MMM dd, yyyy\",\n  blockSize = 24,\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  extraRow,\n  extraColumn,\n  style = EMPTY_STYLE,\n  className,\n  children,\n  ...props\n}: DateHeatmapProps) => {\n  const levels = Math.max(1, levelsProp);\n\n  const dataWithLevels = useMemo((): DateHourlyActivityWithLevel[] => {\n    if (data.length === 0) return [];\n\n    const maxRegular = data.reduce((m, d) => Math.max(m, d.value), 1);\n    const minRegular = isNormalized\n      ? data.reduce((m, d) => Math.min(m, d.value), Infinity)\n      : 0;\n\n    return data.map((activity) => ({\n      ...activity,\n      level: calculateLevel(\n        activity.value,\n        minRegular,\n        maxRegular,\n        levels,\n        isNormalized,\n      ),\n    }));\n  }, [data, levels, isNormalized]);\n\n  const labels = useMemo<DateHeatmapLabels>(\n    () => ({\n      hours: use12Hour ? TWELVE_HOUR_LABELS : DEFAULT_HOUR_LABELS,\n      endHour: use12Hour ? \"12\" : \"00\",\n      cellLabel: \"{{date}} {{hour}}: {{value}}\",\n      heatmapLabel: \"Activity heatmap by date and hour\",\n      legendLabel: \"Activity intensity legend\",\n      legendLevelLabel: \"{{level}} contributions\",\n      ...labelsProp,\n    }),\n    [use12Hour, labelsProp],\n  );\n  const labelWidth = fontSize * 7.5 + LABEL_MARGIN;\n\n  const dates = useMemo(() => {\n    const uniqueDates = new Set<string>();\n    dataWithLevels.forEach((activity) => {\n      uniqueDates.add(activity.date);\n    });\n    return Array.from(uniqueDates).sort();\n  }, [dataWithLevels]);\n\n  const resolvedExtraRow = useMemo<ResolvedDateExtraRow | null>(() => {\n    if (!extraRow) return null;\n    const values = extraRow.compute(data);\n    const maxVal = values.reduce((m, v) => Math.max(m, v), 1);\n    const minVal = isNormalized\n      ? values.reduce((m, v) => Math.min(m, v), Infinity)\n      : 0;\n    return {\n      label: extraRow.label,\n      values: values.map((value, hour) => ({\n        hour,\n        value,\n        level: calculateLevel(value, minVal, maxVal, levels, isNormalized),\n      })),\n    };\n  }, [extraRow, data, levels, isNormalized]);\n\n  const resolvedExtraColumn = useMemo<ResolvedDateExtraColumn | null>(() => {\n    if (!extraColumn) return null;\n    const values = extraColumn.compute(data, dates);\n    const maxVal = values.reduce((m, v) => Math.max(m, v), 1);\n    const minVal = isNormalized\n      ? values.reduce((m, v) => Math.min(m, v), Infinity)\n      : 0;\n    return {\n      label: extraColumn.label,\n      values: values.map((value, i) => ({\n        date: dates[i] ?? \"\",\n        value,\n        level: calculateLevel(value, minVal, maxVal, levels, isNormalized),\n      })),\n    };\n  }, [extraColumn, data, dates, levels, isNormalized]);\n\n  const totalCount =\n    typeof totalCountProp === \"number\"\n      ? totalCountProp\n      : dataWithLevels.reduce((sum, activity) => sum + activity.value, 0);\n\n  const hasExtraRow = resolvedExtraRow !== null;\n  const hasExtraColumn = resolvedExtraColumn !== null;\n\n  const blockWidth = blockSize * blockAspectRatio;\n  const labelHeight = fontSize + LABEL_MARGIN;\n  const width =\n    24 * (blockWidth + blockMargin) +\n    (hasExtraColumn\n      ? blockSize / 2 + (blockWidth + blockMargin) + blockWidth\n      : 0) -\n    blockMargin +\n    labelWidth;\n  const height =\n    dates.length * (blockSize + blockMargin) -\n    blockMargin +\n    labelHeight +\n    (hasExtraRow ? blockSize + blockMargin + (blockSize + blockMargin) : 0);\n\n  const contextValue = useMemo<DateHeatmapContextType>(\n    () => ({\n      data: dataWithLevels,\n      dates,\n      extraRow: resolvedExtraRow,\n      extraColumn: resolvedExtraColumn,\n      blockMargin,\n      blockRadius,\n      blockSize,\n      blockAspectRatio,\n      blockWidth,\n      fontSize,\n      labels,\n      labelWidth,\n      labelHeight,\n      levels,\n      isNormalized,\n      totalCount,\n      width,\n      height,\n      dateFormat,\n      locale,\n      colors,\n    }),\n    [\n      dataWithLevels,\n      dates,\n      resolvedExtraRow,\n      resolvedExtraColumn,\n      blockMargin,\n      blockRadius,\n      blockSize,\n      blockAspectRatio,\n      blockWidth,\n      fontSize,\n      labels,\n      labelWidth,\n      labelHeight,\n      levels,\n      isNormalized,\n      totalCount,\n      width,\n      height,\n      dateFormat,\n      locale,\n      colors,\n    ],\n  );\n\n  if (data.length === 0 || dates.length === 0) {\n    return emptyState ? emptyState : null;\n  }\n\n  return (\n    <DateHeatmapContext value={contextValue}>\n      <div\n        data-slot=\"date-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    </DateHeatmapContext>\n  );\n};\n\nexport type DateHeatmapBlockProps = HTMLAttributes<SVGRectElement> & {\n  activity: DateHourlyActivityWithLevel;\n  dateIndex: number;\n  extra?: \"row\" | \"column\";\n  highlighted?: boolean;\n  onCellClick?: (activity: DateHourlyActivityWithLevel) => void;\n  onCellHover?: (activity: DateHourlyActivityWithLevel | null) => void;\n  className?: string;\n};\n\nexport const DateHeatmapBlock = ({\n  ref,\n  activity,\n  dateIndex,\n  extra,\n  highlighted = false,\n  onCellClick,\n  onCellHover,\n  onClick,\n  onKeyDown,\n  onMouseEnter,\n  onMouseLeave,\n  className,\n  style: styleProp,\n  ...props\n}: DateHeatmapBlockProps & {\n  ref?: React.RefObject<SVGRectElement | null>;\n}) => {\n  const {\n    blockSize,\n    blockWidth,\n    blockMargin,\n    blockRadius,\n    labelWidth,\n    labelHeight,\n    labels,\n    levels,\n    isNormalized,\n    colors,\n  } = useDateHeatmap();\n\n  const level = Math.max(\n    0,\n    Math.min(colorStepCount(levels, isNormalized), activity.level),\n  );\n\n  const isExtraRow = extra === \"row\";\n  const isExtraColumn = extra === \"column\";\n\n  const extraRowGap = isExtraRow ? blockSize + blockMargin : 0;\n  const yPosition =\n    labelHeight + (blockSize + blockMargin) * dateIndex + extraRowGap;\n\n  const extraColumnGap = isExtraColumn ? blockWidth + blockMargin : 0;\n  const xPosition = isExtraColumn\n    ? labelWidth +\n      24 * (blockWidth + blockMargin) +\n      blockSize / 2 +\n      extraColumnGap\n    : labelWidth + (blockWidth + blockMargin) * activity.hour;\n\n  const dateToken = isExtraRow ? \"extra\" : activity.date;\n  const hourToken = isExtraColumn\n    ? \"extra\"\n    : `${String(activity.hour).padStart(2, \"0\")}:00`;\n  const ariaLabel = (labels.cellLabel ?? \"{{date}} {{hour}}: {{value}}\")\n    .replace(\"{{date}}\", dateToken)\n    .replace(\"{{hour}}\", hourToken)\n    .replace(\"{{value}}\", String(activity.value));\n\n  return (\n    <rect\n      ref={ref}\n      data-slot=\"date-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={isExtraRow ? \"extra\" : activity.date}\n      data-hour={isExtraColumn ? \"extra\" : activity.hour}\n      data-level={level}\n      data-highlighted={highlighted || undefined}\n      height={blockSize}\n      rx={blockRadius}\n      ry={blockRadius}\n      width={blockWidth}\n      x={xPosition}\n      y={yPosition}\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};\nDateHeatmapBlock.displayName = \"DateHeatmapBlock\";\n\nexport type DateHeatmapBodyProps = Omit<\n  HTMLAttributes<HTMLDivElement>,\n  \"children\"\n> & {\n  hideDateLabels?: boolean;\n  hideHourLabels?: boolean;\n  className?: string;\n  labelClassName?: string;\n  children: (props: {\n    activity: DateHourlyActivityWithLevel;\n    dateIndex: number;\n  }) => ReactNode;\n  renderExtraRow?: (props: {\n    activity: DateHourlyActivityWithLevel;\n    dateIndex: number;\n  }) => ReactNode;\n  renderExtraColumn?: (props: {\n    activity: DateHourlyActivityWithLevel;\n    dateIndex: number;\n  }) => ReactNode;\n};\n\nexport const DateHeatmapBody = ({\n  hideDateLabels = false,\n  hideHourLabels = false,\n  className,\n  labelClassName,\n  children,\n  renderExtraRow,\n  renderExtraColumn,\n  ...props\n}: DateHeatmapBodyProps) => {\n  const {\n    data,\n    dates,\n    extraRow,\n    extraColumn,\n    width,\n    height,\n    blockSize,\n    blockWidth,\n    blockMargin,\n    labels,\n    labelWidth,\n    labelHeight,\n    fontSize,\n    dateFormat,\n    locale,\n  } = useDateHeatmap();\n\n  const hasExtraRow = extraRow !== null;\n  const hasExtraColumn = extraColumn !== null;\n\n  const extraRowGap = hasExtraRow ? blockSize + blockMargin : 0;\n\n  const svgWidth = width;\n  const svgHeight = height;\n\n  const activityMap = useMemo(() => {\n    const map = new Map<string, DateHourlyActivityWithLevel>();\n    data.forEach((activity) => {\n      const key = `${activity.date}-${activity.hour}`;\n      map.set(key, activity);\n    });\n    return map;\n  }, [data]);\n\n  const extraColumnMap = useMemo(() => {\n    if (!extraColumn) return null;\n    const map = new Map<\n      string,\n      { date: string; value: number; level: number }\n    >();\n    extraColumn.values.forEach((v) => {\n      map.set(v.date, v);\n    });\n    return map;\n  }, [extraColumn]);\n\n  const regularActivities = useMemo(() => {\n    const activities: {\n      activity: DateHourlyActivityWithLevel;\n      dateIndex: number;\n    }[] = [];\n    dates.forEach((date, dateIndex) => {\n      for (let hour = 0; hour < 24; hour++) {\n        activities.push({\n          activity: activityMap.get(`${date}-${hour}`) ?? {\n            date,\n            hour,\n            value: 0,\n            level: 0,\n          },\n          dateIndex,\n        });\n      }\n    });\n    return activities;\n  }, [dates, activityMap]);\n\n  const extraColumnActivities = useMemo(() => {\n    if (!extraColumnMap) return [];\n    return dates\n      .map((date, dateIndex) => {\n        const entry = extraColumnMap.get(date);\n        return entry\n          ? {\n              activity: {\n                date,\n                hour: 0,\n                value: entry.value,\n                level: entry.level,\n              },\n              dateIndex,\n            }\n          : null;\n      })\n      .filter(\n        (\n          a,\n        ): a is { activity: DateHourlyActivityWithLevel; dateIndex: number } =>\n          a !== null,\n      );\n  }, [dates, extraColumnMap]);\n\n  const extraRowActivities = useMemo(() => {\n    if (!extraRow) return [];\n    return extraRow.values.map(({ hour, value, level }) => ({\n      activity: { date: dates[0] ?? \"\", hour, value, level },\n      dateIndex: dates.length,\n    }));\n  }, [extraRow, dates]);\n\n  return (\n    <div\n      data-slot=\"date-heatmap-body\"\n      className={cn(\"max-w-full overflow-x-auto overflow-y-hidden\", className)}\n      {...props}\n    >\n      <svg\n        role=\"img\"\n        aria-label={labels.heatmapLabel ?? \"Activity heatmap by date and hour\"}\n        className=\"block overflow-visible rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n        height={svgHeight + PADDING * 2}\n        viewBox={`${-PADDING} ${-PADDING} ${svgWidth + PADDING * 2} ${svgHeight + PADDING * 2}`}\n        width={svgWidth + PADDING * 2}\n      >\n        {!hideDateLabels && (\n          <g\n            className={cn(\n              \"fill-current font-medium font-mono text-sm\",\n              labelClassName,\n            )}\n          >\n            {dates.map((date, dateIndex) => {\n              const yPosition =\n                labelHeight +\n                (blockSize + blockMargin) * dateIndex +\n                blockSize / 2;\n\n              return (\n                <text\n                  key={`date-${date}`}\n                  x={labelWidth - 8}\n                  y={yPosition}\n                  dominantBaseline=\"middle\"\n                  textAnchor=\"end\"\n                  style={{ fontSize: `${fontSize * 0.75}px` }}\n                >\n                  {formatDateWithWeekday(date, dateFormat, locale)}\n                </text>\n              );\n            })}\n            {hasExtraRow && (\n              <text\n                key=\"date-extra\"\n                x={labelWidth - 8}\n                y={\n                  labelHeight +\n                  (blockSize + blockMargin) * dates.length +\n                  extraRowGap +\n                  blockSize / 2\n                }\n                dominantBaseline=\"middle\"\n                textAnchor=\"end\"\n                style={{ fontSize: `${fontSize * 0.75}px` }}\n              >\n                {extraRow?.label}\n              </text>\n            )}\n          </g>\n        )}\n\n        {!hideHourLabels && (\n          <g className={cn(\"fill-current font-mono\", labelClassName)}>\n            {HOURS.map((hour) => {\n              const label = labels.hours?.[hour] ?? \"\";\n              return (\n                <text\n                  key={`hour-${hour}`}\n                  x={labelWidth + (blockWidth + blockMargin) * hour}\n                  y={0}\n                  textAnchor=\"middle\"\n                  dominantBaseline=\"hanging\"\n                  style={{ fontSize: `${fontSize * 0.75}px` }}\n                >\n                  {label}\n                </text>\n              );\n            })}\n            {labels.endHour != null && (\n              <text\n                key=\"hour-end\"\n                x={labelWidth + (blockWidth + blockMargin) * 24}\n                y={0}\n                textAnchor=\"middle\"\n                dominantBaseline=\"hanging\"\n                style={{ fontSize: `${fontSize * 0.75}px` }}\n              >\n                {labels.endHour}\n              </text>\n            )}\n            {hasExtraColumn && (\n              <text\n                key=\"extra-col-label\"\n                x={\n                  labelWidth +\n                  24 * (blockWidth + blockMargin) +\n                  blockSize / 2 +\n                  (blockWidth + blockMargin) +\n                  blockWidth / 2\n                }\n                y={0}\n                textAnchor=\"middle\"\n                dominantBaseline=\"hanging\"\n                style={{ fontSize: `${fontSize * 0.75}px` }}\n              >\n                {extraColumn?.label}\n              </text>\n            )}\n          </g>\n        )}\n\n        {regularActivities.map(({ activity, dateIndex }) => (\n          <Fragment key={`${activity.date}-${activity.hour}`}>\n            {children({ activity, dateIndex })}\n          </Fragment>\n        ))}\n        {extraColumnActivities.map(({ activity, dateIndex }) => (\n          <Fragment key={`extra-col-${activity.date}`}>\n            {(renderExtraColumn ?? children)({ activity, dateIndex })}\n          </Fragment>\n        ))}\n        {extraRowActivities.map(({ activity, dateIndex }) => (\n          <Fragment key={`extra-row-${activity.hour}`}>\n            {(renderExtraRow ?? children)({ activity, dateIndex })}\n          </Fragment>\n        ))}\n      </svg>\n    </div>\n  );\n};\n\nexport type DateHeatmapFooterProps = HTMLAttributes<HTMLDivElement>;\n\nexport const DateHeatmapFooter = ({\n  className,\n  ...props\n}: DateHeatmapFooterProps) => (\n  <div\n    data-slot=\"date-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 DateHeatmapStatProps = Omit<\n  HTMLAttributes<HTMLDivElement>,\n  \"children\"\n> & {\n  compute?: (data: DateHourlyActivityWithLevel[]) => number | string;\n  label?: string; // Template overriding labels.stat. Placeholder: {{value}}\n  children?: (result: {\n    value: number | string;\n    data: DateHourlyActivityWithLevel[];\n  }) => ReactNode;\n};\n\nexport const DateHeatmapStat = ({\n  compute,\n  label,\n  className,\n  children,\n  ...props\n}: DateHeatmapStatProps) => {\n  const { data, totalCount, labels } = useDateHeatmap();\n\n  const value = compute ? compute(data) : totalCount;\n\n  if (children) {\n    return <>{children({ value, data })}</>;\n  }\n\n  const template = label ?? labels.stat ?? \"{{value}} contributions\";\n\n  return (\n    <div\n      data-slot=\"date-heatmap-stat\"\n      className={cn(\"text-muted-foreground tabular-nums\", className)}\n      {...props}\n    >\n      {template.replace(\"{{value}}\", String(value))}\n    </div>\n  );\n};\n\nexport type DateHeatmapLegendProps = Omit<\n  HTMLAttributes<HTMLFieldSetElement>,\n  \"children\"\n> & {\n  labels?: { less?: string; more?: string };\n  children?: (props: { level: number }) => ReactNode;\n};\n\nexport const DateHeatmapLegend = ({\n  labels: labelsProp,\n  className,\n  children,\n  ...props\n}: DateHeatmapLegendProps) => {\n  const {\n    levels,\n    isNormalized,\n    blockSize,\n    blockWidth,\n    blockRadius,\n    colors,\n    labels,\n  } = useDateHeatmap();\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=\"date-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)"
    }
  }
}
