{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "activity-graph",
  "type": "registry:ui",
  "title": "Activity Graph",
  "description": "A keyboard-accessible calendar heatmap with normalized intensity levels and an interruptible shared focus surface.",
  "files": [
    {
      "path": "registry/sonaui/activity-graph/activity-graph.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { Tooltip } from \"@base-ui/react/tooltip\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport {\n  type ComponentPropsWithoutRef,\n  type CSSProperties,\n  forwardRef,\n  type KeyboardEvent,\n  type ReactNode,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\n\nimport { cn } from \"@/lib/sona-utils\";\n\nexport interface ActivityGraphDatum {\n  /** Calendar date represented by this record. */\n  date: Date | string;\n  /** Non-negative activity recorded for the date. */\n  value: number;\n  /** Optional accessible label for the record. */\n  label?: string;\n  /** Optional domain data retained for selection callbacks. */\n  metadata?: unknown;\n}\n\nexport interface ActivityGraphValueContext {\n  /** Calendar date represented by the active cell. */\n  date: Date;\n  /** Combined activity record for the active date, when one exists. */\n  item: ActivityGraphDatum | undefined;\n  /** Combined numeric value for the active date. */\n  value: number;\n  /** Normalized visual intensity from zero to the configured level count. */\n  level: number;\n}\n\nexport interface ActivityGraphProps\n  extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\" | \"defaultValue\"> {\n  /** Dated activity records displayed in the graph. */\n  data: ActivityGraphDatum[];\n  /**\n   * Inclusive first date displayed by the graph.\n   * @default 364 days before endDate\n   */\n  startDate?: Date | string;\n  /**\n   * Inclusive last date displayed by the graph.\n   * @default latest data date or today\n   */\n  endDate?: Date | string;\n  /**\n   * Number of non-empty color intensity levels.\n   * @default 4\n   */\n  levels?: number;\n  /**\n   * Maximum number of calendar days rendered.\n   * @default 366\n   */\n  maxDays?: number;\n  /**\n   * First day of each visual week: Sunday, Monday, or Saturday.\n   * @default 0\n   */\n  weekStartsOn?: 0 | 1 | 6;\n  /**\n   * Controlled selected date.\n   * @default undefined\n   */\n  value?: Date | string | null;\n  /**\n   * Initially selected date for uncontrolled usage.\n   * @default undefined\n   */\n  defaultValue?: Date | string | null;\n  /**\n   * Called when a date is selected.\n   * @default undefined\n   */\n  onValueChange?: (date: Date, item: ActivityGraphDatum | undefined) => void;\n  /**\n   * Called when a selected date is activated with pointer or keyboard.\n   * @default undefined\n   */\n  onCellSelect?: (item: ActivityGraphDatum | undefined, date: Date) => void;\n  /**\n   * Custom content shown for the currently explored date.\n   * @default undefined\n   */\n  renderValue?: (context: ActivityGraphValueContext) => ReactNode;\n  /**\n   * Shows the active date and value above the graph.\n   * @default true\n   */\n  showValue?: boolean;\n  /**\n   * Shows an anchored tooltip when a date cell is hovered or focused.\n   * @default false\n   */\n  showTooltip?: boolean;\n  /**\n   * Delay before the first tooltip opens, in milliseconds.\n   * @default 400\n   */\n  tooltipDelay?: number;\n  /**\n   * Custom content rendered inside the optional cell tooltip.\n   * @default undefined\n   */\n  renderTooltip?: (context: ActivityGraphValueContext) => ReactNode;\n  /**\n   * Custom colors for non-empty intensity levels, ordered from low to high.\n   * @default undefined\n   */\n  colors?: string[];\n  /**\n   * Custom color for dates without activity.\n   * @default undefined\n   */\n  emptyColor?: string;\n  /**\n   * Shows month labels above the graph.\n   * @default true\n   */\n  showMonthLabels?: boolean;\n  /**\n   * Shows abbreviated weekday labels beside the graph.\n   * @default true\n   */\n  showWeekdayLabels?: boolean;\n  /**\n   * Shows the intensity legend below the graph.\n   * @default true\n   */\n  showLegend?: boolean;\n  /**\n   * Accessible description for a date without activity.\n   * @default \"No activity\"\n   */\n  emptyLabel?: string;\n  /**\n   * Accessible name for the interactive graph.\n   * @default \"Activity graph\"\n   */\n  ariaLabel?: string;\n  /**\n   * Additional classes for the scrollable graph region.\n   * @default undefined\n   */\n  gridClassName?: string;\n  /**\n   * Additional classes applied to every date cell.\n   * @default undefined\n   */\n  cellClassName?: string;\n  /**\n   * Additional classes for the optional tooltip surface.\n   * @default undefined\n   */\n  tooltipClassName?: string;\n  /**\n   * Additional classes for the intensity legend.\n   * @default undefined\n   */\n  legendClassName?: string;\n}\n\ninterface NormalizedCell {\n  date: Date;\n  key: string;\n  item: ActivityGraphDatum | undefined;\n  value: number;\n  level: number;\n}\n\ntype TooltipDirection = \"up\" | \"right\" | \"down\" | \"left\" | \"none\";\n\nconst DAY_MS = 86_400_000;\nconst DEFAULT_MAX_DAYS = 366;\nconst levelClasses = [\n  \"bg-[var(--activity-graph-level-1)]\",\n  \"bg-[var(--activity-graph-level-2)]\",\n  \"bg-[var(--activity-graph-level-3)]\",\n  \"bg-[var(--activity-graph-level-4)]\",\n  \"bg-[var(--activity-graph-level-5)]\",\n  \"bg-[var(--activity-graph-level-6)]\",\n] as const;\n\nconst tokenStyle = {\n  \"--activity-graph-empty\":\n    \"color-mix(in oklab, var(--muted) 76%, var(--background))\",\n  \"--activity-graph-level-1\":\n    \"color-mix(in oklab, var(--primary) 24%, var(--background))\",\n  \"--activity-graph-level-2\":\n    \"color-mix(in oklab, var(--primary) 42%, var(--background))\",\n  \"--activity-graph-level-3\":\n    \"color-mix(in oklab, var(--primary) 62%, var(--background))\",\n  \"--activity-graph-level-4\":\n    \"color-mix(in oklab, var(--primary) 82%, var(--background))\",\n  \"--activity-graph-level-5\":\n    \"color-mix(in oklab, var(--primary) 91%, var(--background))\",\n  \"--activity-graph-level-6\": \"var(--primary)\",\n  \"--activity-graph-focus-ring\": \"var(--ring)\",\n  \"--activity-graph-label\": \"var(--foreground)\",\n  \"--activity-graph-muted-label\": \"var(--muted-foreground)\",\n  \"--activity-graph-tooltip-surface\":\n    \"color-mix(in oklab, var(--popover) 94%, transparent)\",\n  \"--activity-graph-tooltip-foreground\": \"var(--popover-foreground)\",\n  \"--activity-graph-cell-size\": \"0.75rem\",\n  \"--activity-graph-cell-gap\": \"0.25rem\",\n  \"--activity-graph-cell-radius\": \"0.1875rem\",\n} as CSSProperties;\nconst dateFormatter = new Intl.DateTimeFormat(\"en\", {\n  month: \"long\",\n  day: \"numeric\",\n  year: \"numeric\",\n  timeZone: \"UTC\",\n});\nconst monthFormatter = new Intl.DateTimeFormat(\"en\", {\n  month: \"short\",\n  timeZone: \"UTC\",\n});\nconst weekdayFormatter = new Intl.DateTimeFormat(\"en\", {\n  weekday: \"narrow\",\n  timeZone: \"UTC\",\n});\n\nfunction toUtcDate(value: Date | string): Date | null {\n  if (value instanceof Date) {\n    if (Number.isNaN(value.getTime())) return null;\n\n    return new Date(\n      Date.UTC(value.getFullYear(), value.getMonth(), value.getDate()),\n    );\n  }\n\n  const calendarDate = /^(\\d{4})-(\\d{2})-(\\d{2})/.exec(value.trim());\n  if (calendarDate) {\n    const year = Number(calendarDate[1]);\n    const month = Number(calendarDate[2]) - 1;\n    const day = Number(calendarDate[3]);\n    const date = new Date(Date.UTC(year, month, day));\n\n    return date.getUTCFullYear() === year &&\n      date.getUTCMonth() === month &&\n      date.getUTCDate() === day\n      ? date\n      : null;\n  }\n\n  const parsed = new Date(value);\n  if (Number.isNaN(parsed.getTime())) return null;\n\n  return new Date(\n    Date.UTC(parsed.getFullYear(), parsed.getMonth(), parsed.getDate()),\n  );\n}\n\nfunction dateKey(date: Date) {\n  return date.toISOString().slice(0, 10);\n}\n\nfunction addDays(date: Date, amount: number) {\n  return new Date(date.getTime() + amount * DAY_MS);\n}\n\nfunction startOfWeek(date: Date, weekStartsOn: 0 | 1 | 6) {\n  const offset = (date.getUTCDay() - weekStartsOn + 7) % 7;\n  return addDays(date, -offset);\n}\n\nfunction endOfWeek(date: Date, weekStartsOn: 0 | 1 | 6) {\n  return addDays(startOfWeek(date, weekStartsOn), 6);\n}\n\nfunction clampDate(date: Date, start: Date, end: Date) {\n  if (date < start) return start;\n  if (date > end) return end;\n  return date;\n}\n\nfunction getWeekdayLabels(\n  weekStartsOn: 0 | 1 | 6,\n  formatter: Intl.DateTimeFormat,\n) {\n  const sunday = new Date(Date.UTC(2026, 0, 4));\n  return Array.from({ length: 7 }, (_, index) => {\n    const day = (weekStartsOn + index) % 7;\n    return {\n      key: `weekday-${day}`,\n      day,\n      label: formatter.format(addDays(sunday, day)),\n    };\n  });\n}\n\nconst ActivityGraph = forwardRef<HTMLDivElement, ActivityGraphProps>(\n  function ActivityGraph(\n    {\n      data,\n      startDate,\n      endDate,\n      levels = 4,\n      maxDays = DEFAULT_MAX_DAYS,\n      weekStartsOn = 0,\n      value,\n      defaultValue,\n      onValueChange,\n      onCellSelect,\n      renderValue,\n      showValue = true,\n      showTooltip = false,\n      tooltipDelay = 400,\n      renderTooltip,\n      colors,\n      emptyColor,\n      showMonthLabels = true,\n      showWeekdayLabels = true,\n      showLegend = true,\n      emptyLabel = \"No activity\",\n      ariaLabel = \"Activity graph\",\n      className,\n      style,\n      gridClassName,\n      cellClassName,\n      tooltipClassName,\n      legendClassName,\n      ...rootProps\n    },\n    forwardedRef,\n  ) {\n    const layoutId = useId();\n    const shouldReduceMotion = useReducedMotion();\n    const safeLevels = Number.isFinite(levels)\n      ? Math.min(6, Math.max(1, Math.round(levels)))\n      : 4;\n    const safeMaxDays = Number.isFinite(maxDays)\n      ? Math.max(1, Math.round(maxDays))\n      : DEFAULT_MAX_DAYS;\n    const tooltipHandle = useMemo(\n      () => Tooltip.createHandle<ActivityGraphValueContext>(),\n      [],\n    );\n    const resolvedTokenStyle = useMemo(() => {\n      const style = {\n        ...tokenStyle,\n      } as CSSProperties & Record<`--activity-graph-${string}`, string>;\n\n      colors?.slice(0, 6).forEach((color, index) => {\n        style[`--activity-graph-level-${index + 1}`] = color;\n      });\n      if (emptyColor) style[\"--activity-graph-empty\"] = emptyColor;\n\n      return style;\n    }, [colors, emptyColor]);\n\n    const model = useMemo(() => {\n      const records = new Map<string, ActivityGraphDatum>();\n\n      for (const datum of data) {\n        const date = toUtcDate(datum.date);\n        if (!date) continue;\n        const key = dateKey(date);\n        const previous = records.get(key);\n        const nextValue =\n          Math.max(0, Number.isFinite(datum.value) ? datum.value : 0) +\n          (previous?.value ?? 0);\n\n        records.set(key, {\n          ...datum,\n          date,\n          value: nextValue,\n          label: previous ? undefined : datum.label,\n          metadata: datum.metadata ?? previous?.metadata,\n        });\n      }\n\n      const dataDates = [...records.keys()].sort();\n      const latestDataDate = dataDates.at(-1);\n      const fallbackEnd =\n        (latestDataDate ? toUtcDate(latestDataDate) : null) ??\n        toUtcDate(new Date()) ??\n        new Date(0);\n      const requestedEnd = endDate ? toUtcDate(endDate) : null;\n      const resolvedEnd = requestedEnd ?? fallbackEnd;\n      const requestedStart = startDate ? toUtcDate(startDate) : null;\n      const resolvedStart = requestedStart ?? addDays(resolvedEnd, -364);\n      const orderedStart =\n        resolvedStart <= resolvedEnd ? resolvedStart : resolvedEnd;\n      const rangeEnd =\n        resolvedStart <= resolvedEnd ? resolvedEnd : resolvedStart;\n      const rangeStart = new Date(\n        Math.max(\n          orderedStart.getTime(),\n          addDays(rangeEnd, -(safeMaxDays - 1)).getTime(),\n        ),\n      );\n      const gridStart = startOfWeek(rangeStart, weekStartsOn);\n      const gridEnd = endOfWeek(rangeEnd, weekStartsOn);\n\n      const nonZeroValues = [...records.entries()]\n        .filter(\n          ([key]) => key >= dateKey(rangeStart) && key <= dateKey(rangeEnd),\n        )\n        .map(([, item]) => item.value)\n        .filter((itemValue) => itemValue > 0)\n        .sort((a, b) => a - b);\n      const levelByValue = new Map<number, number>();\n\n      nonZeroValues.forEach((itemValue, index) => {\n        if (levelByValue.has(itemValue)) return;\n        levelByValue.set(\n          itemValue,\n          Math.max(\n            1,\n            Math.min(\n              safeLevels,\n              Math.ceil(((index + 1) / nonZeroValues.length) * safeLevels),\n            ),\n          ),\n        );\n      });\n\n      const getLevel = (itemValue: number) => {\n        if (itemValue <= 0) return 0;\n        return levelByValue.get(itemValue) ?? 1;\n      };\n\n      const cells: NormalizedCell[] = [];\n      for (\n        let current = gridStart;\n        current <= gridEnd;\n        current = addDays(current, 1)\n      ) {\n        const key = dateKey(current);\n        const item = records.get(key);\n        const inRange = current >= rangeStart && current <= rangeEnd;\n        const itemValue = inRange ? (item?.value ?? 0) : 0;\n\n        cells.push({\n          date: current,\n          key,\n          item: inRange ? item : undefined,\n          value: itemValue,\n          level: getLevel(itemValue),\n        });\n      }\n\n      const weeks = Array.from(\n        { length: Math.ceil(cells.length / 7) },\n        (_, index) => cells.slice(index * 7, index * 7 + 7),\n      );\n\n      const monthMap = new Map<number, string>([\n        [0, monthFormatter.format(rangeStart)],\n      ]);\n      for (const [weekIndex, week] of weeks.entries()) {\n        const firstOfMonth = week.find(\n          (cell) =>\n            cell.date.getUTCDate() === 1 &&\n            cell.date >= rangeStart &&\n            cell.date <= rangeEnd,\n        );\n        if (firstOfMonth) {\n          monthMap.set(weekIndex, monthFormatter.format(firstOfMonth.date));\n        }\n      }\n      const monthCandidates = [...monthMap].map(([weekIndex, label]) => ({\n        label,\n        weekIndex,\n      }));\n      const months = monthCandidates.filter((month, index) => {\n        if (index === 0) return true;\n        const nextMonth = monthCandidates[index + 1];\n        return !nextMonth || nextMonth.weekIndex - month.weekIndex >= 2;\n      });\n\n      const rangeStartIndex = cells.findIndex(\n        (cell) => cell.key === dateKey(rangeStart),\n      );\n      const rangeEndIndex = cells.findIndex(\n        (cell) => cell.key === dateKey(rangeEnd),\n      );\n\n      return {\n        cells,\n        weeks,\n        months,\n        rangeStart,\n        rangeEnd,\n        rangeStartIndex,\n        rangeEndIndex,\n      };\n    }, [data, endDate, safeLevels, safeMaxDays, startDate, weekStartsOn]);\n\n    const controlledDate =\n      value !== undefined && value !== null ? toUtcDate(value) : null;\n    const controlledKey =\n      value !== undefined\n        ? controlledDate\n          ? dateKey(clampDate(controlledDate, model.rangeStart, model.rangeEnd))\n          : null\n        : undefined;\n    const initialDate =\n      defaultValue !== undefined && defaultValue !== null\n        ? toUtcDate(defaultValue)\n        : null;\n    const [internalKey, setInternalKey] = useState<string | null>(() =>\n      initialDate\n        ? dateKey(clampDate(initialDate, model.rangeStart, model.rangeEnd))\n        : null,\n    );\n    const [focusedKey, setFocusedKey] = useState<string | null>(null);\n    const [lastFocusedKey, setLastFocusedKey] = useState(() =>\n      dateKey(model.rangeEnd),\n    );\n    const [hoveredKey, setHoveredKey] = useState<string | null>(null);\n    const [keyboardNavigation, setKeyboardNavigation] = useState(false);\n    const [tooltipDirection, setTooltipDirection] =\n      useState<TooltipDirection>(\"none\");\n    const previousHoveredIndex = useRef<number | null>(null);\n    const cellRefs = useRef(new Map<string, HTMLButtonElement>());\n    const scrollAreaRef = useRef<HTMLDivElement>(null);\n    const internalDate = internalKey ? toUtcDate(internalKey) : null;\n    const resolvedInternalKey = internalDate\n      ? dateKey(clampDate(internalDate, model.rangeStart, model.rangeEnd))\n      : null;\n    const selectedKey =\n      controlledKey !== undefined ? controlledKey : resolvedInternalKey;\n    const rovingKey = model.cells.some((cell) => cell.key === lastFocusedKey)\n      ? lastFocusedKey\n      : (selectedKey ?? dateKey(model.rangeEnd));\n    const activeKey = keyboardNavigation\n      ? (focusedKey ?? selectedKey)\n      : (hoveredKey ?? focusedKey ?? selectedKey);\n    const activeCell = activeKey\n      ? model.cells.find((cell) => cell.key === activeKey)\n      : undefined;\n    const rovingIndex = model.cells.findIndex((cell) => cell.key === rovingKey);\n\n    useEffect(() => {\n      const scrollArea = scrollAreaRef.current;\n      if (!scrollArea || model.weeks.length === 0) return;\n      scrollArea.scrollLeft = scrollArea.scrollWidth;\n    }, [model.weeks.length]);\n\n    const selectCell = (cell: NormalizedCell, activate = false) => {\n      const clamped = clampDate(cell.date, model.rangeStart, model.rangeEnd);\n      const key = dateKey(clamped);\n      const item = model.cells.find((candidate) => candidate.key === key)?.item;\n      if (value === undefined) setInternalKey(key);\n      onValueChange?.(clamped, item);\n      if (activate) onCellSelect?.(item, clamped);\n    };\n\n    const handleKeyDown = (\n      event: KeyboardEvent<HTMLButtonElement>,\n      index: number,\n    ) => {\n      let nextIndex = index;\n      const rowIndex = index % 7;\n\n      if (event.key === \"ArrowLeft\") nextIndex = index - 7;\n      else if (event.key === \"ArrowRight\") nextIndex = index + 7;\n      else if (event.key === \"ArrowUp\")\n        nextIndex = rowIndex === 0 ? index : index - 1;\n      else if (event.key === \"ArrowDown\")\n        nextIndex = rowIndex === 6 ? index : index + 1;\n      else if (event.key === \"Home\") {\n        if (event.metaKey || event.ctrlKey) {\n          nextIndex = model.rangeStartIndex;\n        } else {\n          nextIndex = model.cells.findIndex(\n            (cell, candidateIndex) =>\n              candidateIndex >= model.rangeStartIndex &&\n              candidateIndex % 7 === rowIndex &&\n              cell.date <= model.rangeEnd,\n          );\n        }\n      } else if (event.key === \"End\") {\n        if (event.metaKey || event.ctrlKey) {\n          nextIndex = model.rangeEndIndex;\n        } else {\n          for (\n            let candidateIndex = model.rangeEndIndex;\n            candidateIndex >= model.rangeStartIndex;\n            candidateIndex -= 1\n          ) {\n            if (candidateIndex % 7 === rowIndex) {\n              nextIndex = candidateIndex;\n              break;\n            }\n          }\n        }\n      } else if (event.key === \"Enter\" || event.key === \" \") {\n        event.preventDefault();\n        selectCell(model.cells[index], true);\n        return;\n      } else {\n        return;\n      }\n\n      event.preventDefault();\n      const boundedIndex =\n        nextIndex >= model.rangeStartIndex && nextIndex <= model.rangeEndIndex\n          ? nextIndex\n          : index;\n      const nextCell = model.cells[boundedIndex];\n      setKeyboardNavigation(true);\n      setHoveredKey(null);\n      setFocusedKey(nextCell.key);\n      setLastFocusedKey(nextCell.key);\n      requestAnimationFrame(() => {\n        cellRefs.current.get(nextCell.key)?.focus();\n      });\n    };\n\n    const weekdayLabels = useMemo(\n      () => getWeekdayLabels(weekStartsOn, weekdayFormatter),\n      [weekStartsOn],\n    );\n    const activeContext: ActivityGraphValueContext | undefined = activeCell\n      ? {\n          date: activeCell.date,\n          item: activeCell.item,\n          value: activeCell.value,\n          level: activeCell.level,\n        }\n      : undefined;\n    const tooltipContentOffset =\n      tooltipDirection === \"left\"\n        ? { x: -4, y: 0 }\n        : tooltipDirection === \"right\"\n          ? { x: 4, y: 0 }\n          : tooltipDirection === \"up\"\n            ? { x: 0, y: -4 }\n            : tooltipDirection === \"down\"\n              ? { x: 0, y: 4 }\n              : { x: 0, y: 0 };\n    const totalActivity = model.cells.reduce(\n      (total, cell) => total + cell.value,\n      0,\n    );\n    const summaryId = `${layoutId}-summary`;\n\n    return (\n      <Tooltip.Provider delay={Math.max(0, tooltipDelay)}>\n        <div\n          ref={forwardedRef}\n          data-slot=\"activity-graph\"\n          className={cn(\n            \"w-full text-[var(--activity-graph-label)] [@media(pointer:coarse)]:[--activity-graph-cell-size:1.25rem]\",\n            className,\n          )}\n          style={{ ...resolvedTokenStyle, ...style }}\n          {...rootProps}\n        >\n          <p id={summaryId} className=\"sr-only\">\n            {totalActivity} total activities from{\" \"}\n            {dateFormatter.format(model.rangeStart)} to{\" \"}\n            {dateFormatter.format(model.rangeEnd)}.\n          </p>\n\n          {showValue && (\n            <div\n              data-slot=\"activity-graph-value\"\n              className=\"mb-3 flex min-h-5 items-baseline gap-2 text-sm\"\n            >\n              {activeContext && renderValue ? (\n                renderValue(activeContext)\n              ) : activeContext ? (\n                <>\n                  <span className=\"font-medium\">\n                    {dateFormatter.format(activeContext.date)}\n                  </span>\n                  <span className=\"text-[var(--activity-graph-muted-label)] tabular-nums\">\n                    {activeContext.value === 0\n                      ? emptyLabel\n                      : `${activeContext.value} ${activeContext.value === 1 ? \"activity\" : \"activities\"}`}\n                  </span>\n                </>\n              ) : (\n                <span className=\"font-medium tabular-nums\">\n                  {totalActivity} total{\" \"}\n                  {totalActivity === 1 ? \"activity\" : \"activities\"}\n                </span>\n              )}\n            </div>\n          )}\n\n          <div\n            ref={scrollAreaRef}\n            data-slot=\"activity-graph-scroll-area\"\n            className={cn(\n              \"max-w-full overflow-x-auto pb-1 [scrollbar-width:thin]\",\n              gridClassName,\n            )}\n          >\n            <div className=\"w-max min-w-full\">\n              {showMonthLabels && (\n                <div\n                  aria-hidden=\"true\"\n                  className={cn(\n                    \"mb-1 grid h-4 text-[0.6875rem] text-[var(--activity-graph-muted-label)]\",\n                    showWeekdayLabels && \"ml-7\",\n                  )}\n                  style={{\n                    gridTemplateColumns: `repeat(${model.weeks.length}, var(--activity-graph-cell-size))`,\n                    columnGap: \"var(--activity-graph-cell-gap)\",\n                  }}\n                >\n                  {model.months.map((month) => (\n                    <span\n                      key={`${month.label}-${month.weekIndex}`}\n                      className=\"whitespace-nowrap\"\n                      style={{ gridColumnStart: month.weekIndex + 1 }}\n                    >\n                      {month.label}\n                    </span>\n                  ))}\n                </div>\n              )}\n\n              <div className=\"flex gap-[var(--activity-graph-cell-gap)]\">\n                {showWeekdayLabels && (\n                  <div\n                    aria-hidden=\"true\"\n                    className=\"grid w-6 shrink-0 text-[0.625rem] text-[var(--activity-graph-muted-label)]\"\n                    style={{\n                      gridTemplateRows:\n                        \"repeat(7, var(--activity-graph-cell-size))\",\n                      rowGap: \"var(--activity-graph-cell-gap)\",\n                    }}\n                  >\n                    {weekdayLabels.map((weekday) => (\n                      <span\n                        key={weekday.key}\n                        className={cn(\n                          \"flex items-center\",\n                          ![1, 3, 5].includes(weekday.day) && \"invisible\",\n                        )}\n                      >\n                        {weekday.label}\n                      </span>\n                    ))}\n                  </div>\n                )}\n\n                {/* biome-ignore lint/a11y/useSemanticElements: the interactive calendar uses ARIA grid navigation rather than table navigation */}\n                <div\n                  role=\"grid\"\n                  aria-label={ariaLabel}\n                  aria-describedby={summaryId}\n                  aria-rowcount={7}\n                  aria-colcount={model.weeks.length}\n                  data-slot=\"activity-graph-grid\"\n                  className=\"m-0 grid min-w-0 grid-flow-col border-0 p-0\"\n                  style={{\n                    gridTemplateRows:\n                      \"repeat(7, var(--activity-graph-cell-size))\",\n                    gridTemplateColumns: `repeat(${model.weeks.length}, var(--activity-graph-cell-size))`,\n                    gap: \"var(--activity-graph-cell-gap)\",\n                  }}\n                  onMouseLeave={() => {\n                    previousHoveredIndex.current = null;\n                    setHoveredKey(null);\n                    setTooltipDirection(\"none\");\n                  }}\n                >\n                  {model.cells.map((cell, index) => {\n                    const outsideRange =\n                      cell.date < model.rangeStart ||\n                      cell.date > model.rangeEnd;\n                    const isSelected = cell.key === selectedKey;\n                    const label =\n                      cell.item?.label ??\n                      (cell.value === 0\n                        ? emptyLabel\n                        : `${cell.value} ${cell.value === 1 ? \"activity\" : \"activities\"}`);\n\n                    if (outsideRange) {\n                      return (\n                        <span\n                          key={cell.key}\n                          aria-hidden=\"true\"\n                          data-slot=\"activity-graph-spacer\"\n                        />\n                      );\n                    }\n\n                    return (\n                      <Tooltip.Trigger\n                        key={cell.key}\n                        handle={tooltipHandle}\n                        payload={{\n                          date: cell.date,\n                          item: cell.item,\n                          value: cell.value,\n                          level: cell.level,\n                        }}\n                        disabled={!showTooltip}\n                        render={<button type=\"button\" />}\n                        role=\"gridcell\"\n                        aria-rowindex={(index % 7) + 1}\n                        aria-colindex={Math.floor(index / 7) + 1}\n                        aria-label={`${dateFormatter.format(cell.date)}: ${label}`}\n                        aria-current={isSelected ? \"date\" : undefined}\n                        aria-selected={isSelected}\n                        tabIndex={index === rovingIndex ? 0 : -1}\n                        ref={(node: HTMLButtonElement | null) => {\n                          if (node) cellRefs.current.set(cell.key, node);\n                          else cellRefs.current.delete(cell.key);\n                        }}\n                        data-slot=\"activity-graph-cell\"\n                        data-activity-graph={layoutId}\n                        data-date={cell.key}\n                        onPointerEnter={(event) => {\n                          if (event.pointerType === \"touch\") return;\n                          const previousIndex = previousHoveredIndex.current;\n\n                          if (previousIndex === null) {\n                            setTooltipDirection(\"none\");\n                          } else {\n                            const horizontalDelta =\n                              Math.floor(index / 7) -\n                              Math.floor(previousIndex / 7);\n                            const verticalDelta =\n                              (index % 7) - (previousIndex % 7);\n\n                            if (\n                              Math.abs(horizontalDelta) >=\n                              Math.abs(verticalDelta)\n                            ) {\n                              setTooltipDirection(\n                                horizontalDelta < 0 ? \"left\" : \"right\",\n                              );\n                            } else {\n                              setTooltipDirection(\n                                verticalDelta < 0 ? \"up\" : \"down\",\n                              );\n                            }\n                          }\n\n                          previousHoveredIndex.current = index;\n                          setKeyboardNavigation(false);\n                          setHoveredKey(cell.key);\n                        }}\n                        onPointerDown={() => setKeyboardNavigation(false)}\n                        onClick={() => selectCell(cell, true)}\n                        onFocus={() => {\n                          setFocusedKey(cell.key);\n                          setLastFocusedKey(cell.key);\n                        }}\n                        onBlur={() => setFocusedKey(null)}\n                        onKeyDown={(event) => handleKeyDown(event, index)}\n                        className={cn(\n                          \"relative isolate rounded-[var(--activity-graph-cell-radius)] outline-none transition-[filter,box-shadow] duration-150 before:absolute before:inset-[calc(var(--activity-graph-cell-gap)/-2)] before:content-[''] hover:brightness-110\",\n                          \"focus-visible:z-10 focus-visible:ring-2 focus-visible:ring-[var(--activity-graph-focus-ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n                          \"active:scale-[0.96]\",\n                          isSelected &&\n                            \"z-[1] ring-1 ring-[var(--activity-graph-focus-ring)] ring-offset-1 ring-offset-background\",\n                          cell.level === 0\n                            ? \"bg-[var(--activity-graph-empty)]\"\n                            : levelClasses[cell.level - 1],\n                          cellClassName,\n                        )}\n                      ></Tooltip.Trigger>\n                    );\n                  })}\n                </div>\n              </div>\n            </div>\n          </div>\n\n          {showLegend && (\n            <div\n              data-slot=\"activity-graph-legend\"\n              className={cn(\n                \"mt-3 flex items-center justify-end gap-1.5 text-[0.6875rem] text-[var(--activity-graph-muted-label)]\",\n                legendClassName,\n              )}\n            >\n              <span>Less</span>\n              <span\n                aria-hidden=\"true\"\n                className=\"size-[var(--activity-graph-cell-size)] rounded-[var(--activity-graph-cell-radius)] bg-[var(--activity-graph-empty)]\"\n              />\n              {Array.from({ length: safeLevels }, (_, index) => (\n                <span\n                  key={levelClasses[index]}\n                  aria-hidden=\"true\"\n                  className={cn(\n                    \"size-[var(--activity-graph-cell-size)] rounded-[var(--activity-graph-cell-radius)]\",\n                    levelClasses[index],\n                  )}\n                />\n              ))}\n              <span>More</span>\n            </div>\n          )}\n        </div>\n\n        <Tooltip.Root handle={tooltipHandle} disabled={!showTooltip}>\n          {({ payload }) => (\n            <Tooltip.Portal>\n              <Tooltip.Positioner\n                sideOffset={8}\n                collisionPadding={8}\n                className={cn(\n                  \"z-50 transition-transform duration-200 [transition-timing-function:cubic-bezier(0.32,0.72,0,1)]\",\n                  (shouldReduceMotion || keyboardNavigation) &&\n                    \"transition-none\",\n                )}\n                style={resolvedTokenStyle}\n              >\n                <Tooltip.Popup\n                  data-slot=\"activity-graph-tooltip\"\n                  className={cn(\n                    \"relative max-w-64 origin-[var(--transform-origin)] rounded-lg border border-border/70 bg-[var(--activity-graph-tooltip-surface)] px-2.5 py-2 text-xs text-[var(--activity-graph-tooltip-foreground)] shadow-lg backdrop-blur-md\",\n                    \"transition-[transform,opacity,filter] duration-150 data-ending-style:scale-[0.98] data-ending-style:opacity-0 data-ending-style:blur-[2px] data-instant:transition-none data-starting-style:scale-[0.96] data-starting-style:opacity-0 data-starting-style:blur-[2px]\",\n                    shouldReduceMotion && \"transition-none\",\n                    tooltipClassName,\n                  )}\n                >\n                  {payload && (\n                    <motion.div\n                      key={dateKey(payload.date)}\n                      data-slot=\"activity-graph-tooltip-content\"\n                      initial={\n                        shouldReduceMotion ||\n                        keyboardNavigation ||\n                        tooltipDirection === \"none\"\n                          ? false\n                          : {\n                              opacity: 0.72,\n                              x: tooltipContentOffset.x,\n                              y: tooltipContentOffset.y,\n                            }\n                      }\n                      animate={{ opacity: 1, x: 0, y: 0 }}\n                      transition={{\n                        duration: 0.14,\n                        ease: [0.32, 0.72, 0, 1],\n                      }}\n                    >\n                      {renderTooltip ? (\n                        renderTooltip(payload)\n                      ) : (\n                        <div className=\"flex flex-col gap-0.5\">\n                          <span className=\"font-medium\">\n                            {dateFormatter.format(payload.date)}\n                          </span>\n                          <span className=\"text-muted-foreground tabular-nums\">\n                            {payload.item?.label ??\n                              (payload.value === 0\n                                ? emptyLabel\n                                : `${payload.value} ${payload.value === 1 ? \"activity\" : \"activities\"}`)}\n                          </span>\n                        </div>\n                      )}\n                    </motion.div>\n                  )}\n                  <Tooltip.Arrow className=\"flex size-2.5 rotate-45 border-border/70 border-r border-b bg-[var(--activity-graph-tooltip-surface)]\" />\n                </Tooltip.Popup>\n              </Tooltip.Positioner>\n            </Tooltip.Portal>\n          )}\n        </Tooltip.Root>\n      </Tooltip.Provider>\n    );\n  },\n);\n\nexport default ActivityGraph;\n",
      "target": "components/ui/activity-graph/activity-graph.tsx"
    }
  ],
  "dependencies": [
    "@base-ui/react",
    "motion"
  ],
  "registryDependencies": [
    "@sona-ui/sona-utils"
  ]
}