{
  "slug": "github-activity",
  "name": "GitHub Activity",
  "description": "A glass-style contribution heatmap with hover tooltips, three color variants and an activity legend.",
  "summary": "A glass-style GitHub contribution heatmap. Renders one calendar year (Jan 1 to Dec 31 of year, 53 or 54 week columns) across 7 weekday rows, with a hover tooltip showing the count and date, three color variants, a total count footer and a Less/More legend. Pass your own contribution data; without it the component generates deterministic sample data for the year. It does not fetch from GitHub.",
  "category": "display",
  "tags": [
    "heatmap",
    "github",
    "contributions",
    "glassmorphism",
    "visualization",
    "metrics",
    "dradix"
  ],
  "dependencies": [
    "clsx",
    "tailwind-merge",
    "motion"
  ],
  "version": "2.0.0",
  "createdDate": "2026-09-20",
  "updatedDate": "2026-09-21",
  "interactive": true,
  "supportsColor": false,
  "highlights": [
    "Full-year grid of 7 weekday rows (53 or 54 week columns) with deterministic sample data when no data is passed",
    "Three color variants: teal (default), emerald and github",
    "Cell hover scale (scale-125) with a spring-animated glass tooltip",
    "Pure chart architecture with zero extra drawer blocks for maximum minimalism",
    "Multi-layered glassmorphic container with backdrop-blur-xl and delicate light sheen"
  ],
  "anatomy": [
    "<div> (Glass Shell: rounded-2xl container with backdrop-blur-xl and multi-layer depth shadow)",
    "<div> (Header: matrix title and subtitle)",
    "<div> (Month Labels: horizontal interval markers across the top of the grid)",
    "<div> (Grid Matrix: one flex column per week, 53 or 54 columns, each with 7 day cells)",
    "<motion.div> (Gliding Tooltip: spring-animated floating glass pill with live rolling text)",
    "<div> (Footer: total count and Less/More legend)"
  ],
  "physics": {
    "engine": "Motion Spring Gliding & popLayout Text Transitions",
    "description": "A spring (stiffness 480, damping 32, mass 0.5) moves the tooltip between hovered cells, and popLayout transitions (140ms ease-out) swap the count and date text.",
    "parameters": [
      {
        "label": "Cell Hover Scale",
        "value": "1.25 (25% magnification)"
      },
      {
        "label": "Transition Duration",
        "value": "150ms cubic-bezier(0.4, 0, 0.2, 1)"
      },
      {
        "label": "Tooltip Delay",
        "value": "0ms instantaneous binding"
      }
    ]
  },
  "accessibility": {
    "aria": "No ARIA attributes are set. Cells are unlabelled divs, so counts and dates are only available through the mouse hover tooltip. The root spreads extra props, so you can pass role and aria-label.",
    "reducedMotion": "No reduced-motion handling. The cell hover scale and the tooltip spring and text transitions always run.",
    "keyboard": [
      {
        "key": "None",
        "description": "No element is focusable. The tooltip opens on mouse hover only."
      }
    ]
  },
  "guidelines": {
    "recommended": [
      "Developer portfolio hero sections and about pages",
      "Open-source organization dashboards and profile cards",
      "Telemetry and commit streak tracking widgets"
    ],
    "bestPractices": [
      "Pass real contribution data through data or contributions to replace the generated sample data. The component does not fetch from GitHub.",
      "totalContributions is optional. When omitted it is the sum of all cell counts; if you pass it, keep it equal to that sum.",
      "With the default blockSize (10) and blockGap (3) a full year is about 690px wide. Narrower containers scroll horizontally, and below a 768px viewport the grid starts scrolled to the latest week."
    ]
  },
  "props": [
    {
      "name": "username",
      "type": "string",
      "defaultValue": "undefined",
      "description": "GitHub username shown as @username under the title when no subtitle is set. No data is fetched."
    },
    {
      "name": "title",
      "type": "string",
      "defaultValue": "\"Commit Heatmap\"",
      "description": "Main header title for the contribution heatmap card."
    },
    {
      "name": "subtitle",
      "type": "string",
      "defaultValue": "undefined",
      "description": "Secondary header label or user attribution."
    },
    {
      "name": "totalContributions",
      "type": "number",
      "defaultValue": "sum of data",
      "description": "Total shown in the footer. Defaults to the sum of the contribution counts."
    },
    {
      "name": "year",
      "type": "number",
      "defaultValue": "current year",
      "description": "Year used for the footer label and for the generated sample data when no data is passed."
    },
    {
      "name": "variant",
      "type": "\"teal\" | \"emerald\" | \"github\"",
      "defaultValue": "\"teal\"",
      "description": "Color theme of the heatmap cells."
    },
    {
      "name": "blockSize",
      "type": "number",
      "defaultValue": "10",
      "description": "Size of each day cell in pixels."
    },
    {
      "name": "blockGap",
      "type": "number",
      "defaultValue": "3",
      "description": "Gap between day cells in pixels."
    },
    {
      "name": "contributions",
      "type": "Contribution[]",
      "defaultValue": "generated sample year",
      "description": "Contribution entries ({ date, count, level }) to render. When empty, a deterministic sample for the full year is generated."
    },
    {
      "name": "data",
      "type": "Contribution[]",
      "description": "Alias of contributions. Takes precedence when both are set."
    },
    {
      "name": "className",
      "type": "string",
      "defaultValue": "undefined",
      "description": "Additional CSS utility classes applied to the root card wrapper."
    },
    {
      "name": "children",
      "type": "ReactNode",
      "description": "Custom layout built from the ContributionGraph* subcomponents. Replaces the default header, calendar and footer."
    }
  ],
  "usage": "import { GitHubActivity, type Contribution } from \"@/components/ui/github-activity\";\n\nconst data: Contribution[] = [\n  { date: \"2026-01-05\", count: 4, level: 2 },\n  { date: \"2026-01-06\", count: 9, level: 3 },\n];\n\nexport function GitHubActivityDemo() {\n  return <GitHubActivity username=\"octocat\" year={2026} data={data} />;\n}",
  "files": [
    {
      "name": "github-activity.tsx",
      "path": "registry/ui/github-activity.tsx",
      "code": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n  useState,\n  useRef,\n  useMemo,\n  useEffect,\n  createContext,\n  useContext,\n} from \"react\";\nimport { motion, AnimatePresence } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type ContributionLevel = 0 | 1 | 2 | 3 | 4;\n\nexport type Contribution = {\n  date: string;\n  count: number;\n  level: ContributionLevel;\n};\n\nexport type Activity = Contribution;\n\nexport type GitHubActivityVariant = \"emerald\" | \"teal\" | \"github\";\n\nexport interface GitHubActivityProps extends React.HTMLAttributes<HTMLDivElement> {\n  username?: string;\n  title?: string;\n  subtitle?: string;\n  totalContributions?: number;\n  year?: number;\n  contributions?: Contribution[];\n  data?: Contribution[];\n  variant?: GitHubActivityVariant;\n  blockSize?: number;\n  blockGap?: number;\n  className?: string;\n}\n\nconst MONTH_NAMES = [\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 VARIANT_CONFIGS: Record<\n  GitHubActivityVariant,\n  {\n    levels: Record<ContributionLevel, string>;\n    badge: string;\n    dot: string;\n    text: string;\n    glow: string;\n  }\n> = {\n  teal: {\n    levels: {\n      0: \"bg-zinc-200/80 border border-zinc-300/60 dark:bg-white/4 dark:border-white/4\",\n      1: \"bg-[#015451]/35 border border-[#015451]/50\",\n      2: \"bg-[#015451]/65 border border-[#00c9a7]/40\",\n      3: \"bg-[#00a88c] border border-[#00c9a7]/70 shadow-[0_0_8px_rgba(0,201,167,0.35)]\",\n      4: \"bg-[#00c9a7] border border-[#5eead4] shadow-[0_0_12px_rgba(0,201,167,0.65)]\",\n    },\n    badge: \"bg-[#00c9a7]/10 text-[#00c9a7] border border-[#00c9a7]/25\",\n    dot: \"bg-[#00c9a7]\",\n    text: \"text-[#00c9a7]\",\n    glow: \"bg-[#00c9a7]/12\",\n  },\n  emerald: {\n    levels: {\n      0: \"bg-zinc-200/80 border border-zinc-300/60 dark:bg-white/4 dark:border-white/4\",\n      1: \"bg-[#0e4429] border border-[#006d32]/40\",\n      2: \"bg-[#006d32] border border-[#26a641]/50\",\n      3: \"bg-[#26a641] border border-[#39d353]/60 shadow-[0_0_8px_rgba(38,166,65,0.35)]\",\n      4: \"bg-[#39d353] border border-[#4ae365] shadow-[0_0_12px_rgba(57,211,83,0.6)]\",\n    },\n    badge: \"bg-emerald-500/10 text-emerald-400 border border-emerald-500/20\",\n    dot: \"bg-emerald-400\",\n    text: \"text-emerald-400\",\n    glow: \"bg-emerald-500/12\",\n  },\n  github: {\n    levels: {\n      0: \"bg-zinc-200/80 border border-zinc-300/60 dark:bg-white/4 dark:border-white/4\",\n      1: \"bg-[#0e4429] border border-[#006d32]/40\",\n      2: \"bg-[#006d32] border border-[#26a641]/50\",\n      3: \"bg-[#26a641] border border-[#39d353]/60 shadow-[0_0_8px_rgba(38,166,65,0.35)]\",\n      4: \"bg-[#39d353] border border-[#4ae365] shadow-[0_0_12px_rgba(57,211,83,0.6)]\",\n    },\n    badge: \"bg-emerald-500/10 text-emerald-400 border border-emerald-500/20\",\n    dot: \"bg-emerald-400\",\n    text: \"text-emerald-400\",\n    glow: \"bg-emerald-500/12\",\n  },\n};\n\ntype Week = Array<Contribution | undefined>;\n\ntype MonthLabel = {\n  weekIndex: number;\n  label: string;\n};\n\nfunction generateYearData(targetYear: number): Contribution[] {\n  const result: Contribution[] = [];\n  const start = new Date(targetYear, 0, 1);\n  const end = new Date(targetYear, 11, 31);\n\n  const current = new Date(start);\n  while (current <= end) {\n    const year = current.getFullYear();\n    const month = String(current.getMonth() + 1).padStart(2, \"0\");\n    const day = String(current.getDate()).padStart(2, \"0\");\n    const dateStr = `${year}-${month}-${day}`;\n\n    const seed =\n      (targetYear * 365 + (current.getMonth() + 1) * 31 + current.getDate()) *\n      17;\n    const pseudoRand = ((seed * 9301 + 49297) % 233280) / 233280;\n\n    let level: ContributionLevel = 0;\n    let count = 0;\n\n    if (pseudoRand > 0.82) {\n      level = 4;\n      count = 13 + Math.floor(pseudoRand * 12);\n    } else if (pseudoRand > 0.62) {\n      level = 3;\n      count = 7 + Math.floor(pseudoRand * 6);\n    } else if (pseudoRand > 0.42) {\n      level = 2;\n      count = 3 + Math.floor(pseudoRand * 4);\n    } else if (pseudoRand > 0.22) {\n      level = 1;\n      count = 1 + Math.floor(pseudoRand * 2);\n    }\n\n    result.push({\n      date: dateStr,\n      count,\n      level,\n    });\n\n    current.setDate(current.getDate() + 1);\n  }\n\n  return result;\n}\n\nfunction parseDateString(dateStr: string): Date {\n  const [y, m, d] = dateStr.split(\"-\").map(Number);\n  return new Date(y, (m || 1) - 1, d || 1);\n}\n\nfunction formatDateDisplay(dateStr: string): string {\n  const date = parseDateString(dateStr);\n  const m = MONTH_NAMES[date.getMonth()];\n  const d = date.getDate();\n  const y = date.getFullYear();\n  return `${m} ${d}, ${y}`;\n}\n\nfunction groupIntoWeeks(data: Contribution[], weekStart = 0): Week[] {\n  if (!data || data.length === 0) return [];\n\n  const sorted = [...data].sort((a, b) => a.date.localeCompare(b.date));\n  const firstDate = parseDateString(sorted[0].date);\n  const firstDay = firstDate.getDay();\n\n  const leadingPadding = (firstDay - weekStart + 7) % 7;\n  const padded: Array<Contribution | undefined> = [\n    ...new Array(leadingPadding).fill(undefined),\n    ...sorted,\n  ];\n\n  const weeksCount = Math.ceil(padded.length / 7);\n  const weeks: Week[] = [];\n\n  for (let w = 0; w < weeksCount; w++) {\n    const weekSlice = padded.slice(w * 7, w * 7 + 7);\n    while (weekSlice.length < 7) {\n      weekSlice.push(undefined);\n    }\n    weeks.push(weekSlice);\n  }\n\n  return weeks;\n}\n\nfunction calculateMonthLabels(weeks: Week[]): MonthLabel[] {\n  const labels: MonthLabel[] = [];\n\n  weeks.forEach((week, weekIndex) => {\n    const validDay = week.find((day) => day !== undefined);\n    if (!validDay) return;\n\n    const date = parseDateString(validDay.date);\n    const month = MONTH_NAMES[date.getMonth()];\n    const prev = labels[labels.length - 1];\n\n    if (weekIndex === 0 || !prev || prev.label !== month) {\n      labels.push({ weekIndex, label: month });\n    }\n  });\n\n  return labels.filter(({ weekIndex }, index) => {\n    const minWeeks = 2;\n    if (index === 0) {\n      return labels[1] ? labels[1].weekIndex - weekIndex >= minWeeks : true;\n    }\n    if (index === labels.length - 1) {\n      return weeks.length - weekIndex >= minWeeks;\n    }\n    return true;\n  });\n}\n\ntype ContributionGraphContextType = {\n  data: Contribution[];\n  weeks: Week[];\n  monthLabels: MonthLabel[];\n  blockSize: number;\n  blockGap: number;\n  totalContributions: number;\n  year: number;\n  variant: GitHubActivityVariant;\n  activeTheme: (typeof VARIANT_CONFIGS)[\"teal\"];\n  hoveredCell: {\n    count: number;\n    date: string;\n    x: number;\n    y: number;\n    height: number;\n    isNearTop: boolean;\n  } | null;\n  setHoveredCell: React.Dispatch<\n    React.SetStateAction<{\n      count: number;\n      date: string;\n      x: number;\n      y: number;\n      height: number;\n      isNearTop: boolean;\n    } | null>\n  >;\n  containerRef: React.RefObject<HTMLDivElement | null>;\n  scrollRef: React.RefObject<HTMLDivElement | null>;\n};\n\nconst ContributionGraphContext =\n  createContext<ContributionGraphContextType | null>(null);\n\nexport function useContributionGraph() {\n  const context = useContext(ContributionGraphContext);\n  if (!context) {\n    throw new Error(\n      \"ContributionGraph components must be used within a ContributionGraph provider\",\n    );\n  }\n  return context;\n}\n\nexport function ContributionGraph({\n  username,\n  title = \"Commit Heatmap\",\n  subtitle,\n  totalContributions: totalContributionsProp,\n  year = new Date().getFullYear(),\n  contributions,\n  data: dataProp,\n  variant = \"teal\",\n  blockSize = 10,\n  blockGap = 3,\n  className,\n  children,\n  ...props\n}: GitHubActivityProps & { children?: React.ReactNode }) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const scrollRef = useRef<HTMLDivElement>(null);\n\n  const [hoveredCell, setHoveredCell] = useState<{\n    count: number;\n    date: string;\n    x: number;\n    y: number;\n    height: number;\n    isNearTop: boolean;\n  } | null>(null);\n\n  const rawData = dataProp || contributions;\n  const data = useMemo(() => {\n    if (rawData && rawData.length > 0) {\n      return rawData;\n    }\n    return generateYearData(year);\n  }, [rawData, year]);\n\n  const weeks = useMemo(() => groupIntoWeeks(data, 0), [data]);\n  const monthLabels = useMemo(() => calculateMonthLabels(weeks), [weeks]);\n\n  const calculatedTotal = useMemo(\n    () => data.reduce((sum, item) => sum + item.count, 0),\n    [data],\n  );\n\n  const totalContributions =\n    typeof totalContributionsProp === \"number\"\n      ? totalContributionsProp\n      : calculatedTotal;\n\n  const activeTheme = VARIANT_CONFIGS[variant] || VARIANT_CONFIGS.teal;\n\n  useEffect(() => {\n    if (scrollRef.current && typeof window !== \"undefined\") {\n      if (window.innerWidth < 768) {\n        scrollRef.current.scrollLeft = scrollRef.current.scrollWidth;\n      }\n    }\n  }, [weeks.length]);\n\n  return (\n    <ContributionGraphContext.Provider\n      value={{\n        data,\n        weeks,\n        monthLabels,\n        blockSize,\n        blockGap,\n        totalContributions,\n        year,\n        variant,\n        activeTheme,\n        hoveredCell,\n        setHoveredCell,\n        containerRef,\n        scrollRef,\n      }}\n    >\n      <div\n        ref={containerRef}\n        className={cn(\n          \"relative w-full max-w-full sm:max-w-3xl rounded-2xl border border-zinc-200 bg-white/90 p-4 sm:p-6 backdrop-blur-xl shadow-[0_20px_48px_-10px_rgba(0,0,0,0.06),inset_0_1px_0_0_rgba(255,255,255,0.8)] dark:border-white/10 dark:bg-zinc-950/75 dark:shadow-[0_20px_48px_-10px_rgba(0,0,0,0.7),inset_0_1px_0_0_rgba(255,255,255,0.14)] select-none\",\n          className,\n        )}\n        {...props}\n      >\n        {children || (\n          <>\n            <div className=\"relative z-10 flex flex-col pb-3 sm:pb-4 border-b border-zinc-200/80 dark:border-white/6\">\n              <span className=\"text-xs sm:text-sm font-semibold text-zinc-900 dark:text-zinc-100 tracking-tight\">\n                {title}\n              </span>\n              <span className=\"text-[11px] sm:text-xs text-zinc-500 dark:text-zinc-400 font-normal\">\n                {subtitle ||\n                  (username ? `@${username}` : \"GitHub Contribution Matrix\")}\n              </span>\n            </div>\n\n            <ContributionGraphCalendar />\n\n            <ContributionGraphTooltip />\n\n            <ContributionGraphFooter>\n              <ContributionGraphTotalCount />\n              <ContributionGraphLegend />\n            </ContributionGraphFooter>\n          </>\n        )}\n      </div>\n    </ContributionGraphContext.Provider>\n  );\n}\n\nexport function ContributionGraphTooltip() {\n  const { hoveredCell, activeTheme } = useContributionGraph();\n\n  return (\n    <AnimatePresence>\n      {hoveredCell && (\n        <motion.div\n          key=\"activity-tooltip\"\n          initial={{\n            opacity: 0,\n            scale: 0.92,\n            x: hoveredCell.x,\n            y: hoveredCell.isNearTop\n              ? hoveredCell.y + hoveredCell.height + 4\n              : hoveredCell.y - 4,\n          }}\n          animate={{\n            opacity: 1,\n            scale: 1,\n            x: hoveredCell.x,\n            y: hoveredCell.isNearTop\n              ? hoveredCell.y + hoveredCell.height + 8\n              : hoveredCell.y - 8,\n          }}\n          exit={{\n            opacity: 0,\n            scale: 0.92,\n            transition: { duration: 0.12, ease: \"easeOut\" },\n          }}\n          transition={{\n            type: \"spring\",\n            stiffness: 480,\n            damping: 32,\n            mass: 0.5,\n          }}\n          style={{\n            left: 0,\n            top: 0,\n            translateX: \"-50%\",\n            translateY: hoveredCell.isNearTop ? \"0%\" : \"-100%\",\n          }}\n          className=\"pointer-events-none absolute z-50 px-3 py-1.5 rounded-lg bg-zinc-900/95 backdrop-blur-md text-xs font-medium text-white border border-white/15 shadow-2xl whitespace-nowrap\"\n        >\n          <div\n            className={cn(\n              \"absolute left-1/2 -translate-x-1/2 w-0 h-0 border-x-4 border-x-transparent\",\n              hoveredCell.isNearTop\n                ? \"bottom-full border-b-4 border-b-zinc-900\"\n                : \"top-full border-t-4 border-t-zinc-900\",\n            )}\n          />\n          <div className=\"flex items-center gap-1.5 font-medium\">\n            <AnimatePresence mode=\"popLayout\" initial={false}>\n              <motion.span\n                key={hoveredCell.count}\n                initial={{ opacity: 0, y: -3 }}\n                animate={{ opacity: 1, y: 0 }}\n                exit={{ opacity: 0, y: 3 }}\n                transition={{ duration: 0.14, ease: \"easeOut\" }}\n                className={cn(\"font-bold tabular-nums\", activeTheme.text)}\n              >\n                {hoveredCell.count} commits\n              </motion.span>\n            </AnimatePresence>\n            <span className=\"text-zinc-400\">on</span>\n            <AnimatePresence mode=\"popLayout\" initial={false}>\n              <motion.span\n                key={hoveredCell.date}\n                initial={{ opacity: 0, y: -2 }}\n                animate={{ opacity: 1, y: 0 }}\n                exit={{ opacity: 0, y: 2 }}\n                transition={{ duration: 0.14, ease: \"easeOut\" }}\n                className=\"text-zinc-200 tabular-nums\"\n              >\n                {formatDateDisplay(hoveredCell.date)}\n              </motion.span>\n            </AnimatePresence>\n          </div>\n        </motion.div>\n      )}\n    </AnimatePresence>\n  );\n}\n\nexport function ContributionGraphCalendar({\n  className,\n  children,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement> & {\n  children?: (props: {\n    activity: Contribution | undefined;\n    dayIndex: number;\n    weekIndex: number;\n  }) => React.ReactNode;\n}) {\n  const { weeks, monthLabels, blockSize, blockGap, scrollRef, setHoveredCell } =\n    useContributionGraph();\n\n  const totalWidth = weeks.length * (blockSize + blockGap) - blockGap;\n\n  return (\n    <div\n      ref={scrollRef}\n      onScroll={() => setHoveredCell(null)}\n      className={cn(\n        \"relative z-10 overflow-x-auto scrollbar-none pb-2 pt-2.5 -mx-1 px-1 sm:mx-0 sm:px-0\",\n        className,\n      )}\n      {...props}\n    >\n      <div\n        className=\"inline-flex flex-col select-none\"\n        style={{ width: `${totalWidth}px` }}\n      >\n        <div\n          className=\"relative h-4 mb-2 text-[10px] sm:text-[11px] font-medium text-zinc-500 dark:text-zinc-400\"\n          style={{ width: `${totalWidth}px` }}\n        >\n          {monthLabels.map(({ label, weekIndex }) => (\n            <span\n              key={`${label}-${weekIndex}`}\n              className=\"absolute top-0 transition-opacity\"\n              style={{\n                left: `${weekIndex * (blockSize + blockGap)}px`,\n              }}\n            >\n              {label}\n            </span>\n          ))}\n        </div>\n\n        <div className=\"flex\" style={{ gap: `${blockGap}px` }}>\n          {weeks.map((week, weekIndex) => (\n            <div\n              key={weekIndex}\n              className=\"flex flex-col\"\n              style={{ gap: `${blockGap}px` }}\n            >\n              {week.map((activity, dayIndex) => {\n                if (children) {\n                  return (\n                    <React.Fragment key={`${weekIndex}-${dayIndex}`}>\n                      {children({ activity, dayIndex, weekIndex })}\n                    </React.Fragment>\n                  );\n                }\n\n                return (\n                  <ContributionGraphBlock\n                    key={`${weekIndex}-${dayIndex}`}\n                    activity={activity}\n                    dayIndex={dayIndex}\n                    weekIndex={weekIndex}\n                  />\n                );\n              })}\n            </div>\n          ))}\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport function ContributionGraphBlock({\n  activity,\n  className,\n  dayIndex: _dayIndex,\n  weekIndex: _weekIndex,\n  ...props\n}: {\n  activity?: Contribution;\n  dayIndex?: number;\n  weekIndex?: number;\n} & React.HTMLAttributes<HTMLDivElement>) {\n  const { activeTheme, blockSize, setHoveredCell, containerRef } =\n    useContributionGraph();\n\n  if (!activity) {\n    return (\n      <div\n        style={{ width: `${blockSize}px`, height: `${blockSize}px` }}\n        className=\"pointer-events-none opacity-0\"\n      />\n    );\n  }\n\n  const activateTooltip = (target: HTMLDivElement) => {\n    const rect = target.getBoundingClientRect();\n    const containerRect = containerRef.current?.getBoundingClientRect();\n    const containerWidth = containerRect?.width || 560;\n    const rawX = containerRect\n      ? rect.left - containerRect.left + rect.width / 2\n      : 0;\n    const clampedX = Math.max(75, Math.min(containerWidth - 75, rawX));\n    const y = containerRect ? rect.top - containerRect.top : 0;\n\n    setHoveredCell({\n      count: activity.count,\n      date: activity.date,\n      x: clampedX,\n      y,\n      height: rect.height,\n      isNearTop: y < 65,\n    });\n  };\n\n  return (\n    <div\n      onMouseEnter={(e) => activateTooltip(e.currentTarget)}\n      onMouseLeave={() => setHoveredCell(null)}\n      onClick={(e) => activateTooltip(e.currentTarget)}\n      style={{ width: `${blockSize}px`, height: `${blockSize}px` }}\n      className={cn(\n        \"rounded-[2.5px] sm:rounded-[3px] transition-all duration-150 cursor-pointer\",\n        \"hover:scale-125 hover:z-20 hover:ring-1 hover:ring-white/50\",\n        activeTheme.levels[activity.level],\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function ContributionGraphFooter({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) {\n  return (\n    <div\n      className={cn(\n        \"relative z-10 flex flex-wrap items-center justify-between gap-2 pt-3 sm:pt-3.5 border-t border-zinc-200/80 text-[11px] sm:text-xs text-zinc-500 dark:border-white/6 dark:text-zinc-400\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function ContributionGraphTotalCount({\n  className,\n  children,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement> & {\n  children?: (props: { totalCount: number; year: number }) => React.ReactNode;\n}) {\n  const { totalContributions, year } = useContributionGraph();\n\n  if (children) {\n    return <>{children({ totalCount: totalContributions, year })}</>;\n  }\n\n  return (\n    <div\n      className={cn(\"font-medium text-zinc-600 dark:text-zinc-400\", className)}\n      {...props}\n    >\n      {totalContributions.toLocaleString()} contributions in {year}\n    </div>\n  );\n}\n\nexport function ContributionGraphLegend({\n  className,\n  children,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement> & {\n  children?: (props: { level: ContributionLevel }) => React.ReactNode;\n}) {\n  const { activeTheme, blockSize } = useContributionGraph();\n  const levels: ContributionLevel[] = [0, 1, 2, 3, 4];\n\n  if (children) {\n    return (\n      <div className={cn(\"flex items-center gap-1.5 sm:gap-2\", className)}>\n        {levels.map((level) => (\n          <React.Fragment key={level}>{children({ level })}</React.Fragment>\n        ))}\n      </div>\n    );\n  }\n\n  return (\n    <div\n      className={cn(\n        \"flex items-center gap-1.5 sm:gap-2 text-[11px] sm:text-xs text-zinc-500 dark:text-zinc-400\",\n        className,\n      )}\n      {...props}\n    >\n      <span>Less</span>\n      {levels.map((level) => (\n        <div\n          key={level}\n          style={{ width: `${blockSize}px`, height: `${blockSize}px` }}\n          className={cn(\n            \"rounded-[2.5px] sm:rounded-[3px]\",\n            activeTheme.levels[level],\n          )}\n        />\n      ))}\n      <span>More</span>\n    </div>\n  );\n}\n\nexport { ContributionGraph as GitHubActivity };\nexport default ContributionGraph;\n",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n  useState,\n  useRef,\n  useMemo,\n  useEffect,\n  createContext,\n  useContext,\n} from \"react\";\nimport { motion, AnimatePresence } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type ContributionLevel = 0 | 1 | 2 | 3 | 4;\n\nexport type Contribution = {\n  date: string;\n  count: number;\n  level: ContributionLevel;\n};\n\nexport type Activity = Contribution;\n\nexport type GitHubActivityVariant = \"emerald\" | \"teal\" | \"github\";\n\nexport interface GitHubActivityProps extends React.HTMLAttributes<HTMLDivElement> {\n  username?: string;\n  title?: string;\n  subtitle?: string;\n  totalContributions?: number;\n  year?: number;\n  contributions?: Contribution[];\n  data?: Contribution[];\n  variant?: GitHubActivityVariant;\n  blockSize?: number;\n  blockGap?: number;\n  className?: string;\n}\n\nconst MONTH_NAMES = [\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 VARIANT_CONFIGS: Record<\n  GitHubActivityVariant,\n  {\n    levels: Record<ContributionLevel, string>;\n    badge: string;\n    dot: string;\n    text: string;\n    glow: string;\n  }\n> = {\n  teal: {\n    levels: {\n      0: \"bg-zinc-200/80 border border-zinc-300/60 dark:bg-white/4 dark:border-white/4\",\n      1: \"bg-[#015451]/35 border border-[#015451]/50\",\n      2: \"bg-[#015451]/65 border border-[#00c9a7]/40\",\n      3: \"bg-[#00a88c] border border-[#00c9a7]/70 shadow-[0_0_8px_rgba(0,201,167,0.35)]\",\n      4: \"bg-[#00c9a7] border border-[#5eead4] shadow-[0_0_12px_rgba(0,201,167,0.65)]\",\n    },\n    badge: \"bg-[#00c9a7]/10 text-[#00c9a7] border border-[#00c9a7]/25\",\n    dot: \"bg-[#00c9a7]\",\n    text: \"text-[#00c9a7]\",\n    glow: \"bg-[#00c9a7]/12\",\n  },\n  emerald: {\n    levels: {\n      0: \"bg-zinc-200/80 border border-zinc-300/60 dark:bg-white/4 dark:border-white/4\",\n      1: \"bg-[#0e4429] border border-[#006d32]/40\",\n      2: \"bg-[#006d32] border border-[#26a641]/50\",\n      3: \"bg-[#26a641] border border-[#39d353]/60 shadow-[0_0_8px_rgba(38,166,65,0.35)]\",\n      4: \"bg-[#39d353] border border-[#4ae365] shadow-[0_0_12px_rgba(57,211,83,0.6)]\",\n    },\n    badge: \"bg-emerald-500/10 text-emerald-400 border border-emerald-500/20\",\n    dot: \"bg-emerald-400\",\n    text: \"text-emerald-400\",\n    glow: \"bg-emerald-500/12\",\n  },\n  github: {\n    levels: {\n      0: \"bg-zinc-200/80 border border-zinc-300/60 dark:bg-white/4 dark:border-white/4\",\n      1: \"bg-[#0e4429] border border-[#006d32]/40\",\n      2: \"bg-[#006d32] border border-[#26a641]/50\",\n      3: \"bg-[#26a641] border border-[#39d353]/60 shadow-[0_0_8px_rgba(38,166,65,0.35)]\",\n      4: \"bg-[#39d353] border border-[#4ae365] shadow-[0_0_12px_rgba(57,211,83,0.6)]\",\n    },\n    badge: \"bg-emerald-500/10 text-emerald-400 border border-emerald-500/20\",\n    dot: \"bg-emerald-400\",\n    text: \"text-emerald-400\",\n    glow: \"bg-emerald-500/12\",\n  },\n};\n\ntype Week = Array<Contribution | undefined>;\n\ntype MonthLabel = {\n  weekIndex: number;\n  label: string;\n};\n\nfunction generateYearData(targetYear: number): Contribution[] {\n  const result: Contribution[] = [];\n  const start = new Date(targetYear, 0, 1);\n  const end = new Date(targetYear, 11, 31);\n\n  const current = new Date(start);\n  while (current <= end) {\n    const year = current.getFullYear();\n    const month = String(current.getMonth() + 1).padStart(2, \"0\");\n    const day = String(current.getDate()).padStart(2, \"0\");\n    const dateStr = `${year}-${month}-${day}`;\n\n    const seed =\n      (targetYear * 365 + (current.getMonth() + 1) * 31 + current.getDate()) *\n      17;\n    const pseudoRand = ((seed * 9301 + 49297) % 233280) / 233280;\n\n    let level: ContributionLevel = 0;\n    let count = 0;\n\n    if (pseudoRand > 0.82) {\n      level = 4;\n      count = 13 + Math.floor(pseudoRand * 12);\n    } else if (pseudoRand > 0.62) {\n      level = 3;\n      count = 7 + Math.floor(pseudoRand * 6);\n    } else if (pseudoRand > 0.42) {\n      level = 2;\n      count = 3 + Math.floor(pseudoRand * 4);\n    } else if (pseudoRand > 0.22) {\n      level = 1;\n      count = 1 + Math.floor(pseudoRand * 2);\n    }\n\n    result.push({\n      date: dateStr,\n      count,\n      level,\n    });\n\n    current.setDate(current.getDate() + 1);\n  }\n\n  return result;\n}\n\nfunction parseDateString(dateStr: string): Date {\n  const [y, m, d] = dateStr.split(\"-\").map(Number);\n  return new Date(y, (m || 1) - 1, d || 1);\n}\n\nfunction formatDateDisplay(dateStr: string): string {\n  const date = parseDateString(dateStr);\n  const m = MONTH_NAMES[date.getMonth()];\n  const d = date.getDate();\n  const y = date.getFullYear();\n  return `${m} ${d}, ${y}`;\n}\n\nfunction groupIntoWeeks(data: Contribution[], weekStart = 0): Week[] {\n  if (!data || data.length === 0) return [];\n\n  const sorted = [...data].sort((a, b) => a.date.localeCompare(b.date));\n  const firstDate = parseDateString(sorted[0].date);\n  const firstDay = firstDate.getDay();\n\n  const leadingPadding = (firstDay - weekStart + 7) % 7;\n  const padded: Array<Contribution | undefined> = [\n    ...new Array(leadingPadding).fill(undefined),\n    ...sorted,\n  ];\n\n  const weeksCount = Math.ceil(padded.length / 7);\n  const weeks: Week[] = [];\n\n  for (let w = 0; w < weeksCount; w++) {\n    const weekSlice = padded.slice(w * 7, w * 7 + 7);\n    while (weekSlice.length < 7) {\n      weekSlice.push(undefined);\n    }\n    weeks.push(weekSlice);\n  }\n\n  return weeks;\n}\n\nfunction calculateMonthLabels(weeks: Week[]): MonthLabel[] {\n  const labels: MonthLabel[] = [];\n\n  weeks.forEach((week, weekIndex) => {\n    const validDay = week.find((day) => day !== undefined);\n    if (!validDay) return;\n\n    const date = parseDateString(validDay.date);\n    const month = MONTH_NAMES[date.getMonth()];\n    const prev = labels[labels.length - 1];\n\n    if (weekIndex === 0 || !prev || prev.label !== month) {\n      labels.push({ weekIndex, label: month });\n    }\n  });\n\n  return labels.filter(({ weekIndex }, index) => {\n    const minWeeks = 2;\n    if (index === 0) {\n      return labels[1] ? labels[1].weekIndex - weekIndex >= minWeeks : true;\n    }\n    if (index === labels.length - 1) {\n      return weeks.length - weekIndex >= minWeeks;\n    }\n    return true;\n  });\n}\n\ntype ContributionGraphContextType = {\n  data: Contribution[];\n  weeks: Week[];\n  monthLabels: MonthLabel[];\n  blockSize: number;\n  blockGap: number;\n  totalContributions: number;\n  year: number;\n  variant: GitHubActivityVariant;\n  activeTheme: (typeof VARIANT_CONFIGS)[\"teal\"];\n  hoveredCell: {\n    count: number;\n    date: string;\n    x: number;\n    y: number;\n    height: number;\n    isNearTop: boolean;\n  } | null;\n  setHoveredCell: React.Dispatch<\n    React.SetStateAction<{\n      count: number;\n      date: string;\n      x: number;\n      y: number;\n      height: number;\n      isNearTop: boolean;\n    } | null>\n  >;\n  containerRef: React.RefObject<HTMLDivElement | null>;\n  scrollRef: React.RefObject<HTMLDivElement | null>;\n};\n\nconst ContributionGraphContext =\n  createContext<ContributionGraphContextType | null>(null);\n\nexport function useContributionGraph() {\n  const context = useContext(ContributionGraphContext);\n  if (!context) {\n    throw new Error(\n      \"ContributionGraph components must be used within a ContributionGraph provider\",\n    );\n  }\n  return context;\n}\n\nexport function ContributionGraph({\n  username,\n  title = \"Commit Heatmap\",\n  subtitle,\n  totalContributions: totalContributionsProp,\n  year = new Date().getFullYear(),\n  contributions,\n  data: dataProp,\n  variant = \"teal\",\n  blockSize = 10,\n  blockGap = 3,\n  className,\n  children,\n  ...props\n}: GitHubActivityProps & { children?: React.ReactNode }) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const scrollRef = useRef<HTMLDivElement>(null);\n\n  const [hoveredCell, setHoveredCell] = useState<{\n    count: number;\n    date: string;\n    x: number;\n    y: number;\n    height: number;\n    isNearTop: boolean;\n  } | null>(null);\n\n  const rawData = dataProp || contributions;\n  const data = useMemo(() => {\n    if (rawData && rawData.length > 0) {\n      return rawData;\n    }\n    return generateYearData(year);\n  }, [rawData, year]);\n\n  const weeks = useMemo(() => groupIntoWeeks(data, 0), [data]);\n  const monthLabels = useMemo(() => calculateMonthLabels(weeks), [weeks]);\n\n  const calculatedTotal = useMemo(\n    () => data.reduce((sum, item) => sum + item.count, 0),\n    [data],\n  );\n\n  const totalContributions =\n    typeof totalContributionsProp === \"number\"\n      ? totalContributionsProp\n      : calculatedTotal;\n\n  const activeTheme = VARIANT_CONFIGS[variant] || VARIANT_CONFIGS.teal;\n\n  useEffect(() => {\n    if (scrollRef.current && typeof window !== \"undefined\") {\n      if (window.innerWidth < 768) {\n        scrollRef.current.scrollLeft = scrollRef.current.scrollWidth;\n      }\n    }\n  }, [weeks.length]);\n\n  return (\n    <ContributionGraphContext.Provider\n      value={{\n        data,\n        weeks,\n        monthLabels,\n        blockSize,\n        blockGap,\n        totalContributions,\n        year,\n        variant,\n        activeTheme,\n        hoveredCell,\n        setHoveredCell,\n        containerRef,\n        scrollRef,\n      }}\n    >\n      <div\n        ref={containerRef}\n        className={cn(\n          \"relative w-full max-w-full sm:max-w-3xl rounded-2xl border border-zinc-200 bg-white/90 p-4 sm:p-6 backdrop-blur-xl shadow-[0_20px_48px_-10px_rgba(0,0,0,0.06),inset_0_1px_0_0_rgba(255,255,255,0.8)] dark:border-white/10 dark:bg-zinc-950/75 dark:shadow-[0_20px_48px_-10px_rgba(0,0,0,0.7),inset_0_1px_0_0_rgba(255,255,255,0.14)] select-none\",\n          className,\n        )}\n        {...props}\n      >\n        {children || (\n          <>\n            <div className=\"relative z-10 flex flex-col pb-3 sm:pb-4 border-b border-zinc-200/80 dark:border-white/6\">\n              <span className=\"text-xs sm:text-sm font-semibold text-zinc-900 dark:text-zinc-100 tracking-tight\">\n                {title}\n              </span>\n              <span className=\"text-[11px] sm:text-xs text-zinc-500 dark:text-zinc-400 font-normal\">\n                {subtitle ||\n                  (username ? `@${username}` : \"GitHub Contribution Matrix\")}\n              </span>\n            </div>\n\n            <ContributionGraphCalendar />\n\n            <ContributionGraphTooltip />\n\n            <ContributionGraphFooter>\n              <ContributionGraphTotalCount />\n              <ContributionGraphLegend />\n            </ContributionGraphFooter>\n          </>\n        )}\n      </div>\n    </ContributionGraphContext.Provider>\n  );\n}\n\nexport function ContributionGraphTooltip() {\n  const { hoveredCell, activeTheme } = useContributionGraph();\n\n  return (\n    <AnimatePresence>\n      {hoveredCell && (\n        <motion.div\n          key=\"activity-tooltip\"\n          initial={{\n            opacity: 0,\n            scale: 0.92,\n            x: hoveredCell.x,\n            y: hoveredCell.isNearTop\n              ? hoveredCell.y + hoveredCell.height + 4\n              : hoveredCell.y - 4,\n          }}\n          animate={{\n            opacity: 1,\n            scale: 1,\n            x: hoveredCell.x,\n            y: hoveredCell.isNearTop\n              ? hoveredCell.y + hoveredCell.height + 8\n              : hoveredCell.y - 8,\n          }}\n          exit={{\n            opacity: 0,\n            scale: 0.92,\n            transition: { duration: 0.12, ease: \"easeOut\" },\n          }}\n          transition={{\n            type: \"spring\",\n            stiffness: 480,\n            damping: 32,\n            mass: 0.5,\n          }}\n          style={{\n            left: 0,\n            top: 0,\n            translateX: \"-50%\",\n            translateY: hoveredCell.isNearTop ? \"0%\" : \"-100%\",\n          }}\n          className=\"pointer-events-none absolute z-50 px-3 py-1.5 rounded-lg bg-zinc-900/95 backdrop-blur-md text-xs font-medium text-white border border-white/15 shadow-2xl whitespace-nowrap\"\n        >\n          <div\n            className={cn(\n              \"absolute left-1/2 -translate-x-1/2 w-0 h-0 border-x-4 border-x-transparent\",\n              hoveredCell.isNearTop\n                ? \"bottom-full border-b-4 border-b-zinc-900\"\n                : \"top-full border-t-4 border-t-zinc-900\",\n            )}\n          />\n          <div className=\"flex items-center gap-1.5 font-medium\">\n            <AnimatePresence mode=\"popLayout\" initial={false}>\n              <motion.span\n                key={hoveredCell.count}\n                initial={{ opacity: 0, y: -3 }}\n                animate={{ opacity: 1, y: 0 }}\n                exit={{ opacity: 0, y: 3 }}\n                transition={{ duration: 0.14, ease: \"easeOut\" }}\n                className={cn(\"font-bold tabular-nums\", activeTheme.text)}\n              >\n                {hoveredCell.count} commits\n              </motion.span>\n            </AnimatePresence>\n            <span className=\"text-zinc-400\">on</span>\n            <AnimatePresence mode=\"popLayout\" initial={false}>\n              <motion.span\n                key={hoveredCell.date}\n                initial={{ opacity: 0, y: -2 }}\n                animate={{ opacity: 1, y: 0 }}\n                exit={{ opacity: 0, y: 2 }}\n                transition={{ duration: 0.14, ease: \"easeOut\" }}\n                className=\"text-zinc-200 tabular-nums\"\n              >\n                {formatDateDisplay(hoveredCell.date)}\n              </motion.span>\n            </AnimatePresence>\n          </div>\n        </motion.div>\n      )}\n    </AnimatePresence>\n  );\n}\n\nexport function ContributionGraphCalendar({\n  className,\n  children,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement> & {\n  children?: (props: {\n    activity: Contribution | undefined;\n    dayIndex: number;\n    weekIndex: number;\n  }) => React.ReactNode;\n}) {\n  const { weeks, monthLabels, blockSize, blockGap, scrollRef, setHoveredCell } =\n    useContributionGraph();\n\n  const totalWidth = weeks.length * (blockSize + blockGap) - blockGap;\n\n  return (\n    <div\n      ref={scrollRef}\n      onScroll={() => setHoveredCell(null)}\n      className={cn(\n        \"relative z-10 overflow-x-auto scrollbar-none pb-2 pt-2.5 -mx-1 px-1 sm:mx-0 sm:px-0\",\n        className,\n      )}\n      {...props}\n    >\n      <div\n        className=\"inline-flex flex-col select-none\"\n        style={{ width: `${totalWidth}px` }}\n      >\n        <div\n          className=\"relative h-4 mb-2 text-[10px] sm:text-[11px] font-medium text-zinc-500 dark:text-zinc-400\"\n          style={{ width: `${totalWidth}px` }}\n        >\n          {monthLabels.map(({ label, weekIndex }) => (\n            <span\n              key={`${label}-${weekIndex}`}\n              className=\"absolute top-0 transition-opacity\"\n              style={{\n                left: `${weekIndex * (blockSize + blockGap)}px`,\n              }}\n            >\n              {label}\n            </span>\n          ))}\n        </div>\n\n        <div className=\"flex\" style={{ gap: `${blockGap}px` }}>\n          {weeks.map((week, weekIndex) => (\n            <div\n              key={weekIndex}\n              className=\"flex flex-col\"\n              style={{ gap: `${blockGap}px` }}\n            >\n              {week.map((activity, dayIndex) => {\n                if (children) {\n                  return (\n                    <React.Fragment key={`${weekIndex}-${dayIndex}`}>\n                      {children({ activity, dayIndex, weekIndex })}\n                    </React.Fragment>\n                  );\n                }\n\n                return (\n                  <ContributionGraphBlock\n                    key={`${weekIndex}-${dayIndex}`}\n                    activity={activity}\n                    dayIndex={dayIndex}\n                    weekIndex={weekIndex}\n                  />\n                );\n              })}\n            </div>\n          ))}\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport function ContributionGraphBlock({\n  activity,\n  className,\n  dayIndex: _dayIndex,\n  weekIndex: _weekIndex,\n  ...props\n}: {\n  activity?: Contribution;\n  dayIndex?: number;\n  weekIndex?: number;\n} & React.HTMLAttributes<HTMLDivElement>) {\n  const { activeTheme, blockSize, setHoveredCell, containerRef } =\n    useContributionGraph();\n\n  if (!activity) {\n    return (\n      <div\n        style={{ width: `${blockSize}px`, height: `${blockSize}px` }}\n        className=\"pointer-events-none opacity-0\"\n      />\n    );\n  }\n\n  const activateTooltip = (target: HTMLDivElement) => {\n    const rect = target.getBoundingClientRect();\n    const containerRect = containerRef.current?.getBoundingClientRect();\n    const containerWidth = containerRect?.width || 560;\n    const rawX = containerRect\n      ? rect.left - containerRect.left + rect.width / 2\n      : 0;\n    const clampedX = Math.max(75, Math.min(containerWidth - 75, rawX));\n    const y = containerRect ? rect.top - containerRect.top : 0;\n\n    setHoveredCell({\n      count: activity.count,\n      date: activity.date,\n      x: clampedX,\n      y,\n      height: rect.height,\n      isNearTop: y < 65,\n    });\n  };\n\n  return (\n    <div\n      onMouseEnter={(e) => activateTooltip(e.currentTarget)}\n      onMouseLeave={() => setHoveredCell(null)}\n      onClick={(e) => activateTooltip(e.currentTarget)}\n      style={{ width: `${blockSize}px`, height: `${blockSize}px` }}\n      className={cn(\n        \"rounded-[2.5px] sm:rounded-[3px] transition-all duration-150 cursor-pointer\",\n        \"hover:scale-125 hover:z-20 hover:ring-1 hover:ring-white/50\",\n        activeTheme.levels[activity.level],\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function ContributionGraphFooter({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) {\n  return (\n    <div\n      className={cn(\n        \"relative z-10 flex flex-wrap items-center justify-between gap-2 pt-3 sm:pt-3.5 border-t border-zinc-200/80 text-[11px] sm:text-xs text-zinc-500 dark:border-white/6 dark:text-zinc-400\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function ContributionGraphTotalCount({\n  className,\n  children,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement> & {\n  children?: (props: { totalCount: number; year: number }) => React.ReactNode;\n}) {\n  const { totalContributions, year } = useContributionGraph();\n\n  if (children) {\n    return <>{children({ totalCount: totalContributions, year })}</>;\n  }\n\n  return (\n    <div\n      className={cn(\"font-medium text-zinc-600 dark:text-zinc-400\", className)}\n      {...props}\n    >\n      {totalContributions.toLocaleString()} contributions in {year}\n    </div>\n  );\n}\n\nexport function ContributionGraphLegend({\n  className,\n  children,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement> & {\n  children?: (props: { level: ContributionLevel }) => React.ReactNode;\n}) {\n  const { activeTheme, blockSize } = useContributionGraph();\n  const levels: ContributionLevel[] = [0, 1, 2, 3, 4];\n\n  if (children) {\n    return (\n      <div className={cn(\"flex items-center gap-1.5 sm:gap-2\", className)}>\n        {levels.map((level) => (\n          <React.Fragment key={level}>{children({ level })}</React.Fragment>\n        ))}\n      </div>\n    );\n  }\n\n  return (\n    <div\n      className={cn(\n        \"flex items-center gap-1.5 sm:gap-2 text-[11px] sm:text-xs text-zinc-500 dark:text-zinc-400\",\n        className,\n      )}\n      {...props}\n    >\n      <span>Less</span>\n      {levels.map((level) => (\n        <div\n          key={level}\n          style={{ width: `${blockSize}px`, height: `${blockSize}px` }}\n          className={cn(\n            \"rounded-[2.5px] sm:rounded-[3px]\",\n            activeTheme.levels[level],\n          )}\n        />\n      ))}\n      <span>More</span>\n    </div>\n  );\n}\n\nexport { ContributionGraph as GitHubActivity };\nexport default ContributionGraph;\n",
      "type": "registry:ui",
      "target": "@ui/github-activity.tsx"
    }
  ],
  "type": "registry:ui",
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "title": "GitHub Activity"
}
