{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ascii-hover-button",
  "type": "registry:ui",
  "title": "ASCII Hover Button",
  "description": "Button with color transitions and an ASCII character scramble on hover.",
  "dependencies": [
    "gsap",
    "@gsap/react",
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/registry/ui/ascii-hover-button.tsx",
      "content": "\"use client\";\n\nimport React, {\n  useCallback,\n  useEffect,\n  useImperativeHandle,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { useGSAP } from \"@gsap/react\";\nimport gsap from \"gsap\";\nimport { cn } from \"@/lib/utils\";\n\ngsap.registerPlugin(useGSAP);\n\nexport type AsciiHoverColor =\n  | \"default\"\n  | \"emerald\"\n  | \"blue\"\n  | \"violet\"\n  | \"amber\"\n  | \"rose\"\n  | \"cyan\";\n\nexport interface AsciiColorPalette {\n  dark: {\n    text: string;\n    glow: string;\n  };\n  light: {\n    text: string;\n    glow: string;\n  };\n}\n\nconst COLOR_PALETTES: Record<AsciiHoverColor, AsciiColorPalette> = {\n  default: {\n    dark: {\n      text: \"#ffffff\",\n      glow: \"rgba(255, 255, 255, 0.4)\",\n    },\n    light: {\n      text: \"#09090b\",\n      glow: \"rgba(0, 0, 0, 0.12)\",\n    },\n  },\n  emerald: {\n    dark: {\n      text: \"#34d399\",\n      glow: \"rgba(52, 211, 153, 0.45)\",\n    },\n    light: {\n      text: \"#059669\",\n      glow: \"rgba(5, 150, 105, 0.25)\",\n    },\n  },\n  blue: {\n    dark: {\n      text: \"#60a5fa\",\n      glow: \"rgba(96, 165, 250, 0.45)\",\n    },\n    light: {\n      text: \"#2563eb\",\n      glow: \"rgba(37, 99, 235, 0.25)\",\n    },\n  },\n  violet: {\n    dark: {\n      text: \"#a78bfa\",\n      glow: \"rgba(167, 139, 250, 0.45)\",\n    },\n    light: {\n      text: \"#7c3aed\",\n      glow: \"rgba(124, 58, 237, 0.25)\",\n    },\n  },\n  cyan: {\n    dark: {\n      text: \"#38bdf8\",\n      glow: \"rgba(56, 189, 248, 0.45)\",\n    },\n    light: {\n      text: \"#0284c7\",\n      glow: \"rgba(2, 132, 199, 0.25)\",\n    },\n  },\n  amber: {\n    dark: {\n      text: \"#fbbf24\",\n      glow: \"rgba(251, 191, 36, 0.45)\",\n    },\n    light: {\n      text: \"#d97706\",\n      glow: \"rgba(217, 119, 6, 0.25)\",\n    },\n  },\n  rose: {\n    dark: {\n      text: \"#fb7185\",\n      glow: \"rgba(251, 113, 133, 0.45)\",\n    },\n    light: {\n      text: \"#e11d48\",\n      glow: \"rgba(225, 29, 72, 0.25)\",\n    },\n  },\n};\n\nconst DEFAULT_ASCII_CHARS = \"!<>-_\\\\/[]{}—=+*^?#_$%&~:;0123456789\";\n\nexport interface AsciiGlitchTextHandle {\n  trigger: () => void;\n  reset: () => void;\n}\n\nexport interface AsciiGlitchTextProps {\n  text: string;\n  isHovered?: boolean;\n  active?: boolean;\n  color?: AsciiHoverColor | string;\n  duration?: number;\n  asciiChars?: string;\n  className?: string;\n}\n\nexport const AsciiGlitchText = React.forwardRef<\n  AsciiGlitchTextHandle,\n  AsciiGlitchTextProps\n>(\n  (\n    {\n      text,\n      isHovered = false,\n      active = false,\n      color = \"default\",\n      duration = 0.75,\n      asciiChars = DEFAULT_ASCII_CHARS,\n      className,\n    },\n    ref,\n  ) => {\n    const [displayChars, setDisplayChars] = useState<string[]>(() =>\n      text.split(\"\"),\n    );\n    const [prevText, setPrevText] = useState(text);\n    const progressRef = useRef({ progress: 0 });\n    const lastUpdateRef = useRef(0);\n    const tweenRef = useRef<gsap.core.Tween | null>(null);\n\n    if (prevText !== text) {\n      setPrevText(text);\n      setDisplayChars(text.split(\"\"));\n    }\n\n    const isPresetColor = color in COLOR_PALETTES;\n    const isCustomColor = !isPresetColor && Boolean(color);\n\n    const triggerGlitch = useCallback(() => {\n      if (typeof window === \"undefined\") return;\n\n      if (tweenRef.current) {\n        tweenRef.current.kill();\n      }\n\n      const reduceMotion = window.matchMedia(\n        \"(prefers-reduced-motion: reduce)\",\n      ).matches;\n\n      if (reduceMotion) {\n        setDisplayChars(text.split(\"\"));\n        return;\n      }\n\n      progressRef.current.progress = 0;\n      lastUpdateRef.current = 0;\n      const chars = text.split(\"\");\n      const total = chars.length;\n      const glyphs = asciiChars || DEFAULT_ASCII_CHARS;\n\n      tweenRef.current = gsap.to(progressRef.current, {\n        progress: 1,\n        duration,\n        ease: \"power2.out\",\n        onUpdate: () => {\n          const now = performance.now();\n          const p = progressRef.current.progress;\n\n          const shouldRegenerate =\n            now - lastUpdateRef.current >= 45 || p >= 0.98;\n          if (!shouldRegenerate && p < 0.98) return;\n          lastUpdateRef.current = now;\n\n          const updated = chars.map((char, index) => {\n            if (char === \" \") return \" \";\n            const threshold = (index + 0.6) / (total + 0.4);\n            if (p >= threshold) {\n              return char;\n            }\n            const randomIndex = Math.floor(Math.random() * glyphs.length);\n            return glyphs[randomIndex];\n          });\n          setDisplayChars(updated);\n        },\n        onComplete: () => {\n          setDisplayChars(chars);\n        },\n      });\n    }, [text, duration, asciiChars]);\n\n    const resetGlitch = useCallback(() => {\n      if (tweenRef.current) {\n        tweenRef.current.kill();\n      }\n      setDisplayChars(text.split(\"\"));\n    }, [text]);\n\n    useImperativeHandle(\n      ref,\n      () => ({\n        trigger: triggerGlitch,\n        reset: resetGlitch,\n      }),\n      [triggerGlitch, resetGlitch],\n    );\n\n    useEffect(() => {\n      if (isHovered) {\n        const frame = requestAnimationFrame(() => {\n          triggerGlitch();\n        });\n        return () => cancelAnimationFrame(frame);\n      }\n    }, [isHovered, triggerGlitch]);\n\n    useEffect(() => {\n      return () => {\n        if (tweenRef.current) {\n          tweenRef.current.kill();\n        }\n      };\n    }, []);\n\n    const highlighted = isHovered || active;\n\n    const customColorStyle = useMemo(() => {\n      if (!highlighted) return undefined;\n      if (isCustomColor) {\n        return {\n          color,\n          textShadow: `0 0 10px ${color}80`,\n        };\n      }\n      return undefined;\n    }, [highlighted, isCustomColor, color]);\n\n    const originalChars = useMemo(() => text.split(\"\"), [text]);\n\n    return (\n      <span\n        className={cn(\n          \"inline-flex items-center justify-center font-sans text-sm tracking-wider font-semibold select-none transition-colors duration-300 ease-out\",\n          !highlighted &&\n            \"text-zinc-400 hover:text-zinc-800 dark:text-zinc-400 dark:hover:text-zinc-100\",\n          highlighted &&\n            isPresetColor &&\n            color === \"default\" &&\n            \"text-zinc-950 dark:text-white\",\n          highlighted &&\n            isPresetColor &&\n            color === \"emerald\" &&\n            \"text-emerald-600 dark:text-emerald-400\",\n          highlighted &&\n            isPresetColor &&\n            color === \"blue\" &&\n            \"text-blue-600 dark:text-blue-400\",\n          highlighted &&\n            isPresetColor &&\n            color === \"violet\" &&\n            \"text-violet-600 dark:text-violet-400\",\n          highlighted &&\n            isPresetColor &&\n            color === \"cyan\" &&\n            \"text-cyan-600 dark:text-cyan-400\",\n          highlighted &&\n            isPresetColor &&\n            color === \"amber\" &&\n            \"text-amber-600 dark:text-amber-400\",\n          highlighted &&\n            isPresetColor &&\n            color === \"rose\" &&\n            \"text-rose-600 dark:text-rose-400\",\n          className,\n        )}\n        style={customColorStyle}\n        aria-label={text}\n      >\n        {displayChars.map((char, index) => {\n          const isGlitching = char !== originalChars[index];\n\n          return (\n            <span\n              key={index}\n              className={cn(\n                \"inline-block text-center transition-colors duration-150\",\n                isGlitching &&\n                  \"text-zinc-500 dark:text-zinc-300 opacity-90 font-mono\",\n              )}\n            >\n              {char === \" \" ? \"\\u00A0\" : char}\n            </span>\n          );\n        })}\n      </span>\n    );\n  },\n);\n\nAsciiGlitchText.displayName = \"AsciiGlitchText\";\n\nexport type AsciiHoverButtonVariant =\n  | \"segmented\"\n  | \"card\"\n  | \"outline\"\n  | \"ghost\";\n\nexport type AsciiHoverButtonSize = \"sm\" | \"default\" | \"lg\";\n\nexport interface AsciiHoverButtonProps extends Omit<\n  React.ButtonHTMLAttributes<HTMLButtonElement>,\n  \"color\"\n> {\n  text?: string;\n  variant?: AsciiHoverButtonVariant;\n  size?: AsciiHoverButtonSize;\n  color?: AsciiHoverColor | string;\n  active?: boolean;\n  asciiChars?: string;\n  scrambleDuration?: number;\n  leftIcon?: React.ReactNode;\n  rightIcon?: React.ReactNode;\n}\n\nconst sizeStyles: Record<AsciiHoverButtonSize, string> = {\n  sm: \"h-9 px-3.5 text-xs gap-1.5 rounded-xl\",\n  default: \"h-11 px-5 text-sm gap-2 rounded-2xl\",\n  lg: \"h-13 px-7 text-base gap-2.5 rounded-2xl\",\n};\n\nexport const AsciiHoverButton = React.forwardRef<\n  HTMLButtonElement,\n  AsciiHoverButtonProps\n>(\n  (\n    {\n      text = \"Explore\",\n      children,\n      variant = \"segmented\",\n      size = \"default\",\n      color = \"default\",\n      active = false,\n      asciiChars = DEFAULT_ASCII_CHARS,\n      scrambleDuration = 0.75,\n      leftIcon,\n      rightIcon,\n      className,\n      disabled,\n      onMouseEnter,\n      onMouseLeave,\n      ...props\n    },\n    ref,\n  ) => {\n    const [isHovered, setIsHovered] = useState(false);\n    const glitchRef = useRef<AsciiGlitchTextHandle>(null);\n\n    const handleMouseEnter = (e: React.MouseEvent<HTMLButtonElement>) => {\n      if (disabled) return;\n      setIsHovered(true);\n      glitchRef.current?.trigger();\n      onMouseEnter?.(e);\n    };\n\n    const handleMouseLeave = (e: React.MouseEvent<HTMLButtonElement>) => {\n      setIsHovered(false);\n      onMouseLeave?.(e);\n    };\n\n    const buttonText = typeof children === \"string\" ? children : text;\n\n    return (\n      <button\n        ref={ref}\n        disabled={disabled}\n        onMouseEnter={handleMouseEnter}\n        onMouseLeave={handleMouseLeave}\n        className={cn(\n          \"relative inline-flex items-center justify-center font-medium transition-all duration-200 cursor-pointer select-none overflow-hidden active:scale-[0.97] disabled:opacity-50 disabled:pointer-events-none disabled:cursor-not-allowed\",\n          sizeStyles[size],\n          variant === \"segmented\" &&\n            (active\n              ? \"bg-white text-zinc-950 border border-zinc-200/90 shadow-[0_1px_3px_rgba(0,0,0,0.1),0_1px_2px_rgba(0,0,0,0.06)] dark:bg-[#18181b] dark:text-white dark:border-white/20 dark:shadow-[0_2px_10px_rgba(0,0,0,0.6),inset_0_1px_0_rgba(255,255,255,0.15)]\"\n              : \"bg-zinc-100/70 border border-transparent text-zinc-600 hover:bg-zinc-200/80 hover:text-zinc-900 dark:bg-zinc-900/60 dark:text-zinc-400 dark:hover:bg-zinc-800/80 dark:hover:text-zinc-100\"),\n          variant === \"card\" &&\n            \"bg-zinc-100 text-zinc-900 border border-zinc-200/80 shadow-sm hover:border-zinc-300 hover:bg-zinc-200/60 dark:bg-[#121214] dark:text-zinc-100 dark:border-white/10 dark:hover:border-white/25 dark:hover:bg-zinc-900/90 dark:shadow-xl\",\n          variant === \"outline\" &&\n            \"border border-zinc-300 text-zinc-700 hover:border-zinc-400 hover:bg-zinc-100/80 dark:border-white/20 dark:text-zinc-300 dark:hover:border-white/40 dark:hover:bg-white/5\",\n          variant === \"ghost\" &&\n            \"text-zinc-600 hover:bg-zinc-100 hover:text-zinc-900 dark:text-zinc-400 dark:hover:bg-white/5 dark:hover:text-zinc-100\",\n          className,\n        )}\n        {...props}\n      >\n        {leftIcon && (\n          <span className=\"relative z-10 shrink-0 transition-transform duration-200 group-hover:scale-105\">\n            {leftIcon}\n          </span>\n        )}\n        <AsciiGlitchText\n          ref={glitchRef}\n          text={buttonText}\n          isHovered={isHovered}\n          active={active}\n          color={color}\n          duration={scrambleDuration}\n          asciiChars={asciiChars}\n        />\n        {rightIcon && (\n          <span className=\"relative z-10 shrink-0 transition-transform duration-200 group-hover:scale-105\">\n            {rightIcon}\n          </span>\n        )}\n      </button>\n    );\n  },\n);\n\nAsciiHoverButton.displayName = \"AsciiHoverButton\";\n\nexport default AsciiHoverButton;\n",
      "type": "registry:ui",
      "target": "@ui/ascii-hover-button.tsx"
    }
  ]
}
