'use client'; import React, { useCallback, useEffect, useState } from 'react'; import { cva, type VariantProps } from 'class-variance-authority'; import { Star } from 'lucide-react'; import { motion, useInView, type SpringOptions, type UseInViewOptions } from 'motion/react'; import { cn } from '@/lib/utils'; const githubButtonVariants = cva( 'cursor-pointer relative overflow-hidden will-change-transform backface-visibility-hidden transform-gpu group whitespace-nowrap focus-visible:outline-hidden inline-flex items-center justify-center whitespace-nowrap font-medium ring-offset-background disabled:pointer-events-none disabled:opacity-60 [&_svg]:shrink-0', { variants: { variant: { default: 'bg-zinc-950 hover:bg-zinc-900 text-white border-gray-700 dark:bg-zinc-50 dark:border-gray-300 dark:text-zinc-950 dark:hover:bg-zinc-50', outline: 'bg-background text-accent-foreground border border-input hover:bg-accent', }, size: { default: 'h-8.5 rounded-md px-3 gap-2 text-[0.8125rem] leading-none [&_svg]:size-4 gap-2', sm: 'h-7 rounded-md px-2.5 gap-1.5 text-xs leading-none [&_svg]:size-3.5 gap-1.5', lg: 'h-10 rounded-md px-4 gap-2.5 text-sm leading-none [&_svg]:size-5 gap-2.5', }, }, defaultVariants: { variant: 'default', size: 'default', }, }, ); interface GithubButtonProps extends React.ComponentProps<'button'>, VariantProps { /** Whether to round stars */ roundStars?: boolean; /** Whether to show Github icon */ fixedWidth?: boolean; /** Initial number of stars */ initialStars?: number; /** Class for stars */ starsClass?: string; /** Target number of stars to animate to */ targetStars?: number; /** Animation duration in seconds */ animationDuration?: number; /** Animation delay in seconds */ animationDelay?: number; /** Whether to start animation automatically */ autoAnimate?: boolean; /** Callback when animation completes */ onAnimationComplete?: () => void; /** Whether to show Github icon */ showGithubIcon?: boolean; /** Whether to show star icon */ showStarIcon?: boolean; /** Whether to show separator */ separator?: boolean; /** Whether stars should be filled */ filled?: boolean; /** Repository URL for actual Github integration */ repoUrl?: string; /** Button text label */ label?: string; /** Use in-view detection to trigger animation */ useInViewTrigger?: boolean; /** In-view options */ inViewOptions?: UseInViewOptions; /** Spring transition options */ transition?: SpringOptions; } function GithubButton({ initialStars = 0, targetStars = 0, starsClass = '', fixedWidth = true, animationDuration = 2, animationDelay = 0, autoAnimate = true, className, variant = 'default', size = 'default', showGithubIcon = true, showStarIcon = true, roundStars = false, separator = false, filled = false, repoUrl, onClick, label = '', useInViewTrigger = false, inViewOptions = { once: true }, transition, ...props }: GithubButtonProps) { const [currentStars, setCurrentStars] = useState(initialStars); const [isAnimating, setIsAnimating] = useState(false); const [starProgress, setStarProgress] = useState(filled ? 100 : 0); const [hasAnimated, setHasAnimated] = useState(false); // Format number with units const formatNumber = (num: number) => { const units = ['k', 'M', 'B', 'T']; if (roundStars && num >= 1000) { let unitIndex = -1; let value = num; while (value >= 1000 && unitIndex < units.length - 1) { value /= 1000; unitIndex++; } // Format to 1 decimal place if needed, otherwise show whole number const formatted = value % 1 === 0 ? value.toString() : value.toFixed(1); return `${formatted}${units[unitIndex]}`; } return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); }; // Start animation const startAnimation = useCallback(() => { if (isAnimating || hasAnimated) return; setIsAnimating(true); const startTime = Date.now(); const startValue = 0; // Always start from 0 for number animation const endValue = targetStars; const duration = animationDuration * 1000; const animate = () => { const elapsed = Date.now() - startTime; const progress = Math.min(elapsed / duration, 1); // Easing function for smooth animation const easeOutQuart = 1 - Math.pow(1 - progress, 4); // Update star count from 0 to target with more frequent updates const newStars = Math.round(startValue + (endValue - startValue) * easeOutQuart); setCurrentStars(newStars); // Update star fill progress (0 to 100) setStarProgress(progress * 100); if (progress < 1) { requestAnimationFrame(animate); } else { setCurrentStars(endValue); setStarProgress(100); setIsAnimating(false); setHasAnimated(true); } }; setTimeout(() => { requestAnimationFrame(animate); }, animationDelay * 1000); }, [isAnimating, hasAnimated, targetStars, animationDuration, animationDelay]); // Use in-view detection if enabled const ref = React.useRef(null); const isInView = useInView(ref, inViewOptions); // Reset animation state when targetStars changes useEffect(() => { setHasAnimated(false); setCurrentStars(initialStars); }, [targetStars, initialStars]); // Auto-start animation or use in-view trigger useEffect(() => { if (useInViewTrigger) { if (isInView && !hasAnimated) { startAnimation(); } } else if (autoAnimate && !hasAnimated) { startAnimation(); } }, [autoAnimate, useInViewTrigger, isInView, hasAnimated, startAnimation]); const navigateToRepo = () => { if (!repoUrl) { return; } // Next.js compatible navigation approach try { // Create a temporary anchor element for reliable navigation const link = document.createElement('a'); link.href = repoUrl; link.target = '_blank'; link.rel = 'noopener noreferrer'; // Temporarily add to DOM and click document.body.appendChild(link); link.click(); document.body.removeChild(link); } catch { // Fallback to window.open try { window.open(repoUrl, '_blank', 'noopener,noreferrer'); } catch { // Final fallback window.location.href = repoUrl; } } }; const handleClick = (event: React.MouseEvent) => { if (onClick) { onClick(event); return; } if (repoUrl) { navigateToRepo(); } else if (!hasAnimated) { startAnimation(); } }; const handleKeyDown = (event: React.KeyboardEvent) => { // Handle Enter and Space key presses for accessibility if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); if (repoUrl) { navigateToRepo(); } else if (!hasAnimated) { startAnimation(); } } }; return ( ); } export { GithubButton, githubButtonVariants }; export type { GithubButtonProps };