{
  "name": "button-scramble",
  "type": "registry:ui",
  "tier": "free",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://www.craftui.space/r/craftui-base.json",
    "craftui-base"
  ],
  "files": [
    {
      "path": "components/craftui/ui/button/scramble.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { useReducedMotion } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/components/ui/button\";\n\nexport interface ScrambleButtonProps\n  extends Omit<React.ComponentProps<typeof Button>, \"children\"> {\n  /** The label — scrambled on hover, then revealed character-by-character. */\n  label?: string;\n  /** Characters shown in place of not-yet-revealed characters. */\n  charset?: string;\n  /** Milliseconds between scramble frames. */\n  frameDuration?: number;\n  /** Frames each subsequent character waits before locking in — creates the left-to-right reveal. */\n  staggerFrames?: number;\n}\n\nconst DEFAULT_CHARSET = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ!<>-_\\\\/[]{}=+*^?#\";\n\nfunction randomChar(charset: string) {\n  return charset[Math.floor(Math.random() * charset.length)];\n}\n\nexport const ScrambleButton = React.forwardRef<\n  HTMLButtonElement,\n  ScrambleButtonProps\n>(function ScrambleButton(\n  {\n    label = \"Whimsy\",\n    charset = DEFAULT_CHARSET,\n    frameDuration = 35,\n    staggerFrames = 3,\n    className,\n    onMouseEnter,\n    onMouseLeave,\n    onFocus,\n    onBlur,\n    ...props\n  },\n  ref,\n) {\n  const reduced = useReducedMotion();\n  const [display, setDisplay] = React.useState(label);\n  const frameRef = React.useRef(0);\n  const intervalRef = React.useRef<ReturnType<typeof setInterval> | null>(\n    null,\n  );\n\n  const stop = React.useCallback(() => {\n    if (intervalRef.current !== null) {\n      clearInterval(intervalRef.current);\n      intervalRef.current = null;\n    }\n  }, []);\n\n  const scramble = React.useCallback(() => {\n    if (reduced) return;\n    stop();\n    frameRef.current = 0;\n    const totalFrames = label.length * staggerFrames + staggerFrames * 2;\n\n    intervalRef.current = setInterval(() => {\n      const frame = frameRef.current++;\n      let next = \"\";\n      for (let i = 0; i < label.length; i++) {\n        const char = label[i];\n        if (char === \" \") {\n          next += \" \";\n          continue;\n        }\n        const revealFrame = i * staggerFrames;\n        next += frame >= revealFrame ? char : randomChar(charset);\n      }\n      setDisplay(next);\n\n      if (frame >= totalFrames) stop();\n    }, frameDuration);\n  }, [label, charset, frameDuration, staggerFrames, reduced, stop]);\n\n  React.useEffect(() => {\n    setDisplay(label);\n    return stop;\n  }, [label, stop]);\n\n  return (\n    <Button\n      ref={ref}\n      className={cn(\"font-mono rounded-[12px]\", className)}\n      onMouseEnter={(e) => {\n        scramble();\n        onMouseEnter?.(e);\n      }}\n      onMouseLeave={(e) => {\n        stop();\n        setDisplay(label);\n        onMouseLeave?.(e);\n      }}\n      onFocus={(e) => {\n        scramble();\n        onFocus?.(e);\n      }}\n      onBlur={(e) => {\n        stop();\n        setDisplay(label);\n        onBlur?.(e);\n      }}\n      {...props}\n    >\n      {display}\n    </Button>\n  );\n});\n",
      "type": "registry:ui",
      "target": "~/components/craftui/ui/button/scramble.tsx"
    }
  ],
  "meta": {
    "displayName": "Button - Scramble Text",
    "description": "On hover or focus, the label's characters cycle rapidly through random glyphs before locking in to the real text one character at a time, left to right — a decode-style reveal built entirely from content substitution, no positional or color motion.",
    "category": "button",
    "tags": [
      "button",
      "cta",
      "hover",
      "click",
      "sparkle",
      "particles",
      "confetti",
      "scramble",
      "offset-path",
      "whimsical"
    ],
    "animationType": "keyframes"
  }
}