{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion","lucide-react"],"description":"A GitHubStarsAnimation component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Star } from \"lucide-react\";\nimport { motion, useReducedMotion, useSpring } from \"motion/react\";\nimport { useEffect, useState } from \"react\";\n\nconst TRANSITION_DURATION = 0.3;\nconst EASE_OUT_CUBIC = [0.215, 0.61, 0.355, 1] as const;\nconst COUNTDOWN_DURATION = 2000;\nconst AVATAR_COUNT = 5;\nconst STAGGER_DELAY = 0.05;\n\nexport interface Stargazer {\n  avatar_url: string;\n  html_url: string;\n  login: string;\n}\n\nexport interface GitHubStarsAnimationProps {\n  apiEndpoint?: string;\n  avatarClassName?: string;\n  className?: string;\n  countClassName?: string;\n  maxAvatars?: number;\n  owner?: string;\n  repo?: string;\n  showAvatars?: boolean;\n  starCount?: number;\n  stargazers?: Stargazer[];\n}\n\nexport default function GitHubStarsAnimation({\n  owner = \"educlopez\",\n  repo = \"smoothui\",\n  stargazers: providedStargazers,\n  starCount: providedStarCount,\n  apiEndpoint,\n  className = \"\",\n  avatarClassName = \"\",\n  countClassName = \"\",\n  showAvatars = true,\n  maxAvatars = AVATAR_COUNT,\n}: GitHubStarsAnimationProps) {\n  const [stargazers, setStargazers] = useState<Stargazer[]>(\n    providedStargazers || []\n  );\n  const [starCount, setStarCount] = useState(providedStarCount || 0);\n  const [displayCount, setDisplayCount] = useState(0);\n  const [isLoading, setIsLoading] = useState(!providedStargazers);\n  const [error, setError] = useState(false);\n  const shouldReduceMotion = useReducedMotion();\n\n  const countSpring = useSpring(0, {\n    damping: 30,\n    stiffness: 100,\n  });\n\n  // Fetch stargazers and star count\n  useEffect(() => {\n    if (providedStargazers && providedStarCount !== undefined) {\n      setStargazers(providedStargazers);\n      setStarCount(providedStarCount);\n      setIsLoading(false);\n      return;\n    }\n\n    const fetchData = async () => {\n      try {\n        setIsLoading(true);\n        setError(false);\n\n        // Try to fetch from custom API endpoint first\n        if (apiEndpoint) {\n          const response = await fetch(\n            `${apiEndpoint}?owner=${owner}&repo=${repo}`\n          );\n          if (response.ok) {\n            const data = await response.json();\n            if (data.stargazers) {\n              setStargazers(data.stargazers.slice(0, maxAvatars));\n            }\n            if (data.stars !== undefined) {\n              setStarCount(data.stars);\n            }\n            setIsLoading(false);\n            return;\n          }\n        }\n\n        // Fallback to GitHub API directly (client-side)\n        // Note: This has rate limits, so using a token is recommended\n        const headers: HeadersInit = {\n          Accept: \"application/vnd.github.v3+json\",\n        };\n\n        // Parallelize independent fetches to eliminate waterfall\n        const [repoResponse, stargazersResponse] = await Promise.all([\n          fetch(`https://api.github.com/repos/${owner}/${repo}`, { headers }),\n          fetch(\n            `https://api.github.com/repos/${owner}/${repo}/stargazers?per_page=${maxAvatars}`,\n            { headers }\n          ),\n        ]);\n\n        // Process repo info for star count\n        if (repoResponse.ok) {\n          try {\n            const repoData = await repoResponse.json();\n            setStarCount(repoData.stargazers_count || 0);\n          } catch {\n            // Silently fail for star count\n          }\n        }\n\n        // Process stargazers\n        if (stargazersResponse.ok) {\n          try {\n            const stargazersData =\n              (await stargazersResponse.json()) as Stargazer[];\n            setStargazers(stargazersData.slice(0, maxAvatars));\n          } catch {\n            // Silently fail for stargazers\n          }\n        }\n      } catch {\n        setError(true);\n      } finally {\n        setIsLoading(false);\n      }\n    };\n\n    fetchData();\n  }, [\n    owner,\n    repo,\n    apiEndpoint,\n    maxAvatars,\n    providedStargazers,\n    providedStarCount,\n  ]);\n\n  // Animate countdown\n  useEffect(() => {\n    if (starCount === 0 || shouldReduceMotion) {\n      if (shouldReduceMotion) {\n        setDisplayCount(starCount);\n        countSpring.set(starCount);\n      }\n      return;\n    }\n\n    const startTime = Date.now();\n    const startValue = 0;\n    const endValue = starCount;\n\n    const animate = () => {\n      const elapsed = Date.now() - startTime;\n      const progress = Math.min(elapsed / COUNTDOWN_DURATION, 1);\n\n      // Ease-out function\n      const eased = 1 - (1 - progress) ** 3;\n      const current = Math.floor(startValue + (endValue - startValue) * eased);\n\n      setDisplayCount(current);\n      countSpring.set(current);\n\n      if (progress < 1) {\n        requestAnimationFrame(animate);\n      } else {\n        setDisplayCount(endValue);\n        countSpring.set(endValue);\n      }\n    };\n\n    animate();\n  }, [starCount, countSpring, shouldReduceMotion]);\n\n  if (isLoading) {\n    return (\n      <div\n        className={cn(\"flex items-center gap-3 text-foreground/60\", className)}\n      >\n        <div className=\"h-10 w-10 animate-pulse rounded-full bg-foreground/20\" />\n        <div className=\"h-6 w-20 animate-pulse rounded bg-foreground/20\" />\n      </div>\n    );\n  }\n\n  if (error && starCount === 0) {\n    return null;\n  }\n\n  const visibleAvatars = stargazers.slice(0, maxAvatars);\n\n  return (\n    <div className={cn(\"flex items-center gap-3\", className)}>\n      {/* Avatars */}\n      {showAvatars && visibleAvatars.length > 0 && (\n        <div className=\"relative flex items-center\">\n          {visibleAvatars.map((stargazer, index) => (\n            <motion.a\n              animate={\n                shouldReduceMotion\n                  ? { opacity: 1 }\n                  : {\n                      opacity: 1,\n                      scale: 1,\n                      x: 0,\n                    }\n              }\n              aria-label={`${stargazer.login}'s GitHub profile`}\n              className={cn(\n                \"relative z-10 h-10 w-10 overflow-hidden rounded-full border-2 border-background bg-background transition-transform hover:z-20 hover:scale-110\",\n                avatarClassName\n              )}\n              href={stargazer.html_url}\n              initial={\n                shouldReduceMotion\n                  ? { opacity: 1 }\n                  : {\n                      opacity: 0,\n                      scale: 0.8,\n                      x: -20,\n                    }\n              }\n              key={stargazer.login}\n              rel=\"noopener noreferrer\"\n              style={{\n                marginLeft: index > 0 ? \"-8px\" : \"0\",\n              }}\n              target=\"_blank\"\n              transition={\n                shouldReduceMotion\n                  ? { duration: 0 }\n                  : {\n                      delay: index * STAGGER_DELAY,\n                      duration: TRANSITION_DURATION,\n                      ease: EASE_OUT_CUBIC,\n                    }\n              }\n              whileHover={shouldReduceMotion ? {} : { scale: 1.1, zIndex: 20 }}\n            >\n              <img\n                alt={`${stargazer.login}'s avatar`}\n                className=\"h-full w-full object-cover\"\n                draggable={false}\n                src={stargazer.avatar_url}\n              />\n            </motion.a>\n          ))}\n        </div>\n      )}\n\n      {/* Star count */}\n      <motion.div\n        animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1, scale: 1 }}\n        className={cn(\"flex items-center gap-1.5 font-medium\", countClassName)}\n        initial={\n          shouldReduceMotion ? { opacity: 1 } : { opacity: 0, scale: 0.9 }\n        }\n        transition={\n          shouldReduceMotion\n            ? { duration: 0 }\n            : {\n                duration: TRANSITION_DURATION,\n                ease: EASE_OUT_CUBIC,\n              }\n        }\n      >\n        <Star className=\"h-4 w-4 fill-current\" />\n        <motion.span\n          animate={shouldReduceMotion ? { scale: 1 } : { scale: [1, 1.1, 1] }}\n          className=\"tabular-nums\"\n          transition={\n            shouldReduceMotion\n              ? { duration: 0 }\n              : {\n                  duration: 0.3,\n                  ease: EASE_OUT_CUBIC,\n                }\n          }\n        >\n          {displayCount.toLocaleString()}\n        </motion.span>\n        <span className=\"text-foreground/70 text-sm\">\n          {displayCount === 1 ? \"star\" : \"stars\"}\n        </span>\n      </motion.div>\n    </div>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/github-stars-animation/index.tsx","type":"registry:ui"}],"name":"github-stars-animation","registryDependencies":[],"title":"Github Stars Animation","type":"registry:ui"}