{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "text-spotlight",
  "type": "registry:ui",
  "title": "Text Spotlight",
  "description": "spotlight text effect with hover and mobile reveal, customizable size and color.",
  "author": "Ahdeetai <https://aditya.is-cool.dev>",
  "registryDependencies": [],
  "dependencies": ["motion", "clsx", "tailwind-merge"],
  "files": [
    {
      "type": "registry:ui",
      "path": "components/ui/text-spotlight.tsx",
      "content": "'use client';\n\nimport React, { useRef, useState, useEffect } from 'react';\nimport { motion, useMotionValue, useMotionTemplate } from 'motion/react';\nimport { cn } from '@/lib/utils';\n\ninterface TextSpotlightProps extends React.HTMLAttributes<HTMLDivElement> {\n  text: string;\n  spotlightColor?: string;\n  spotlightSize?: number;\n  textClassName?: string;\n  spotlightOpacity?: number;\n  spotlightArea?: number;\n  animateOnPhone?: boolean;\n  colorDuration?: number;\n}\n\nconst BREAKPOINT = 1024;\n\nconst useIsMobile = () => {\n  const [isMobile, setIsMobile] = useState(false);\n\n  useEffect(() => {\n    const handleResize = () => setIsMobile(window.innerWidth < BREAKPOINT);\n    handleResize();\n    window.addEventListener('resize', handleResize);\n    return () => window.removeEventListener('resize', handleResize);\n  }, []);\n\n  return isMobile;\n};\n\nexport function TextSpotlight({\n  text,\n  className,\n  textClassName,\n  spotlightColor = '255, 255, 255',\n  spotlightSize = 450,\n  spotlightOpacity = 1,\n  spotlightArea,\n  animateOnPhone = false,\n  colorDuration = 2000,\n  ...props\n}: TextSpotlightProps) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const mouseX = useMotionValue(0);\n  const mouseY = useMotionValue(0);\n  const [isHovered, setIsHovered] = useState(false);\n  const [isVisible, setIsVisible] = useState(false);\n  const [revealProgress, setRevealProgress] = useState(0);\n  const isMobile = useIsMobile();\n\n  useEffect(() => {\n    if (!animateOnPhone || !isMobile || !containerRef.current) return;\n\n    const observer = new IntersectionObserver(\n      ([entry]) => {\n        setIsVisible(entry.isIntersecting);\n        if (!entry.isIntersecting) {\n          setRevealProgress(0);\n        }\n      },\n      { threshold: 0.3 },\n    );\n\n    observer.observe(containerRef.current);\n    return () => observer.disconnect();\n  }, [animateOnPhone, isMobile]);\n\n  useEffect(() => {\n    if (!animateOnPhone || !isMobile || !isVisible) return;\n\n    const startTime = Date.now();\n\n    const animate = () => {\n      const elapsed = Date.now() - startTime;\n      const progress = Math.min(elapsed / colorDuration, 1);\n      setRevealProgress(progress);\n\n      if (progress < 1) {\n        requestAnimationFrame(animate);\n      }\n    };\n\n    requestAnimationFrame(animate);\n  }, [isVisible, animateOnPhone, isMobile, colorDuration]);\n\n  const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {\n    if (!containerRef.current) return;\n    const rect = containerRef.current.getBoundingClientRect();\n    mouseX.set(e.clientX - rect.left);\n    mouseY.set(e.clientY - rect.top);\n    setIsHovered(true);\n  };\n\n  const handleMouseLeave = () => {\n    setIsHovered(false);\n  };\n\n  const background = useMotionTemplate`\n    radial-gradient(\n      ${spotlightSize}px circle at ${mouseX}px ${mouseY}px,\n      rgba(${spotlightColor}, ${spotlightOpacity}),\n      transparent 80%\n    )\n  `;\n\n  const chars = text.split('');\n  const shouldShowMobileReveal = animateOnPhone && isMobile;\n\n  return (\n    <div\n      ref={containerRef}\n      onMouseMove={handleMouseMove}\n      onMouseLeave={handleMouseLeave}\n      className={cn('relative w-full overflow-hidden', className)}\n      {...props}\n    >\n      {shouldShowMobileReveal ? (\n        <p\n          className={cn('relative z-10 select-none text-center', textClassName)}\n        >\n          {chars.map((char, index) => {\n            const charProgress = (revealProgress * chars.length - index) / 1;\n            const opacity = Math.max(0, Math.min(1, charProgress));\n            const isRevealed = opacity > 0.5;\n\n            return (\n              <span\n                key={index}\n                style={{\n                  transition: 'all 0.3s ease-out',\n                  color: isRevealed ? 'inherit' : undefined,\n                }}\n                className={\n                  isRevealed ? '' : 'text-gray-600/10 dark:text-white/15'\n                }\n              >\n                {char}\n              </span>\n            );\n          })}\n        </p>\n      ) : (\n        <>\n          <p\n            className={cn(\n              'relative z-10 text-gray-600/10 dark:text-white/15 select-none text-center',\n              textClassName,\n            )}\n          >\n            {text}\n          </p>\n\n          <motion.div\n            className='absolute inset-0 z-20 flex items-center justify-center pointer-events-none'\n            style={{\n              WebkitMaskImage: background,\n              maskImage: background,\n              opacity: isHovered ? 1 : 0,\n            }}\n          >\n            <p className={cn('text-foreground text-center', textClassName)}>\n              {text}\n            </p>\n          </motion.div>\n        </>\n      )}\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}"
    }
  ]
}
