{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "activity-heatmap",
  "title": "Activity Heatmap",
  "description": "Calendar activity matrix showing contribution intensity over a year with keyboard navigation.",
  "dependencies": [],
  "registryDependencies": [
    "utils",
    "@drift-ui/use-controllable-state"
  ],
  "files": [
    {
      "path": "registry/default/ui/activity-heatmap.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { useControllableState } from \"@/registry/default/hooks/use-controllable-state\"\nimport { cn } from \"@/registry/default/lib/utils\"\nimport {\n  buildHeatmapLayout,\n  calculateTooltipPosition,\n  formatDateLabel,\n  stepHeatmapCoordinate,\n  type ActivityDay,\n  type ActivityDayInput,\n  type HeatmapCell,\n} from \"@/registry/default/lib/activity-heatmap.ts\"\n\nexport type { ActivityDay, ActivityDayInput, HeatmapCell }\n\nexport interface ActivityHeatmapProps extends Omit<\n  React.ComponentProps<\"div\">,\n  \"defaultValue\" | \"value\" | \"onChange\"\n> {\n  data: ActivityDayInput[]\n  weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6\n  startDate?: string\n  endDate?: string\n  selectedDate?: string | null\n  defaultSelectedDate?: string | null\n  onDaySelect?: (day: ActivityDay) => void\n  thresholds?: number[]\n  unit?: string\n  showSummary?: boolean\n  summary?:\n    | React.ReactNode\n    | ((total: number, startDate: string, endDate: string) => React.ReactNode)\n  showTooltip?: boolean\n  renderTooltip?: (day: ActivityDay) => React.ReactNode\n  showLegend?: boolean\n  showMonthLabels?: boolean\n  showWeekdayLabels?: boolean\n  levelColors?: Record<number, string>\n  \"aria-label\"?: string\n}\n\nconst DEFAULT_LEVEL_CLASSES: Record<number, string> = {\n  0: \"bg-muted/40 border border-border/30\",\n  1: \"bg-primary/20\",\n  2: \"bg-primary/45\",\n  3: \"bg-primary/70\",\n  4: \"bg-primary\",\n}\n\nexport function ActivityHeatmap({\n  data = [],\n  weekStartsOn = 0,\n  startDate,\n  endDate,\n  selectedDate: selectedDateProp,\n  defaultSelectedDate,\n  onDaySelect,\n  thresholds,\n  unit = \"contributions\",\n  showSummary = true,\n  summary,\n  showLegend = true,\n  showMonthLabels = true,\n  showWeekdayLabels = true,\n  levelColors,\n  showTooltip = true,\n  renderTooltip,\n  \"aria-label\": ariaLabel = \"Activity heatmap\",\n  className,\n  ...props\n}: ActivityHeatmapProps) {\n  const {\n    ref: externalRef,\n    onKeyDown: externalOnKeyDown,\n    ...restDivProps\n  } = props as React.ComponentProps<\"div\"> & { ref?: React.Ref<HTMLDivElement> }\n\n  const { value: selectedDate, setValue: setSelectedDate } =\n    useControllableState<string | null>({\n      value: selectedDateProp,\n      defaultValue: defaultSelectedDate ?? null,\n    })\n\n  const layout = React.useMemo(() => {\n    return buildHeatmapLayout({\n      data,\n      startDate,\n      endDate,\n      weekStartsOn,\n      thresholds,\n      unit,\n    })\n  }, [data, startDate, endDate, weekStartsOn, thresholds, unit])\n\n  const [internalFocusedCoord, setInternalFocusedCoord] = React.useState<\n    [number, number] | null\n  >(null)\n\n  const selectedCoord = selectedDate\n    ? (layout.dateToCoord[selectedDate] ?? null)\n    : null\n  const focusedCoord =\n    internalFocusedCoord ?? selectedCoord ?? layout.defaultCoord\n\n  const tooltipId = React.useId()\n  const containerRef = React.useRef<HTMLDivElement | null>(null)\n  React.useImperativeHandle(\n    externalRef,\n    () => containerRef.current as HTMLDivElement\n  )\n\n  const scrollContainerRef = React.useRef<HTMLDivElement>(null)\n  const tooltipRef = React.useRef<HTMLDivElement>(null)\n\n  const [hoveredState, setHoveredState] = React.useState<{\n    date: string\n    element: HTMLTableCellElement\n  } | null>(null)\n\n  const [focusedState, setFocusedState] = React.useState<{\n    date: string\n    element: HTMLTableCellElement\n  } | null>(null)\n\n  const [dismissedDate, setDismissedDate] = React.useState<string | null>(null)\n\n  const activeState = hoveredState ?? focusedState\n\n  const activeCell = React.useMemo(() => {\n    if (!activeState) return null\n    const cell =\n      layout.weeks\n        .flat()\n        .find((c) => c.date === activeState.date && c.inRange) ?? null\n    if (!cell) return null\n    return { cell, element: activeState.element }\n  }, [layout, activeState])\n\n  const isTooltipActive = Boolean(\n    showTooltip && activeCell && dismissedDate !== activeCell.cell.date\n  )\n\n  const pointerRef = React.useRef<{ x: number; y: number } | null>(null)\n\n  const updateTooltipPosition = React.useCallback(() => {\n    const tooltipEl = tooltipRef.current\n    const containerEl = containerRef.current\n    if (!activeCell || !tooltipEl || !containerEl) return\n\n    const pointer = pointerRef.current\n    const pos = calculateTooltipPosition({\n      cellRect: pointer\n        ? { left: pointer.x, top: pointer.y, width: 0, height: 16 }\n        : activeCell.element.getBoundingClientRect(),\n      containerRect: containerEl.getBoundingClientRect(),\n      scrollRect: scrollContainerRef.current?.getBoundingClientRect(),\n      tooltipWidth: tooltipEl.offsetWidth || 140,\n      tooltipHeight: tooltipEl.offsetHeight || 44,\n      margin: 8,\n    })\n\n    Object.assign(tooltipEl.style, {\n      top: `${pos.top}px`,\n      left: `${pos.left}px`,\n      transform:\n        pos.placement === \"bottom\"\n          ? \"translate(-50%, 0)\"\n          : \"translate(-50%, -100%)\",\n      opacity: pos.visible ? \"1\" : \"0\",\n    })\n  }, [activeCell])\n\n  React.useLayoutEffect(() => {\n    if (isTooltipActive) updateTooltipPosition()\n  }, [isTooltipActive, updateTooltipPosition])\n\n  React.useEffect(() => {\n    if (!isTooltipActive) return\n    const handleResize = () => updateTooltipPosition()\n    window.addEventListener(\"resize\", handleResize)\n    return () => window.removeEventListener(\"resize\", handleResize)\n  }, [isTooltipActive, updateTooltipPosition])\n\n  const cellRefs = React.useRef<Map<string, HTMLTableCellElement>>(new Map())\n\n  const handleSelect = React.useCallback(\n    (cell: HeatmapCell) => {\n      if (!cell.inRange) return\n      setSelectedDate(cell.date)\n      setInternalFocusedCoord([cell.dayIndex, cell.weekIndex])\n      onDaySelect?.({\n        date: cell.date,\n        count: cell.count,\n        level: cell.level,\n      })\n    },\n    [setSelectedDate, onDaySelect]\n  )\n\n  const handleCellKeyDown = React.useCallback(\n    (\n      e: React.KeyboardEvent<HTMLTableCellElement>,\n      row: number,\n      col: number\n    ) => {\n      const currentCell = layout.rows[row]?.[col]\n      if (e.key === \"Escape\") {\n        e.preventDefault()\n        if (activeCell) {\n          setDismissedDate(activeCell.cell.date)\n        }\n        return\n      }\n\n      if (e.key === \"Enter\" || e.key === \" \" || e.key === \"Spacebar\") {\n        e.preventDefault()\n        if (currentCell && currentCell.inRange) {\n          handleSelect(currentCell)\n        }\n        return\n      }\n\n      if (\n        [\n          \"ArrowUp\",\n          \"ArrowDown\",\n          \"ArrowLeft\",\n          \"ArrowRight\",\n          \"Home\",\n          \"End\",\n          \"PageUp\",\n          \"PageDown\",\n        ].includes(e.key)\n      ) {\n        e.preventDefault()\n        const [nextRow, nextCol] = stepHeatmapCoordinate(\n          [row, col],\n          e.key,\n          layout\n        )\n        setInternalFocusedCoord([nextRow, nextCol])\n        const key = `${nextRow}-${nextCol}`\n        const cellEl = cellRefs.current.get(key)\n        cellEl?.focus()\n      }\n    },\n    [layout, handleSelect, activeCell]\n  )\n\n  const handleContainerKeyDown = React.useCallback(\n    (e: React.KeyboardEvent<HTMLDivElement>) => {\n      externalOnKeyDown?.(e)\n      if (e.defaultPrevented) return\n\n      if (e.key === \"Escape\" && activeCell) {\n        e.preventDefault()\n        setDismissedDate(activeCell.cell.date)\n      }\n    },\n    [activeCell, externalOnKeyDown]\n  )\n\n  // Map month labels by week index\n  const monthByWeek = React.useMemo(() => {\n    const map = new Map<number, string>()\n    for (const m of layout.monthLabels) {\n      map.set(m.weekIndex, m.monthName)\n    }\n    return map\n  }, [layout.monthLabels])\n\n  // Compute selected cell if any\n  const selectedCell = React.useMemo(() => {\n    if (!selectedDate) return null\n    return (\n      layout.weeks.flat().find((c) => c.date === selectedDate && c.inRange) ??\n      null\n    )\n  }, [layout, selectedDate])\n\n  const summaryText = React.useMemo(() => {\n    if (!showSummary) return null\n    if (typeof summary === \"function\") {\n      return summary(layout.totalCount, layout.startDate, layout.endDate)\n    }\n    if (summary !== undefined) {\n      return summary\n    }\n    return `${layout.totalCount.toLocaleString()} ${unit} in the last year`\n  }, [showSummary, summary, layout, unit])\n\n  return (\n    <div\n      ref={containerRef}\n      data-slot=\"activity-heatmap\"\n      className={cn(\"relative flex flex-col gap-2.5\", className)}\n      onKeyDown={handleContainerKeyDown}\n      {...restDivProps}\n    >\n      {/* Header Summary */}\n      {(showSummary || selectedCell) && (\n        <div className=\"flex items-center justify-between gap-4 text-sm\">\n          {summaryText && (\n            <span className=\"font-medium text-foreground\">{summaryText}</span>\n          )}\n          {selectedCell && (\n            <span className=\"ml-auto text-xs text-muted-foreground\">\n              {selectedCell.count}{\" \"}\n              {selectedCell.count === 1\n                ? unit.endsWith(\"s\")\n                  ? unit.slice(0, -1)\n                  : unit\n                : unit}{\" \"}\n              on {formatDateLabel(selectedCell.date, true)}\n            </span>\n          )}\n        </div>\n      )}\n\n      {/* Heatmap Grid */}\n      <div\n        ref={scrollContainerRef}\n        className=\"w-full overflow-x-auto pb-1\"\n        onScroll={updateTooltipPosition}\n      >\n        <table\n          role=\"grid\"\n          aria-label={ariaLabel}\n          aria-rowcount={7}\n          aria-colcount={layout.weeks.length + (showWeekdayLabels ? 1 : 0)}\n          className=\"border-separate border-spacing-[3px] text-left select-none\"\n          onMouseMove={(e) => {\n            pointerRef.current = { x: e.clientX, y: e.clientY }\n            updateTooltipPosition()\n          }}\n          onMouseLeave={() => {\n            pointerRef.current = null\n            setHoveredState(null)\n          }}\n        >\n          {showMonthLabels && (\n            <thead>\n              <tr role=\"row\" className=\"text-[10px] text-muted-foreground\">\n                {showWeekdayLabels && (\n                  <th\n                    role=\"presentation\"\n                    aria-hidden=\"true\"\n                    className=\"w-7 min-w-7 font-normal\"\n                  />\n                )}\n                {layout.weeks.map((_, weekIdx) => {\n                  const month = monthByWeek.get(weekIdx)\n                  return (\n                    <th\n                      key={weekIdx}\n                      role=\"columnheader\"\n                      aria-colindex={weekIdx + (showWeekdayLabels ? 2 : 1)}\n                      aria-hidden={month ? undefined : true}\n                      className=\"relative h-4 min-w-[12px] p-0 leading-none font-normal\"\n                    >\n                      {month && (\n                        <span className=\"absolute top-0 left-0 whitespace-nowrap\">\n                          {month}\n                        </span>\n                      )}\n                    </th>\n                  )\n                })}\n              </tr>\n            </thead>\n          )}\n          <tbody>\n            {layout.rows.map((rowCells, rowIdx) => {\n              const weekday = layout.weekdayLabels[rowIdx]\n              // Show label on alternating days (e.g. Mon, Wed, Fri) to avoid vertical clutter\n              const showThisLabel = rowIdx % 2 === 1\n\n              return (\n                <tr\n                  key={rowIdx}\n                  role=\"row\"\n                  aria-rowindex={rowIdx + 1}\n                  className=\"h-3\"\n                >\n                  {showWeekdayLabels && (\n                    <th\n                      role=\"rowheader\"\n                      aria-colindex={1}\n                      className=\"w-7 min-w-7 pr-1.5 text-right text-[10px] leading-none font-normal text-muted-foreground\"\n                    >\n                      {showThisLabel ? (\n                        weekday\n                      ) : (\n                        <span className=\"sr-only\">{weekday}</span>\n                      )}\n                    </th>\n                  )}\n                  {rowCells.map((cell, colIdx) => {\n                    if (!cell.inRange) {\n                      return (\n                        <td\n                          key={cell.date + colIdx}\n                          role=\"presentation\"\n                          aria-hidden=\"true\"\n                          tabIndex={-1}\n                          className=\"pointer-events-none size-3 min-w-[12px] rounded-[2px] opacity-0\"\n                        />\n                      )\n                    }\n\n                    const isCellActive = activeCell?.cell.date === cell.date\n                    const isFocused =\n                      focusedCoord[0] === rowIdx && focusedCoord[1] === colIdx\n                    const isSelected = selectedDate === cell.date\n                    const colorClass =\n                      levelColors?.[cell.level] ??\n                      DEFAULT_LEVEL_CLASSES[cell.level] ??\n                      DEFAULT_LEVEL_CLASSES[0]\n\n                    return (\n                      <td\n                        key={cell.date}\n                        ref={(el) => {\n                          const key = `${rowIdx}-${colIdx}`\n                          if (el) {\n                            cellRefs.current.set(key, el)\n                          } else {\n                            cellRefs.current.delete(key)\n                          }\n                        }}\n                        role=\"gridcell\"\n                        aria-colindex={colIdx + (showWeekdayLabels ? 2 : 1)}\n                        aria-selected={isSelected}\n                        aria-label={cell.label}\n                        aria-describedby={\n                          isTooltipActive && isCellActive\n                            ? tooltipId\n                            : undefined\n                        }\n                        tabIndex={isFocused ? 0 : -1}\n                        onClick={() => handleSelect(cell)}\n                        onKeyDown={(e) => handleCellKeyDown(e, rowIdx, colIdx)}\n                        onMouseEnter={(e) => {\n                          pointerRef.current = { x: e.clientX, y: e.clientY }\n                          setHoveredState({\n                            date: cell.date,\n                            element: e.currentTarget,\n                          })\n                          setDismissedDate(null)\n                        }}\n                        onFocus={(e) => {\n                          setFocusedState({\n                            date: cell.date,\n                            element: e.currentTarget,\n                          })\n                          setInternalFocusedCoord([rowIdx, colIdx])\n                          setDismissedDate(null)\n                        }}\n                        onBlur={() => {\n                          setFocusedState((prev) =>\n                            prev?.date === cell.date ? null : prev\n                          )\n                        }}\n                        className={cn(\n                          \"size-3 min-w-[12px] cursor-pointer rounded-[2px] outline-hidden transition-colors\",\n                          colorClass,\n                          \"hover:ring-1 hover:ring-ring/60\",\n                          isSelected &&\n                            \"ring-2 ring-ring ring-offset-1 ring-offset-background\",\n                          isFocused &&\n                            \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background\"\n                        )}\n                      />\n                    )\n                  })}\n                </tr>\n              )\n            })}\n          </tbody>\n        </table>\n      </div>\n\n      {/* Legend & Range */}\n      {showLegend && (\n        <div className=\"flex flex-wrap items-center justify-between gap-3 pt-1 text-xs text-muted-foreground\">\n          <span className=\"text-[11px]\">\n            {formatDateLabel(layout.startDate, false)} &ndash;{\" \"}\n            {formatDateLabel(layout.endDate, true)}\n          </span>\n          <div\n            role=\"group\"\n            className=\"flex items-center gap-1.5\"\n            aria-label=\"Activity intensity scale\"\n          >\n            <span className=\"text-[11px]\">Less</span>\n            {[0, 1, 2, 3, 4].map((lvl) => (\n              <span\n                key={lvl}\n                role=\"img\"\n                aria-label={`Level ${lvl}`}\n                className={cn(\n                  \"size-3 rounded-[2px]\",\n                  levelColors?.[lvl] ?? DEFAULT_LEVEL_CLASSES[lvl]\n                )}\n              />\n            ))}\n            <span className=\"text-[11px]\">More</span>\n          </div>\n        </div>\n      )}\n\n      {/* Floating Tooltip */}\n      {isTooltipActive && activeCell && (\n        <div\n          ref={tooltipRef}\n          id={tooltipId}\n          role=\"tooltip\"\n          data-slot=\"activity-heatmap-tooltip\"\n          className={cn(\n            \"pointer-events-none absolute z-50 rounded-md border border-border bg-popover px-2.5 py-1.5 text-xs text-popover-foreground opacity-0 shadow-md\"\n          )}\n        >\n          {renderTooltip ? (\n            renderTooltip(activeCell.cell)\n          ) : (\n            <div className=\"flex flex-col gap-0.5 text-center\">\n              <span className=\"font-medium text-popover-foreground\">\n                {activeCell.cell.count === 0\n                  ? `No ${unit}`\n                  : `${activeCell.cell.count.toLocaleString()} ${\n                      activeCell.cell.count === 1\n                        ? unit.endsWith(\"s\")\n                          ? unit.slice(0, -1)\n                          : unit\n                        : unit\n                    }`}\n              </span>\n              <span className=\"text-[10px] text-muted-foreground\">\n                {formatDateLabel(activeCell.cell.date, true)}\n              </span>\n            </div>\n          )}\n        </div>\n      )}\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/default/lib/activity-heatmap.ts",
      "content": "export interface ActivityDay {\n  date: string\n  count: number\n  level: number\n}\n\nexport interface ActivityDayInput {\n  date: string\n  count: number\n  level?: number\n}\n\nexport interface HeatmapCell {\n  date: string\n  count: number\n  level: number\n  weekIndex: number\n  dayIndex: number // 0..6 relative to weekStartsOn\n  dayOfWeek: number // 0..6 (0 = Sunday, 1 = Monday, ..., 6 = Saturday)\n  inRange: boolean\n  label: string\n}\n\nexport interface HeatmapMonthLabel {\n  monthName: string\n  weekIndex: number\n}\n\nexport interface HeatmapLayout {\n  weeks: HeatmapCell[][]\n  rows: HeatmapCell[][]\n  monthLabels: HeatmapMonthLabel[]\n  weekdayLabels: string[]\n  totalCount: number\n  totalDays: number\n  startDate: string\n  endDate: string\n  maxCount: number\n  defaultCoord: [number, number]\n  dateToCoord: Record<string, [number, number]>\n}\n\nexport interface BuildHeatmapOptions {\n  data: ActivityDayInput[]\n  startDate?: string\n  endDate?: string\n  weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6\n  thresholds?: number[]\n  unit?: string\n}\n\nconst MONTH_NAMES_SHORT = [\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 MONTH_NAMES_FULL = [\n  \"January\",\n  \"February\",\n  \"March\",\n  \"April\",\n  \"May\",\n  \"June\",\n  \"July\",\n  \"August\",\n  \"September\",\n  \"October\",\n  \"November\",\n  \"December\",\n]\n\nconst WEEKDAY_NAMES_SHORT = [\"Sun\", \"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\"]\n\n/**\n * Parses a \"YYYY-MM-DD\" calendar date in UTC to avoid time-zone shifts.\n */\nexport function parseUTCDate(dateStr: string): Date {\n  const match = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(dateStr.trim())\n  if (!match) {\n    const fallback = new Date(dateStr)\n    if (isNaN(fallback.getTime())) {\n      return new Date(Date.UTC(2000, 0, 1))\n    }\n    return new Date(\n      Date.UTC(\n        fallback.getUTCFullYear(),\n        fallback.getUTCMonth(),\n        fallback.getUTCDate()\n      )\n    )\n  }\n  const year = parseInt(match[1], 10)\n  const month = parseInt(match[2], 10) - 1\n  const day = parseInt(match[3], 10)\n  return new Date(Date.UTC(year, month, day))\n}\n\n/**\n * Formats a Date object as a \"YYYY-MM-DD\" string in UTC.\n */\nexport function formatUTCDate(date: Date): string {\n  const y = date.getUTCFullYear()\n  const m = String(date.getUTCMonth() + 1).padStart(2, \"0\")\n  const d = String(date.getUTCDate()).padStart(2, \"0\")\n  return `${y}-${m}-${d}`\n}\n\n/**\n * Adds integer days to a Date in UTC.\n */\nexport function addUTCDays(date: Date, days: number): Date {\n  const result = new Date(date.getTime())\n  result.setUTCDate(result.getUTCDate() + days)\n  return result\n}\n\n/**\n * Returns UTC day of week (0 = Sunday, 1 = Monday, ..., 6 = Saturday).\n */\nexport function getUTCDayOfWeek(date: Date): number {\n  return date.getUTCDay()\n}\n\n/**\n * Formats a human-readable date label in UTC (e.g., \"October 31\" or \"October 31, 2026\").\n */\nexport function formatDateLabel(\n  dateInput: string | Date,\n  includeYear = true\n): string {\n  const d = typeof dateInput === \"string\" ? parseUTCDate(dateInput) : dateInput\n  const month = MONTH_NAMES_FULL[d.getUTCMonth()]\n  const day = d.getUTCDate()\n  if (includeYear) {\n    return `${month} ${day}, ${d.getUTCFullYear()}`\n  }\n  return `${month} ${day}`\n}\n\n/**\n * Formats an accessible cell label such as \"7 contributions on October 31\" or \"No contributions on October 31\".\n */\nexport function formatCellAccessibleLabel(\n  count: number,\n  dateStr: string,\n  unit = \"contributions\"\n): string {\n  const formattedDate = formatDateLabel(dateStr, false)\n  if (count <= 0) {\n    return `No ${unit} on ${formattedDate}`\n  }\n  const singularUnit = unit.endsWith(\"s\") ? unit.slice(0, -1) : unit\n  const unitLabel = count === 1 ? singularUnit : unit\n  return `${count} ${unitLabel} on ${formattedDate}`\n}\n\n/**\n * Maps an activity count to an intensity level from 0 to 4.\n */\nexport function calculateIntensityLevel(\n  count: number,\n  maxCount: number,\n  thresholds?: number[]\n): number {\n  if (!Number.isFinite(count) || count <= 0) return 0\n\n  if (thresholds && thresholds.length === 4) {\n    if (count < thresholds[0]) return 0\n    if (count < thresholds[1]) return 1\n    if (count < thresholds[2]) return 2\n    if (count < thresholds[3]) return 3\n    return 4\n  }\n\n  if (!Number.isFinite(maxCount) || maxCount <= 0) {\n    return count > 0 ? 1 : 0\n  }\n\n  const ratio = count / maxCount\n  if (ratio <= 0.25) return 1\n  if (ratio <= 0.5) return 2\n  if (ratio <= 0.75) return 3\n  return 4\n}\n\n/**\n * Returns the weekday names ordered according to weekStartsOn (0 = Sun, 1 = Mon, etc.).\n */\nexport function getWeekdayLabels(\n  weekStartsOn: 0 | 1 | 2 | 3 | 4 | 5 | 6 = 0\n): string[] {\n  const labels: string[] = []\n  for (let i = 0; i < 7; i++) {\n    const day = (weekStartsOn + i) % 7\n    labels.push(WEEKDAY_NAMES_SHORT[day])\n  }\n  return labels\n}\n\n/**\n * Builds the full calendar heatmap layout: week columns, weekday rows, month labels, and metrics.\n */\nexport function buildHeatmapLayout(\n  options: BuildHeatmapOptions\n): HeatmapLayout {\n  const {\n    data = [],\n    startDate: startDateProp,\n    endDate: endDateProp,\n    weekStartsOn = 0,\n    thresholds,\n    unit = \"contributions\",\n  } = options\n\n  // Aggregate duplicate dates and find data bounds\n  const dataMap = new Map<string, { count: number; level?: number }>()\n  let maxDataCount = 0\n  let earliestDataDate: string | null = null\n  let latestDataDate: string | null = null\n\n  for (const item of data) {\n    if (!item?.date) continue\n    const dateStr = item.date.trim()\n    const sanitizedCount =\n      Number.isFinite(item.count) && item.count > 0 ? item.count : 0\n    const existing = dataMap.get(dateStr)\n\n    if (existing) {\n      existing.count += sanitizedCount\n      if (item.level !== undefined) existing.level = item.level\n    } else {\n      dataMap.set(dateStr, {\n        count: sanitizedCount,\n        level: item.level,\n      })\n    }\n\n    const currentTotal = dataMap.get(dateStr)!.count\n    if (currentTotal > maxDataCount) {\n      maxDataCount = currentTotal\n    }\n\n    if (!earliestDataDate || dateStr < earliestDataDate) {\n      earliestDataDate = dateStr\n    }\n    if (!latestDataDate || dateStr > latestDataDate) {\n      latestDataDate = dateStr\n    }\n  }\n\n  // Determine end date\n  let effectiveEnd: Date\n  if (endDateProp) {\n    effectiveEnd = parseUTCDate(endDateProp)\n  } else if (latestDataDate) {\n    effectiveEnd = parseUTCDate(latestDataDate)\n  } else {\n    const now = new Date()\n    effectiveEnd = new Date(\n      Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())\n    )\n  }\n\n  // Determine start date (default to 364 days before end date -> 52 full weeks)\n  let effectiveStart: Date\n  if (startDateProp) {\n    effectiveStart = parseUTCDate(startDateProp)\n  } else {\n    effectiveStart = addUTCDays(effectiveEnd, -364)\n  }\n\n  if (effectiveStart > effectiveEnd) {\n    const temp = effectiveStart\n    effectiveStart = effectiveEnd\n    effectiveEnd = temp\n  }\n\n  const startDateStr = formatUTCDate(effectiveStart)\n  const endDateStr = formatUTCDate(effectiveEnd)\n\n  // Align start to the weekStartsOn boundary\n  const startDayOfWeek = getUTCDayOfWeek(effectiveStart)\n  const daysBeforeStart = (((startDayOfWeek - weekStartsOn) % 7) + 7) % 7\n  const calendarStart = addUTCDays(effectiveStart, -daysBeforeStart)\n\n  // Align end to the week boundary end\n  const endDayOfWeek = getUTCDayOfWeek(effectiveEnd)\n  const daysAfterEnd = (6 - ((((endDayOfWeek - weekStartsOn) % 7) + 7) % 7)) % 7\n  const calendarEnd = addUTCDays(effectiveEnd, daysAfterEnd)\n\n  // Calculate total days on calendar grid\n  const totalCalendarDays =\n    Math.round(\n      (calendarEnd.getTime() - calendarStart.getTime()) / (1000 * 60 * 60 * 24)\n    ) + 1\n  const weekCount = Math.ceil(totalCalendarDays / 7)\n\n  // Calculate maxCount for in-range days only\n  let inRangeMaxCount = 0\n  let totalCount = 0\n  let totalDays = 0\n\n  for (let i = 0; i < totalCalendarDays; i++) {\n    const current = addUTCDays(calendarStart, i)\n    const dateStr = formatUTCDate(current)\n    const inRange = dateStr >= startDateStr && dateStr <= endDateStr\n    if (inRange) {\n      totalDays++\n      const dayData = dataMap.get(dateStr)\n      const count = dayData?.count ?? 0\n      totalCount += count\n      if (count > inRangeMaxCount) {\n        inRangeMaxCount = count\n      }\n    }\n  }\n\n  const effectiveMaxCount =\n    maxDataCount > 0 ? maxDataCount : inRangeMaxCount > 0 ? inRangeMaxCount : 0\n\n  // Construct weeks and rows\n  const weeks: HeatmapCell[][] = []\n  const rows: HeatmapCell[][] = Array.from({ length: 7 }, () => [])\n  const dateToCoord: Record<string, [number, number]> = {}\n  let defaultCoord: [number, number] = [0, 0]\n\n  for (let w = 0; w < weekCount; w++) {\n    const weekCells: HeatmapCell[] = []\n    for (let d = 0; d < 7; d++) {\n      const dayOffset = w * 7 + d\n      const current = addUTCDays(calendarStart, dayOffset)\n      const dateStr = formatUTCDate(current)\n      const inRange = dateStr >= startDateStr && dateStr <= endDateStr\n      const dayData = inRange ? dataMap.get(dateStr) : undefined\n      const count = dayData?.count ?? 0\n      const level = !inRange\n        ? 0\n        : dayData?.level !== undefined\n          ? Math.max(0, Math.min(4, Math.floor(dayData.level)))\n          : calculateIntensityLevel(count, effectiveMaxCount, thresholds)\n\n      const label = inRange\n        ? formatCellAccessibleLabel(count, dateStr, unit)\n        : \"\"\n\n      const cell: HeatmapCell = {\n        date: dateStr,\n        count,\n        level,\n        weekIndex: w,\n        dayIndex: d,\n        dayOfWeek: getUTCDayOfWeek(current),\n        inRange,\n        label,\n      }\n\n      if (inRange) {\n        dateToCoord[dateStr] = [d, w]\n        defaultCoord = [d, w]\n      }\n\n      weekCells.push(cell)\n      rows[d].push(cell)\n    }\n    weeks.push(weekCells)\n  }\n\n  // Construct month labels\n  const monthLabels: HeatmapMonthLabel[] = []\n  let lastLabeledMonth = -1\n  let lastLabeledWeek = -99\n\n  for (let w = 0; w < weeks.length; w++) {\n    // Find the first in-range day in this week, or the first day\n    const firstDay = weeks[w].find((c) => c.inRange) ?? weeks[w][0]\n    const d = parseUTCDate(firstDay.date)\n    const month = d.getUTCMonth()\n\n    if (month !== lastLabeledMonth && w - lastLabeledWeek >= 2) {\n      monthLabels.push({\n        monthName: MONTH_NAMES_SHORT[month],\n        weekIndex: w,\n      })\n      lastLabeledMonth = month\n      lastLabeledWeek = w\n    }\n  }\n\n  const weekdayLabels = getWeekdayLabels(weekStartsOn)\n\n  return {\n    weeks,\n    rows,\n    monthLabels,\n    weekdayLabels,\n    totalCount,\n    totalDays,\n    startDate: startDateStr,\n    endDate: endDateStr,\n    maxCount: effectiveMaxCount,\n    defaultCoord,\n    dateToCoord,\n  }\n}\n\n/**\n * 2D navigation helper that advances coordinates while ensuring the target lands on an in-range cell.\n */\nexport function stepHeatmapCoordinate(\n  current: [number, number],\n  key: string,\n  layout: HeatmapLayout\n): [number, number] {\n  const [currentRow, currentCol] = current\n  const rowCount = 7\n  const colCount = layout.weeks.length\n\n  let targetRow = currentRow\n  let targetCol = currentCol\n\n  switch (key) {\n    case \"ArrowUp\":\n      targetRow = Math.max(0, currentRow - 1)\n      break\n    case \"ArrowDown\":\n      targetRow = Math.min(rowCount - 1, currentRow + 1)\n      break\n    case \"ArrowLeft\":\n      targetCol = Math.max(0, currentCol - 1)\n      break\n    case \"ArrowRight\":\n      targetCol = Math.min(colCount - 1, currentCol + 1)\n      break\n    case \"Home\":\n      targetCol = 0\n      break\n    case \"End\":\n      targetCol = colCount - 1\n      break\n    case \"PageUp\":\n      targetRow = 0\n      break\n    case \"PageDown\":\n      targetRow = rowCount - 1\n      break\n    default:\n      return current\n  }\n\n  // If the target cell is inRange, return it\n  if (layout.rows[targetRow]?.[targetCol]?.inRange) {\n    return [targetRow, targetCol]\n  }\n\n  // If target cell is not in range, search along the column or row for the closest inRange cell\n  // First search same column:\n  for (let r = 0; r < rowCount; r++) {\n    if (layout.rows[r]?.[targetCol]?.inRange) {\n      return [r, targetCol]\n    }\n  }\n\n  // If entire column is out of range, search adjacent columns\n  const colStep = targetCol > currentCol ? 1 : -1\n  for (let c = targetCol; c >= 0 && c < colCount; c += colStep) {\n    if (layout.rows[targetRow]?.[c]?.inRange) {\n      return [targetRow, c]\n    }\n  }\n\n  // Fallback: stay on current\n  return current\n}\n\nexport interface TooltipPositionOptions {\n  cellRect: {\n    top: number\n    left: number\n    right?: number\n    bottom?: number\n    width: number\n    height: number\n  }\n  containerRect: { top: number; left: number; width: number; height: number }\n  scrollRect?: { left: number; right: number; width: number }\n  tooltipWidth?: number\n  tooltipHeight?: number\n  margin?: number\n}\n\nexport interface TooltipPositionResult {\n  top: number\n  left: number\n  placement: \"top\" | \"bottom\"\n  visible: boolean\n}\n\n/**\n * Calculates absolute coordinates and placement for a floating tooltip relative to\n * the heatmap root container, clamping horizontally and flipping vertically near edges.\n */\nexport function calculateTooltipPosition({\n  cellRect,\n  containerRect,\n  scrollRect,\n  tooltipWidth = 140,\n  tooltipHeight = 44,\n  margin = 8,\n}: TooltipPositionOptions): TooltipPositionResult {\n  const cellRight = cellRect.right ?? cellRect.left + cellRect.width\n  const cellBottom = cellRect.bottom ?? cellRect.top + cellRect.height\n\n  if (scrollRect && scrollRect.width > 0) {\n    if (\n      cellRight < scrollRect.left + 2 ||\n      cellRect.left > scrollRect.right - 2\n    ) {\n      return { top: 0, left: 0, placement: \"top\", visible: false }\n    }\n  }\n\n  const halfWidth = tooltipWidth / 2\n  const rawCenterX = cellRect.left - containerRect.left + cellRect.width / 2\n\n  let clampedLeft = rawCenterX\n  if (containerRect.width >= tooltipWidth + margin * 2) {\n    clampedLeft = Math.max(\n      halfWidth + margin,\n      Math.min(containerRect.width - halfWidth - margin, rawCenterX)\n    )\n  }\n\n  const containerHeight =\n    containerRect.height > 0 ? containerRect.height : Infinity\n  const spaceAbove = cellRect.top - containerRect.top\n  const spaceBelow = containerRect.top + containerHeight - cellBottom\n\n  const placement: \"top\" | \"bottom\" =\n    spaceAbove >= tooltipHeight + margin\n      ? \"top\"\n      : spaceBelow >= tooltipHeight + margin\n        ? \"bottom\"\n        : spaceAbove >= spaceBelow\n          ? \"top\"\n          : \"bottom\"\n\n  let top =\n    placement === \"top\"\n      ? cellRect.top - containerRect.top - margin\n      : cellBottom - containerRect.top + margin\n\n  if (Number.isFinite(containerHeight)) {\n    if (\n      placement === \"bottom\" &&\n      containerHeight >= tooltipHeight + margin * 2\n    ) {\n      top = Math.min(top, containerHeight - tooltipHeight - margin)\n    } else if (\n      placement === \"top\" &&\n      containerHeight >= tooltipHeight + margin * 2\n    ) {\n      top = Math.max(tooltipHeight + margin, top)\n    }\n  }\n\n  return {\n    top,\n    left: clampedLeft,\n    placement,\n    visible: true,\n  }\n}\n",
      "type": "registry:lib"
    }
  ],
  "type": "registry:ui"
}