{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "activity-heatmap",
  "type": "registry:component",
  "title": "Activity Heatmap",
  "description": "Contribution-style grid for visualizing operational activity over time.",
  "dependencies": [
    "@vllnt/ui@^0.2.1"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/activity-heatmap/activity-heatmap.tsx",
      "content": "import * as React from \"react\";\n\nimport { cn } from \"@vllnt/ui\";\n\nexport type ActivityHeatmapItem = {\n  count: number;\n  date: string;\n};\n\nexport type ActivityHeatmapProps = React.ComponentPropsWithoutRef<\"div\"> & {\n  data: ActivityHeatmapItem[];\n  description?: string;\n  endDate?: Date | number | string;\n  title?: string;\n  weeks?: number;\n};\n\ntype DayCell = {\n  count: number;\n  date: Date;\n  key: string;\n  level: number;\n};\n\nconst WEEKDAY_LABELS = [\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"];\nconst VISIBLE_DAY_LABELS = new Set([\"Mon\", \"Wed\", \"Fri\"]);\nconst LEVEL_CLASS_NAMES = [\n  \"bg-muted\",\n  \"bg-emerald-500/25\",\n  \"bg-emerald-500/45\",\n  \"bg-emerald-500/65\",\n  \"bg-emerald-500\",\n];\n\nfunction normalizeDate(input: Date | number | string): Date {\n  if (input instanceof Date) {\n    return new Date(input.getTime());\n  }\n\n  return new Date(input);\n}\n\nfunction toUtcDate(date: Date): Date {\n  return new Date(\n    Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()),\n  );\n}\n\nfunction addUtcDays(date: Date, days: number): Date {\n  return new Date(\n    Date.UTC(\n      date.getUTCFullYear(),\n      date.getUTCMonth(),\n      date.getUTCDate() + days,\n    ),\n  );\n}\n\nfunction formatDayKey(date: Date): string {\n  return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, \"0\")}-${String(date.getUTCDate()).padStart(2, \"0\")}`;\n}\n\nfunction getIntensityLevel(count: number, maxCount: number): number {\n  if (count <= 0 || maxCount <= 0) {\n    return 0;\n  }\n\n  const ratio = count / maxCount;\n\n  if (ratio < 0.25) {\n    return 1;\n  }\n\n  if (ratio < 0.5) {\n    return 2;\n  }\n\n  if (ratio < 0.75) {\n    return 3;\n  }\n\n  return 4;\n}\n\nfunction getGridData(\n  data: ActivityHeatmapItem[],\n  endDate: Date,\n  weeks: number,\n): DayCell[][] {\n  const normalizedEnd = toUtcDate(endDate);\n  const startDate = addUtcDays(normalizedEnd, -(weeks * 7 - 1));\n  const countsByDate = new Map<string, number>();\n\n  data.forEach((item) => {\n    const normalizedDate = toUtcDate(normalizeDate(item.date));\n    countsByDate.set(formatDayKey(normalizedDate), item.count);\n  });\n\n  const maxCount = Math.max(...data.map((item) => item.count), 0);\n\n  return Array.from({ length: weeks }, (_, weekIndex) => {\n    return Array.from({ length: 7 }, (_, dayIndex) => {\n      const date = addUtcDays(startDate, weekIndex * 7 + dayIndex);\n      const key = formatDayKey(date);\n      const count = countsByDate.get(key) ?? 0;\n\n      return {\n        count,\n        date,\n        key,\n        level: getIntensityLevel(count, maxCount),\n      };\n    });\n  });\n}\n\nconst MONTH_LABEL_FORMATTER = new Intl.DateTimeFormat(\"en-US\", {\n  month: \"short\",\n  timeZone: \"UTC\",\n});\n\nconst TOOLTIP_DATE_FORMATTER = new Intl.DateTimeFormat(\"en-US\", {\n  day: \"numeric\",\n  month: \"short\",\n  timeZone: \"UTC\",\n  year: \"numeric\",\n});\n\nfunction formatMonthLabel(date: Date): string {\n  return MONTH_LABEL_FORMATTER.format(date);\n}\n\nfunction formatTooltip(date: Date, count: number): string {\n  const formattedDate = TOOLTIP_DATE_FORMATTER.format(date);\n  return `${count} activity ${count === 1 ? \"event\" : \"events\"} on ${formattedDate}`;\n}\n\nfunction HeatmapGrid({\n  gridData,\n  weeks,\n}: {\n  gridData: DayCell[][];\n  weeks: number;\n}) {\n  return (\n    <div className=\"min-w-[640px] space-y-3\">\n      <div\n        className=\"grid gap-2 text-xs text-muted-foreground\"\n        style={{ gridTemplateColumns: `40px repeat(${weeks}, minmax(0, 1fr))` }}\n      >\n        <span />\n        {gridData.map((week) => (\n          <span className=\"text-center\" key={`month-${week[0]?.key}`}>\n            {week[0] && week[0].date.getUTCDate() <= 7\n              ? formatMonthLabel(week[0].date)\n              : \"\"}\n          </span>\n        ))}\n      </div>\n\n      <div\n        className=\"grid gap-2\"\n        style={{ gridTemplateColumns: `40px repeat(${weeks}, minmax(0, 1fr))` }}\n      >\n        <div className=\"grid grid-rows-7 gap-2 pt-1 text-xs text-muted-foreground\">\n          {WEEKDAY_LABELS.map((label) => (\n            <span className=\"h-4 leading-4\" key={label}>\n              {VISIBLE_DAY_LABELS.has(label) ? label : \"\"}\n            </span>\n          ))}\n        </div>\n\n        {gridData.map((week) => (\n          <div className=\"grid grid-rows-7 gap-2\" key={`week-${week[0]?.key}`}>\n            {week.map((day) => (\n              <div\n                className={cn(\n                  \"h-4 rounded-sm border border-border/40 transition-colors\",\n                  LEVEL_CLASS_NAMES[day.level],\n                )}\n                key={day.key}\n                role=\"img\"\n                title={formatTooltip(day.date, day.count)}\n              />\n            ))}\n          </div>\n        ))}\n      </div>\n\n      <div className=\"flex items-center justify-end gap-2 text-xs text-muted-foreground\">\n        <span>Less</span>\n        {LEVEL_CLASS_NAMES.map((className) => (\n          <span\n            className={cn(\n              \"size-3 rounded-[3px] border border-border/40\",\n              className,\n            )}\n            key={`legend-${className}`}\n          />\n        ))}\n        <span>More</span>\n      </div>\n    </div>\n  );\n}\n\nexport const ActivityHeatmap = React.forwardRef<\n  HTMLDivElement,\n  ActivityHeatmapProps\n>(\n  (\n    {\n      className,\n      data,\n      description,\n      endDate = new Date(),\n      title = \"Activity heatmap\",\n      weeks = 12,\n      ...props\n    },\n    ref,\n  ) => {\n    const normalizedEndDate = normalizeDate(endDate);\n    const gridData = getGridData(data, normalizedEndDate, weeks);\n\n    return (\n      <div className={cn(\"space-y-4\", className)} ref={ref} {...props}>\n        <div className=\"space-y-1\">\n          <h2 className=\"text-lg font-semibold tracking-tight\">{title}</h2>\n          {description ? (\n            <p className=\"text-sm text-muted-foreground\">{description}</p>\n          ) : null}\n        </div>\n\n        <div className=\"overflow-x-auto rounded-lg border bg-card p-4 shadow-sm\">\n          <HeatmapGrid gridData={gridData} weeks={weeks} />\n        </div>\n      </div>\n    );\n  },\n);\n\nActivityHeatmap.displayName = \"ActivityHeatmap\";\n",
      "type": "registry:component"
    }
  ],
  "version": "0.2.1",
  "stability": "stable"
}
