{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"gradient-text","type":"registry:component","title":"Gradient Text","description":"Text filled with a slowly drifting rainbow. It pauses while off screen and holds still when reduced motion is on.","author":"Ryan","dependencies":["clsx","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/gradient-text.tsx","type":"registry:component","target":"@components/motion/gradient-text.tsx","content":"\"use client\";\n// easeui.dev/components/motion/gradient-text\n\nimport { type ComponentProps, useEffect, useRef } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\n/** A full color wheel. Painted three times larger than the text so only part of it shows at once. */\nconst SPECTRUM =\n  \"conic-gradient(from 0deg, hsl(0 90% 60%), hsl(60 90% 55%), hsl(120 85% 50%), hsl(180 85% 50%), hsl(240 90% 65%), hsl(300 85% 60%), hsl(360 90% 60%))\";\n\n/** The visible window circles around the oversized gradient, so the colors drift through the text. */\nconst DRIFT: Keyframe[] = [\n  { backgroundPosition: \"0% 50%\" },\n  { backgroundPosition: \"50% 100%\" },\n  { backgroundPosition: \"100% 50%\" },\n  { backgroundPosition: \"50% 0%\" },\n  { backgroundPosition: \"0% 50%\" },\n];\n\nexport interface GradientTextProps extends Omit<ComponentProps<\"span\">, \"ref\"> {\n  /** Fills the text with the drifting spectrum. When false the text is muted. Default true. */\n  active?: boolean;\n  /** Seconds for one full loop. Default 5.5. */\n  duration?: number;\n}\n\n/**\n * Text filled with a slowly drifting rainbow. The drift runs through the Web\n * Animations API, so there is no global CSS to install. It pauses while off\n * screen, and with reduced motion the gradient stays still.\n */\nexport function GradientText({\n  active = true,\n  duration = 5.5,\n  className,\n  style,\n  children,\n  ...props\n}: GradientTextProps) {\n  const ref = useRef<HTMLSpanElement>(null);\n\n  useEffect(() => {\n    const element = ref.current;\n    if (!active || !element || typeof element.animate !== \"function\") return;\n\n    const reducedMotion = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    const animation = element.animate(DRIFT, {\n      duration: duration * 1000,\n      iterations: Number.POSITIVE_INFINITY,\n      easing: \"linear\",\n    });\n    let onScreen = true;\n\n    const sync = () => {\n      if (reducedMotion.matches || !onScreen) animation.pause();\n      else animation.play();\n    };\n\n    // Nothing to animate while the text is scrolled out of view.\n    const observer = new IntersectionObserver(([entry]) => {\n      onScreen = entry.isIntersecting;\n      sync();\n    });\n    observer.observe(element);\n    reducedMotion.addEventListener(\"change\", sync);\n    sync();\n\n    return () => {\n      observer.disconnect();\n      reducedMotion.removeEventListener(\"change\", sync);\n      animation.cancel();\n    };\n  }, [active, duration]);\n\n  return (\n    <span\n      ref={ref}\n      data-active={active || undefined}\n      className={cn(\n        active\n          ? \"bg-clip-text font-semibold text-transparent [-webkit-background-clip:text]\"\n          : \"text-muted-foreground\",\n        className,\n      )}\n      style={active ? { backgroundImage: SPECTRUM, backgroundSize: \"300% 300%\", ...style } : style}\n      {...props}\n    >\n      {children}\n    </span>\n  );\n}\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@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}\n"}]}