{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "social-orbit",
  "type": "registry:ui",
  "title": "Social Orbit",
  "description": "orbits your socials in smooth motion where icons spin, ripple, and glow with purpose.",
  "author": "Ahdeetai <https://aditya.is-cool.dev>",
  "registryDependencies": [],
  "dependencies": ["motion", "clsx", "tailwind-merge"],
  "files": [
    {
      "type": "registry:ui",
      "path": "components/ui/social-orbit.tsx",
      "content": "'use client';\nimport React, { useState, useEffect } from 'react';\nimport { motion } from 'motion/react';\nimport { cn } from '@/lib/utils';\n\ninterface SocialIcon {\n  icon: React.ReactNode;\n  orbitIndex?: number;\n  position?: number;\n}\n\ninterface SocialOrbitProps {\n  icons?: SocialIcon[];\n  text?: string;\n  textClassName?: string;\n  textOrbitIndex?: number;\n  children?: React.ReactNode;\n  rippleCount?: number;\n  rippleDuration?: number;\n  textDuration?: number;\n  iconDelay?: number;\n  iconDuration?: number;\n  orbitDuration?: number;\n  size?: number;\n  className?: string;\n}\n\nexport function SocialOrbit({\n  icons = [],\n  text = '',\n  textClassName = '',\n  textOrbitIndex = 2,\n  children,\n  rippleCount = 5,\n  rippleDuration = 2,\n  textDuration = 20,\n  iconDelay = 150,\n  iconDuration = 800,\n  orbitDuration = 30,\n  size = 500,\n  className = '',\n}: SocialOrbitProps) {\n  const [animatedIcons, setAnimatedIcons] = useState<Set<number>>(new Set());\n  const [rotationStarted, setRotationStarted] = useState(false);\n  const [isHovering, setIsHovering] = useState(false);\n\n  useEffect(() => {\n    icons.forEach((_, index) => {\n      setTimeout(() => {\n        setAnimatedIcons((prev) => new Set([...prev, index]));\n      }, index * iconDelay);\n    });\n    const totalAnimationTime = icons.length * iconDelay + iconDuration;\n    setTimeout(() => setRotationStarted(true), totalAnimationTime);\n  }, [icons, iconDelay, iconDuration]);\n\n  const baseInset = 40;\n  const rippleBoxes = Array.from({ length: rippleCount }, (_, i) => {\n    const insetPercent = baseInset - i * 8;\n    const radiusPercent = 50 - insetPercent;\n    return {\n      inset: `${insetPercent}%`,\n      radius: (size / 2) * (radiusPercent / 50),\n      zIndex: 99 - i,\n      delay: i * 0.2,\n      opacity: 1 - i * 0.15,\n    };\n  });\n\n  const textRippleIndex = Math.min(textOrbitIndex, rippleBoxes.length - 1);\n  const textRippleRadius = rippleBoxes[textRippleIndex].radius;\n  const letters = Array.from(text);\n\n  const calculatePosition = (\n    index: number,\n    total: number,\n    radius: number,\n    customAngle?: number,\n  ) => {\n    const angle =\n      customAngle !== undefined ? customAngle : (360 / total) * index;\n    const radian = (angle * Math.PI) / 180;\n    return { x: Math.cos(radian) * radius, y: Math.sin(radian) * radius };\n  };\n\n  const iconsByOrbit = icons.reduce(\n    (acc, icon, index) => {\n      const orbitIdx = icon.orbitIndex ?? 0;\n      if (!acc[orbitIdx]) acc[orbitIdx] = [];\n      acc[orbitIdx].push({ ...icon, originalIndex: index });\n      return acc;\n    },\n    {} as Record<number, Array<SocialIcon & { originalIndex: number }>>,\n  );\n\n  return (\n    <div\n      className={cn('relative', className)}\n      style={{ width: size, height: size }}\n    >\n      <div className='absolute inset-0'>\n        {rippleBoxes.map((box, i) => (\n          <motion.div\n            key={`ripple-${i}`}\n            className='absolute rounded-full border-2 border-border/50 bg-linear-to-b from-muted/10 to-muted/20'\n            style={{\n              width: box.radius * 2,\n              height: box.radius * 2,\n              left: '50%',\n              top: '50%',\n              marginLeft: -box.radius,\n              marginTop: -box.radius,\n              zIndex: box.zIndex,\n              opacity: box.opacity,\n            }}\n            animate={{\n              scale: [1, 1.15, 1],\n              boxShadow: [\n                'rgba(0,0,0,0.3) 0px 10px 10px 0px',\n                'rgba(0,0,0,0.3) 0px 30px 20px 0px',\n                'rgba(0,0,0,0.3) 0px 10px 10px 0px',\n              ],\n            }}\n            transition={{\n              repeat: Infinity,\n              duration: rippleDuration,\n              delay: box.delay,\n              ease: 'easeInOut',\n            }}\n          />\n        ))}\n      </div>\n\n      {text && (\n        <motion.div\n          className='absolute cursor-pointer'\n          style={{\n            width: textRippleRadius * 2,\n            height: textRippleRadius * 2,\n            left: '50%',\n            top: '50%',\n            marginLeft: -textRippleRadius,\n            marginTop: -textRippleRadius,\n            zIndex: 100,\n          }}\n          animate={{\n            scale: [1, 1.15, 1],\n            rotate: rotationStarted ? [0, 360] : 0,\n          }}\n          transition={{\n            scale: {\n              repeat: Infinity,\n              duration: rippleDuration,\n              delay: rippleBoxes[textRippleIndex].delay,\n              ease: 'easeInOut',\n            },\n            rotate: {\n              repeat: Infinity,\n              duration: isHovering ? textDuration / 4 : textDuration,\n              ease: 'linear',\n            },\n          }}\n          onHoverStart={() => setIsHovering(true)}\n          onHoverEnd={() => setIsHovering(false)}\n        >\n          {letters.map((letter, i) => {\n            const angle = (360 / letters.length) * i;\n            const radian = (angle * Math.PI) / 180;\n            const x = Math.cos(radian) * textRippleRadius;\n            const y = Math.sin(radian) * textRippleRadius;\n            return (\n              <div\n                key={`letter-${i}`}\n                className='absolute'\n                style={{\n                  left: '50%',\n                  top: '50%',\n                  transform: `translate(${x}px, ${y}px) translate(-50%, -50%)`,\n                }}\n              >\n                <motion.span\n                  className={cn(\n                    'inline-block text-lg font-bold text-foreground/90 transition-all duration-300 hover:text-foreground hover:scale-110',\n                    textClassName,\n                  )}\n                  style={{\n                    animation: rotationStarted\n                      ? `counter-orbit ${\n                          isHovering ? textDuration / 4 : textDuration\n                        }s linear infinite`\n                      : 'none',\n                  }}\n                >\n                  {letter}\n                </motion.span>\n              </div>\n            );\n          })}\n        </motion.div>\n      )}\n\n      {Object.entries(iconsByOrbit).map(([orbitIdx, orbitIcons]) => {\n        const orbitIndex = Math.min(parseInt(orbitIdx), rippleBoxes.length - 1);\n        const iconRippleRadius = rippleBoxes[orbitIndex].radius;\n        return (\n          <motion.div\n            key={`orbit-${orbitIdx}`}\n            className='absolute'\n            style={{\n              width: iconRippleRadius * 2,\n              height: iconRippleRadius * 2,\n              left: '50%',\n              top: '50%',\n              marginLeft: -iconRippleRadius,\n              marginTop: -iconRippleRadius,\n              zIndex: 101 + parseInt(orbitIdx),\n            }}\n            animate={{\n              scale: [1, 1.15, 1],\n              rotate: rotationStarted ? [0, 360] : 0,\n            }}\n            transition={{\n              scale: {\n                repeat: Infinity,\n                duration: rippleDuration,\n                delay: rippleBoxes[orbitIndex].delay,\n                ease: 'easeInOut',\n              },\n              rotate: {\n                repeat: Infinity,\n                duration: orbitDuration,\n                ease: 'linear',\n              },\n            }}\n          >\n            {orbitIcons.map((social, localIndex) => {\n              const position = calculatePosition(\n                localIndex,\n                orbitIcons.length,\n                iconRippleRadius,\n                social.position,\n              );\n              const isAnimated = animatedIcons.has(social.originalIndex);\n              return (\n                <div\n                  key={`icon-${social.originalIndex}`}\n                  className='absolute'\n                  style={{\n                    left: '50%',\n                    top: '50%',\n                    marginLeft: -24,\n                    marginTop: -24,\n                    transform: isAnimated\n                      ? `translate(${position.x}px, ${position.y}px) scale(1)`\n                      : 'translate(0px,0px) scale(0)',\n                    transition: `transform ${iconDuration}ms cubic-bezier(0.34,1.56,0.64,1)`,\n                    opacity: isAnimated ? 1 : 0,\n                  }}\n                >\n                  <motion.div\n                    className='flex items-center justify-center w-12 h-12 rounded-full bg-background text-foreground border border-border shadow-lg'\n                    style={{\n                      animation: rotationStarted\n                        ? `counter-orbit ${orbitDuration}s linear infinite`\n                        : 'none',\n                    }}\n                    whileHover={{ scale: 1.2 }}\n                  >\n                    {social.icon}\n                  </motion.div>\n                </div>\n              );\n            })}\n          </motion.div>\n        );\n      })}\n\n      {children && (\n        <div className='absolute inset-0 flex items-center justify-center z-200 pointer-events-none'>\n          <motion.div\n            initial={{ opacity: 0, scale: 0.5 }}\n            animate={{ opacity: 1, scale: 1 }}\n            transition={{ delay: 2, duration: 0.8, type: 'spring' }}\n          >\n            {children}\n          </motion.div>\n        </div>\n      )}\n\n      <style jsx>{`\n        @keyframes orbit {\n          from {\n            transform: rotate(0deg);\n          }\n          to {\n            transform: rotate(360deg);\n          }\n        }\n        @keyframes counter-orbit {\n          from {\n            transform: rotate(0deg);\n          }\n          to {\n            transform: rotate(-360deg);\n          }\n        }\n      `}</style>\n    </div>\n  );\n}\n"
    },
    {
      "type": "registry:lib",
      "path": "lib/utils.ts",
      "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}"
    }
  ]
}
