{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "github-activity",
  "type": "registry:ui",
  "title": "GitHub Activity",
  "description": "A glass-style contribution heatmap with hover tooltips, three color variants and an activity legend.",
  "dependencies": [
    "clsx",
    "tailwind-merge",
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/registry/ui/github-activity.tsx",
      "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"
    }
  ]
}
