{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "gradient-wave-text",
  "title": "Gradient Wave Text",
  "description": "Apple-style animated gradient text with wave effect.",
  "dependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/spell-ui/gradient-wave-text.tsx",
      "content": "\"use client\";\n\nimport { useRef, useEffect, useMemo, useCallback, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ntype Align = \"left\" | \"center\" | \"right\";\n\nconst defaultColors = [\"#8d6869\", \"#5a8ea6\", \"#b9c96e\", \"#c7c571\", \"#cb706f\", \"#7e5e5f\"];\n\ninterface GradientWaveTextProps {\n  children?: React.ReactNode;\n  align?: Align;\n  className?: string;\n\n  speed?: number; \n  paused?: boolean;\n  delay?: number;\n  repeat?: boolean;\n  inView?: boolean;\n  once?: boolean;\n\n  radial?: boolean;\n  bottomOffset?: number;\n  bandGap?: number;\n  bandCount?: number;\n  customColors?: string[];\n\n  onClick?: (e: React.MouseEvent) => void;\n  onMouseEnter?: (e: React.MouseEvent) => void;\n  onMouseLeave?: (e: React.MouseEvent) => void;\n\n  ariaLabel?: string;\n}\n\n\nexport function GradientWaveText({\n  children,\n  align = \"center\",\n  className,\n\n  speed = 1,\n  paused = false,\n  delay = 0,\n  repeat = false,\n  inView = false,\n  once = true,\n\n  radial = true,\n  bottomOffset = 20,\n  bandGap = 4,\n  bandCount = 8,\n  customColors,\n\n  onClick,\n  onMouseEnter,\n  onMouseLeave,\n\n  ariaLabel,\n}: GradientWaveTextProps) {\n  const elRef = useRef<HTMLDivElement | null>(null);\n  const rafRef = useRef(0);\n  const tRef = useRef(0);\n  const cyclesDoneRef = useRef(0);\n  const finishedRef = useRef(false);\n  const startedRef = useRef(false);\n  const startAtRef = useRef(0);\n  const hasPlayedRef = useRef(false);\n\n  const [isInView, setIsInView] = useState(!inView);\n\n  const cycles = repeat ? 0 : 1;\n\n  useEffect(() => {\n    if (!inView) {\n      setIsInView(true);\n      return;\n    }\n\n    const node = elRef.current;\n    if (!node) return;\n\n    const observer = new IntersectionObserver(\n      (entries) => {\n        entries.forEach((entry) => {\n          if (entry.isIntersecting) {\n            if (once && hasPlayedRef.current) return;\n            setIsInView(true);\n            hasPlayedRef.current = true;\n          } else if (!once) {\n            setIsInView(false);\n          }\n        });\n      },\n      { threshold: 0.1 }\n    );\n\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, [inView, once]);\n\n  const resolvedColors = useMemo(() => {\n    return customColors?.length ? customColors : defaultColors;\n  }, [customColors]);\n\n  const stops = useMemo(() => {\n    const arr: string[] = [];\n    const baseColor = \"var(--gradient-wave-base, rgb(29,29,31))\";\n    arr.push(`${baseColor} calc((var(--gi) + 0) * 1%)`);\n    for (let i = 0; i < bandCount && i < resolvedColors.length * 2; i++) {\n      const color = resolvedColors[i % resolvedColors.length];\n      const offset = (i + 2) * bandGap;\n      arr.push(`${color} calc((var(--gi) + ${offset}) * 1%)`);\n    }\n    const endOffset = (bandCount + 2) * bandGap;\n    arr.push(`${baseColor} calc((var(--gi) + ${endOffset}) * 1%)`);\n    return arr.join(\", \");\n  }, [resolvedColors, bandGap, bandCount]);\n\n  const gradient = useMemo(() => {\n    return radial\n      ? `radial-gradient(circle at 50% bottom, ${stops})`\n      : `linear-gradient(0deg, ${stops})`;\n  }, [radial, stops]);\n\n  useEffect(() => {\n    const node = elRef.current;\n    if (node) node.style.setProperty(\"--gi\", \"-25\");\n  }, []);\n\n  useEffect(() => {\n    if (!isInView) return;\n\n    const node = elRef.current;\n    if (!node) return;\n\n    tRef.current = -25;\n    cyclesDoneRef.current = 0;\n    finishedRef.current = false;\n    startedRef.current = false;\n    startAtRef.current = performance.now() + Math.max(0, (delay ?? 0) * 1000);\n    node.style.setProperty(\"--gi\", \"-25\");\n  }, [isInView, delay]);\n\n  useEffect(() => {\n    const node = elRef.current;\n    if (!node || !isInView) return;\n\n    const RANGE = 200;\n    let last = performance.now();\n\n    const tick = (now: number) => {\n      if (finishedRef.current) return;\n\n      if (!startedRef.current) {\n        if (now >= startAtRef.current) {\n          startedRef.current = true;\n          last = now;\n        } else {\n          rafRef.current = requestAnimationFrame(tick);\n          return;\n        }\n      }\n\n      const dt = Math.min(64, now - last);\n      last = now;\n\n      const shouldAnimate = !paused;\n\n      if (shouldAnimate) {\n        const increment = (dt * speed) / 16.6667;\n        let next = tRef.current + increment;\n\n        if (cycles === 0) {\n          if (next >= RANGE) next = next % RANGE;\n          tRef.current = next;\n          node.style.setProperty(\"--gi\", String(next));\n        } else {\n          while (next >= RANGE && cyclesDoneRef.current < cycles) {\n            next -= RANGE;\n            cyclesDoneRef.current += 1;\n          }\n\n          if (cyclesDoneRef.current >= cycles) {\n            tRef.current = RANGE;\n            node.style.setProperty(\"--gi\", String(RANGE));\n            finishedRef.current = true;\n            return;\n          } else {\n            tRef.current = next;\n            node.style.setProperty(\"--gi\", String(next));\n          }\n        }\n      }\n\n      rafRef.current = requestAnimationFrame(tick);\n    };\n\n    rafRef.current = requestAnimationFrame(tick);\n    return () => cancelAnimationFrame(rafRef.current);\n  }, [speed, paused, cycles, isInView]);\n\n  const justifyContent =\n    align === \"left\"\n      ? \"flex-start\"\n      : align === \"right\"\n        ? \"flex-end\"\n        : \"center\";\n\n  const handleClick = useCallback(\n    (e: React.MouseEvent) => {\n      onClick?.(e);\n    },\n    [onClick]\n  );\n\n  const handleMouseEnter = useCallback(\n    (e: React.MouseEvent) => {\n      onMouseEnter?.(e);\n    },\n    [onMouseEnter]\n  );\n\n  const handleMouseLeave = useCallback(\n    (e: React.MouseEvent) => {\n      onMouseLeave?.(e);\n    },\n    [onMouseLeave]\n  );\n\n  return (\n    <div\n      ref={elRef}\n      className={cn(\n        \"flex w-full h-full items-center [--gradient-wave-base:rgb(29,29,31)] dark:[--gradient-wave-base:rgb(255,255,255)]\",\n        className\n      )}\n      style={{ justifyContent, \"--gi\": -25 } as React.CSSProperties}\n      aria-label={ariaLabel || undefined}\n      role={ariaLabel ? \"img\" : undefined}\n      onClick={handleClick}\n      onMouseEnter={handleMouseEnter}\n      onMouseLeave={handleMouseLeave}\n    >\n      <span\n        style={{\n          textAlign: align,\n          backgroundImage: gradient,\n          WebkitBackgroundClip: \"text\",\n          backgroundClip: \"text\",\n          WebkitTextFillColor: \"transparent\",\n          color: \"transparent\",\n          whiteSpace: \"pre-wrap\",\n          wordBreak: \"break-word\",\n          display: \"inline-block\",\n          WebkitFontSmoothing: \"antialiased\",\n          MozOsxFontSmoothing: \"grayscale\",\n          WebkitBackfaceVisibility: \"hidden\",\n          backfaceVisibility: \"hidden\",\n          transform: \"translateZ(0)\",\n          paddingBottom: `${bottomOffset}%`,\n          marginBottom: `-${bottomOffset}%`,\n          paddingInline: 2,\n        }}\n      >\n        {children}\n      </span>\n    </div>\n  );\n}\n\nexport default GradientWaveText;\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}