{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "contribution-graph",
  "title": "Contribution Graph",
  "description": "GitHub-style heatmap of daily activity over time.",
  "dependencies": [
    "@vllnt/ui@^0.2.1"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/contribution-graph/contribution-graph.tsx",
      "content": "import * as React from \"react\";\n\nimport { cn } from \"@vllnt/ui\";\n\n/**\n * A single day cell of a {@link ContributionGraph}.\n *\n * @public\n */\nexport type ContributionDay = {\n  /** Contribution count for the day. */\n  count: number;\n  /** ISO date string (`YYYY-MM-DD`). */\n  date: string;\n};\n\n/**\n * Props for {@link ContributionGraph}.\n *\n * @public\n */\nexport type ContributionGraphProps = {\n  /** Gap between cells in pixels. @defaultValue 3 */\n  cellGap?: number;\n  /** Square cell size in pixels. @defaultValue 12 */\n  cellSize?: number;\n  /** Color of the cells. Defaults to `currentColor` to follow the text token. */\n  color?: string;\n  /** One entry per day. Missing days render as empty cells. */\n  data: ContributionDay[];\n  /** Number of intensity buckets above zero. @defaultValue 4 */\n  levels?: number;\n  /** Optional cap on week columns; keeps the most recent weeks. */\n  weeks?: number;\n} & React.HTMLAttributes<HTMLDivElement>;\n\nconst DEFAULT_CELL_SIZE = 12;\nconst DEFAULT_CELL_GAP = 3;\nconst DEFAULT_LEVELS = 4;\nconst DAYS_PER_WEEK = 7;\nconst MS_PER_DAY = 24 * 60 * 60 * 1000;\n\ntype Cell = { count: number; date: string; level: number };\n\nfunction parseDay(value: string): null | number {\n  const parsed = Date.parse(value);\n  if (Number.isNaN(parsed)) return null;\n  const date = new Date(parsed);\n  return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());\n}\n\nfunction isoFromUtc(time: number): string {\n  return new Date(time).toISOString().slice(0, 10);\n}\n\nfunction bucket(count: number, max: number, levels: number): number {\n  if (count <= 0 || max <= 0) return 0;\n  return Math.min(levels, Math.ceil((count / max) * levels));\n}\n\nfunction countByDay(data: ContributionDay[]): Map<number, number> {\n  return data.reduce<Map<number, number>>((map, entry) => {\n    const time = parseDay(entry.date);\n    if (time !== null) map.set(time, (map.get(time) ?? 0) + entry.count);\n    return map;\n  }, new Map());\n}\n\nfunction buildWeeks(\n  data: ContributionDay[],\n  levels: number,\n  weeksLimit?: number,\n): Cell[][] {\n  const counts = countByDay(data);\n  if (counts.size === 0) return [];\n\n  const times = [...counts.keys()];\n  const minTime = Math.min(...times);\n  const maxTime = Math.max(...times);\n  const start = minTime - new Date(minTime).getUTCDay() * MS_PER_DAY;\n  const end = maxTime + (6 - new Date(maxTime).getUTCDay()) * MS_PER_DAY;\n  const maxCount = Math.max(...counts.values());\n  const weekCount =\n    Math.round((end - start) / (DAYS_PER_WEEK * MS_PER_DAY)) + 1;\n\n  const weeks = Array.from({ length: weekCount }, (_week, weekIndex) =>\n    Array.from({ length: DAYS_PER_WEEK }, (_day, dayIndex) => {\n      const time = start + (weekIndex * DAYS_PER_WEEK + dayIndex) * MS_PER_DAY;\n      const count = counts.get(time) ?? 0;\n      return {\n        count,\n        date: isoFromUtc(time),\n        level: bucket(count, maxCount, levels),\n      };\n    }),\n  );\n\n  if (weeksLimit && weeks.length > weeksLimit) {\n    return weeks.slice(weeks.length - weeksLimit);\n  }\n  return weeks;\n}\n\nfunction cellOpacity(level: number, levels: number): number {\n  if (level <= 0) return 0.1;\n  return 0.25 + 0.75 * (level / levels);\n}\n\n/**\n * Token-styled SVG contribution graph (GitHub-style activity heatmap).\n *\n * Pure SVG, no chart dependency. The chart buckets days into intensity levels\n * and draws a week-by-weekday grid from `currentColor` with stepped opacity, so\n * the heatmap follows the active theme. Returns `null` without dated entries.\n *\n * @example\n * ```tsx\n * <ContributionGraph\n *   className=\"text-primary\"\n *   data={[\n *     { date: \"2026-01-01\", count: 2 },\n *     { date: \"2026-01-02\", count: 7 },\n *   ]}\n * />\n * ```\n *\n * @public\n */\nexport const ContributionGraph = ({\n  cellGap = DEFAULT_CELL_GAP,\n  cellSize = DEFAULT_CELL_SIZE,\n  className,\n  color = \"currentColor\",\n  data,\n  levels = DEFAULT_LEVELS,\n  ref,\n  weeks,\n  ...props\n}: ContributionGraphProps & { ref?: React.Ref<HTMLDivElement> }) => {\n  const ringLevels = Math.max(1, levels);\n  const columns = buildWeeks(data, ringLevels, weeks);\n  if (columns.length === 0) return null;\n\n  const step = cellSize + cellGap;\n  const width = columns.length * step - cellGap;\n  const height = DAYS_PER_WEEK * step - cellGap;\n\n  return (\n    <div\n      className={cn(\n        \"rounded-2xl border border-border bg-background/40 p-3\",\n        className,\n      )}\n      ref={ref}\n      {...props}\n    >\n      <svg\n        aria-label=\"Contribution graph\"\n        className=\"h-full w-full\"\n        height={height}\n        role=\"img\"\n        viewBox={`0 0 ${width} ${height}`}\n        width={width}\n      >\n        {columns.map((column, weekIndex) =>\n          column.map((cell, dayIndex) => (\n            <rect\n              fill={color}\n              fillOpacity={cellOpacity(cell.level, ringLevels)}\n              height={cellSize}\n              key={cell.date}\n              rx={2}\n              width={cellSize}\n              x={weekIndex * step}\n              y={dayIndex * step}\n            >\n              <title>{`${cell.date}: ${cell.count.toLocaleString()}`}</title>\n            </rect>\n          )),\n        )}\n      </svg>\n    </div>\n  );\n};\n\nContributionGraph.displayName = \"ContributionGraph\";\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component",
  "version": "0.2.1",
  "stability": "stable"
}
