{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "slug": "ascii-hover-button",
  "name": "ASCII Hover Button",
  "title": "ASCII Hover Button",
  "type": "registry:ui",
  "description": "Button with color transitions and an ASCII character scramble on hover.",
  "summary": "An interactive button with an ASCII cipher scramble effect powered by GSAP. On hover the label shifts color and scrambles through a set of ASCII glyphs before resolving. Supports a segmented style with an active state, preset and custom colors, and light and dark modes.",
  "category": "buttons",
  "tags": [
    "button",
    "ascii",
    "text-hover",
    "glitch",
    "scramble",
    "segmented",
    "tabs",
    "micro-interactions",
    "gsap"
  ],
  "dependencies": [
    "gsap",
    "@gsap/react",
    "clsx",
    "tailwind-merge"
  ],
  "version": "1.0.0",
  "createdDate": "2026-10-01",
  "updatedDate": "2026-10-01",
  "interactive": true,
  "supportsColor": true,
  "highlights": [
    "GSAP progress tween (power2.out) driving glyph scrambles, re-rolled every 45ms",
    "Per-character spans; scrambling glyphs render in monospace",
    "Text color and glow transitions in light and dark modes",
    "Four surface variants (segmented, card, outline, ghost) with an active prop for selected segments",
    "Seven preset palettes plus arbitrary hex/rgb color overrides",
    "Respects prefers-reduced-motion by skipping the scramble"
  ],
  "anatomy": [
    "<AsciiHoverButton> (Native button with variant, size and icon slots)",
    "<AsciiGlitchText> (Text primitive that renders per-character spans and the scramble overlay)"
  ],
  "physics": {
    "engine": "GSAP Core Tween",
    "description": "Progressive character resolution driven by a GSAP power2.out tween.",
    "parameters": [
      {
        "label": "Scramble Duration",
        "value": "750ms (scrambleDuration)"
      },
      {
        "label": "Tween Easing",
        "value": "power2.out"
      },
      {
        "label": "Glyph Re-roll",
        "value": "every 45ms"
      },
      {
        "label": "Active Scale",
        "value": "scale-[0.97]"
      }
    ]
  },
  "accessibility": {
    "role": "button",
    "aria": "Native button. The glitch text span has aria-label set to the label text so screen readers read the real label during the scramble.",
    "reducedMotion": "Bypasses ASCII glyph scramble and renders target text immediately when prefers-reduced-motion is active.",
    "keyboard": [
      {
        "key": "Tab",
        "description": "Focus the button (browser default focus outline)."
      },
      {
        "key": "Enter / Space",
        "description": "Activate the button."
      }
    ]
  },
  "guidelines": {
    "recommended": [
      "Timeframe selectors (1D, 7D, 1M, 1Y, ALL) in analytics, crypto, and finance charts",
      "High-tech call-to-action triggers on developer tools, AI platforms, and cyberpunk aesthetics",
      "Segmented view switchers built from several buttons using the active prop"
    ],
    "bestPractices": [
      "Pair with monospace or clean geometric sans-serif fonts for optimal aesthetic cohesion",
      "Use preset colors or vibrant custom colors that maintain high contrast in both dark and light modes",
      "Keep text concise (1-16 characters) for snappy visual impact"
    ]
  },
  "props": [
    {
      "name": "text",
      "type": "string",
      "defaultValue": "\"Explore\"",
      "description": "Label text to display and scramble on hover."
    },
    {
      "name": "color",
      "type": "\"default\" | \"emerald\" | \"blue\" | \"violet\" | \"amber\" | \"rose\" | \"cyan\" | string",
      "defaultValue": "\"default\"",
      "description": "Active and hover text accent color preset or custom hex string."
    },
    {
      "name": "active",
      "type": "boolean",
      "defaultValue": "false",
      "description": "Keeps the button in its highlighted state, for example to mark the selected segment."
    },
    {
      "name": "variant",
      "type": "\"segmented\" | \"card\" | \"outline\" | \"ghost\"",
      "defaultValue": "\"segmented\"",
      "description": "Visual surface style for standalone buttons."
    },
    {
      "name": "size",
      "type": "\"sm\" | \"default\" | \"lg\"",
      "defaultValue": "\"default\"",
      "description": "Size dimensions and font scale."
    },
    {
      "name": "scrambleDuration",
      "type": "number",
      "defaultValue": "0.75",
      "description": "Duration in seconds for the ASCII glitch scramble transition."
    },
    {
      "name": "leftIcon",
      "type": "ReactNode",
      "description": "Icon rendered before the label."
    },
    {
      "name": "rightIcon",
      "type": "ReactNode",
      "description": "Icon rendered after the label."
    },
    {
      "name": "asciiChars",
      "type": "string",
      "defaultValue": "\"!<>-_\\\\/[]{}—=+*^?#_$%&~:;0123456789\"",
      "description": "Character set used for the random glitch animation."
    }
  ],
  "usage": "import { AsciiHoverButton } from \"@/components/ui/ascii-hover-button\";\n\nexport function AsciiHoverButtonDemo() {\n  return <AsciiHoverButton text=\"Explore\" variant=\"outline\" />;\n}",
  "files": [
    {
      "name": "ascii-hover-button.tsx",
      "path": "registry/ui/ascii-hover-button.tsx",
      "type": "registry:ui",
      "target": "@ui/ascii-hover-button.tsx",
      "code": "\"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",
      "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"
    }
  ]
}
