{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"rainbow-button","type":"registry:ui","title":"RainbowButton","description":"Animated button with a rainbow gradient border; on hover or focus a rainbow beam rides around the border with a soft halo. Other native attributes are forwarded to the rendered element","author":"fancy-ui <https://fancy-ui.rama.app>","categories":["buttons"],"registryDependencies":["https://fancy-ui.rama.app/r/internals-dom.json","https://fancy-ui.rama.app/r/sound.json","https://fancy-ui.rama.app/r/utils.json"],"files":[{"path":"react/src/components/rainbow-button/RainbowButton.tsx","content":"\"use client\";\n\nimport { forwardRef } from \"react\";\nimport type {\n\tAnchorHTMLAttributes,\n\tButtonHTMLAttributes,\n\tCSSProperties,\n\tMouseEvent,\n\tReactNode,\n\tRef,\n} from \"react\";\nimport { cn } from \"../../utils\";\nimport { useSoundCue } from \"../../sound/use-sound\";\nimport { useSoundFeedback } from \"../../sound/sound-feedback\";\nimport { useComposedRefs } from \"../../internals/dom/use-composed-refs\";\nimport \"./rainbow-button.css\";\n\ntype BaseProps = {\n\t/** Animation speed in seconds */\n\tspeed?: number;\n\t/** Custom CSS class */\n\tclassName?: string;\n\t/** Render as anchor element */\n\thref?: string;\n\t/** Button content */\n\tchildren?: ReactNode;\n\t/** Click handler, called once per activation and never while `disabled` */\n\tonClick?: (event: MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => void;\n\t/**\n\t * Plays the `hover` cue on mouse/pen pointer enter and the `press` cue on\n\t * click. Off by default; only audible once the user has enabled sound.\n\t */\n\tsound?: boolean;\n};\n\n// The two attribute sets are intersected BEFORE `Omit`, then extended as one:\n// an interface may not extend two types that name the same property with\n// different signatures (`onAbort` and friends differ between button and\n// anchor), while an intersection tolerates it. Same resulting type as the\n// alias this replaces — the interface form is the tooling contract in\n// PORTING.md.\ntype RainbowButtonElementProps = Omit<\n\tButtonHTMLAttributes<HTMLButtonElement> & AnchorHTMLAttributes<HTMLAnchorElement>,\n\tkeyof BaseProps\n>;\n\nexport interface RainbowButtonProps extends BaseProps, RainbowButtonElementProps {}\n\nexport const RainbowButton = forwardRef<HTMLButtonElement | HTMLAnchorElement, RainbowButtonProps>(\n\t(\n\t\t{\n\t\t\tclassName,\n\t\t\tspeed = 2,\n\t\t\thref,\n\t\t\ttarget,\n\t\t\tstyle,\n\t\t\ttype = \"button\",\n\t\t\tdisabled,\n\t\t\tchildren,\n\t\t\tonClick,\n\t\t\tsound = false,\n\t\t\t...restProps\n\t\t},\n\t\tref\n\t) => {\n\t\tconst speedStyle = { \"--rainbow-speed\": `${speed}s`, ...style } as CSSProperties;\n\t\tconst playCue = useSoundCue(sound);\n\n\t\t// Hover cue for mouse/pen pointers only. `on: {}` binds nothing when sound is\n\t\t// off (and never the default click → press, which `handleClick` already plays).\n\t\tconst soundRef = useSoundFeedback({\n\t\t\ton: sound ? { pointerenter: \"hover\" } : {},\n\t\t\tdisabled,\n\t\t});\n\t\tconst composedRef = useComposedRefs(ref, soundRef);\n\n\t\t// The single guard both branches funnel through. A native `disabled`\n\t\t// button already refuses real input, but a synthetic click (or an anchor,\n\t\t// which has no disabled state at all) walks straight past that, so a\n\t\t// disabled click is cancelled here and never reaches the consumer or the\n\t\t// sound cue.\n\t\tfunction handleClick(event: MouseEvent<HTMLButtonElement | HTMLAnchorElement>) {\n\t\t\tif (disabled) {\n\t\t\t\tevent.preventDefault();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tplayCue(\"press\");\n\t\t\tonClick?.(event);\n\t\t}\n\n\t\tconst baseClasses = cn(\n\t\t\t\"rainbow-button\",\n\t\t\t\"group relative inline-flex h-11 cursor-pointer items-center justify-center rounded-xl border-0 bg-[length:200%] px-8 py-2 font-medium transition-colors [background-clip:padding-box,border-box,border-box] [background-origin:border-box] [border:calc(0.08*1rem)_solid_transparent] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50\",\n\t\t\t// Glow effect\n\t\t\t\"before:absolute before:bottom-[-20%] before:left-1/2 before:z-0 before:h-1/5 before:w-3/5 before:-translate-x-1/2 before:animate-rainbow before:bg-[linear-gradient(90deg,var(--rainbow-1),var(--rainbow-5),var(--rainbow-3),var(--rainbow-4),var(--rainbow-2))] before:bg-[length:200%] before:[filter:blur(calc(0.8*1rem))]\",\n\t\t\t// Light mode: dark button with light text\n\t\t\t\"text-white bg-[linear-gradient(#121213,#121213),linear-gradient(#121213_50%,rgba(18,18,19,0.6)_80%,rgba(18,18,19,0)),linear-gradient(90deg,var(--rainbow-1),var(--rainbow-5),var(--rainbow-3),var(--rainbow-4),var(--rainbow-2))]\",\n\t\t\t// Dark mode: light button with dark text\n\t\t\t\"dark:text-black dark:bg-[linear-gradient(#fff,#fff),linear-gradient(#fff_50%,rgba(255,255,255,0.6)_80%,rgba(0,0,0,0)),linear-gradient(90deg,var(--rainbow-1),var(--rainbow-5),var(--rainbow-3),var(--rainbow-4),var(--rainbow-2))]\",\n\t\t\tclassName\n\t\t);\n\n\t\tif (href) {\n\t\t\treturn (\n\t\t\t\t<a\n\t\t\t\t\tref={composedRef as Ref<HTMLAnchorElement>}\n\t\t\t\t\t{...(restProps as AnchorHTMLAttributes<HTMLAnchorElement>)}\n\t\t\t\t\tclassName={baseClasses}\n\t\t\t\t\tstyle={speedStyle}\n\t\t\t\t\thref={disabled ? undefined : href}\n\t\t\t\t\ttarget={disabled ? undefined : target}\n\t\t\t\t\taria-disabled={disabled ? \"true\" : restProps[\"aria-disabled\"]}\n\t\t\t\t\trole={disabled ? \"link\" : restProps.role}\n\t\t\t\t\ttabIndex={disabled ? -1 : restProps.tabIndex}\n\t\t\t\t\tonClick={handleClick}\n\t\t\t\t>\n\t\t\t\t\t<span className=\"rainbow-button__glow\" aria-hidden=\"true\">\n\t\t\t\t\t\t<span></span>\n\t\t\t\t\t</span>\n\t\t\t\t\t<span className=\"rainbow-button__beam\" aria-hidden=\"true\"></span>\n\t\t\t\t\t{children}\n\t\t\t\t</a>\n\t\t\t);\n\t\t}\n\n\t\treturn (\n\t\t\t<button\n\t\t\t\tref={composedRef as Ref<HTMLButtonElement>}\n\t\t\t\t{...(restProps as ButtonHTMLAttributes<HTMLButtonElement>)}\n\t\t\t\tclassName={baseClasses}\n\t\t\t\tstyle={speedStyle}\n\t\t\t\ttype={type}\n\t\t\t\tdisabled={disabled}\n\t\t\t\tonClick={handleClick}\n\t\t\t>\n\t\t\t\t<span className=\"rainbow-button__glow\" aria-hidden=\"true\">\n\t\t\t\t\t<span></span>\n\t\t\t\t</span>\n\t\t\t\t<span className=\"rainbow-button__beam\" aria-hidden=\"true\"></span>\n\t\t\t\t{children}\n\t\t\t</button>\n\t\t);\n\t}\n);\n\nRainbowButton.displayName = \"RainbowButton\";\n","type":"registry:ui","target":"@ui/fancy/components/rainbow-button/RainbowButton.tsx"},{"path":"react/src/components/rainbow-button/index.ts","content":"\"use client\";\n\nexport { RainbowButton } from \"./RainbowButton\";\nexport type { RainbowButtonProps } from \"./RainbowButton\";\n","type":"registry:ui","target":"@ui/fancy/components/rainbow-button/index.ts"},{"path":"react/src/components/rainbow-button/rainbow-button.css","content":".rainbow-button {\n\t--rainbow-1: hsl(0 100% 63%);\n\t--rainbow-2: hsl(270 100% 63%);\n\t--rainbow-3: hsl(210 100% 63%);\n\t--rainbow-4: hsl(195 100% 63%);\n\t--rainbow-5: hsl(90 100% 63%);\n}\n\n@keyframes rainbow {\n\t0% {\n\t\tbackground-position: 0%;\n\t}\n\t100% {\n\t\tbackground-position: 200%;\n\t}\n}\n\n.animate-rainbow {\n\tanimation: rainbow var(--rainbow-speed, 2s) infinite linear;\n}\n\n.rainbow-button::before {\n\tanimation: rainbow var(--rainbow-speed, 2s) infinite linear;\n}\n\n/*\n * Hover beam: a rainbow light that rides the border. One conic gradient,\n * turned through a registered angle, drawn twice — masked to the border ring\n * (the beam itself) and blurred around the button (its halo).\n */\n@property --beam-angle {\n\tsyntax: \"<angle>\";\n\tinherits: true;\n\tinitial-value: 0deg;\n}\n\n.rainbow-button {\n\t--beam-width: 1.5px;\n\t--glow-reach: 6px;\n\t--glow-blur: 10px;\n\t--beam: conic-gradient(\n\t\tfrom var(--beam-angle),\n\t\ttransparent 0%,\n\t\tvar(--rainbow-4) 5%,\n\t\tvar(--rainbow-3) 10%,\n\t\tvar(--rainbow-2) 15%,\n\t\tvar(--rainbow-1) 20%,\n\t\thsl(35 100% 60%) 24%,\n\t\ttransparent 31%,\n\t\ttransparent 50%,\n\t\tvar(--rainbow-5) 55%,\n\t\tvar(--rainbow-4) 60%,\n\t\tvar(--rainbow-3) 64%,\n\t\ttransparent 71%\n\t);\n}\n\n.rainbow-button .rainbow-button__beam,\n.rainbow-button .rainbow-button__glow {\n\tposition: absolute;\n\tborder-radius: inherit;\n\tpointer-events: none;\n\topacity: 0;\n\ttransition: opacity 0.35s ease;\n}\n\n/* keep only the padding ring: the element minus its content box */\n.rainbow-button .rainbow-button__beam,\n.rainbow-button .rainbow-button__glow > span {\n\tbackground: var(--beam);\n\t-webkit-mask:\n\t\tlinear-gradient(#000 0 0) content-box,\n\t\tlinear-gradient(#000 0 0);\n\t-webkit-mask-composite: xor;\n\tmask:\n\t\tlinear-gradient(#000 0 0) content-box,\n\t\tlinear-gradient(#000 0 0);\n\tmask-composite: exclude;\n}\n\n/* the beam itself, on the border */\n.rainbow-button .rainbow-button__beam {\n\tinset: calc(-1 * var(--beam-width));\n\tpadding: var(--beam-width);\n\tz-index: 1;\n}\n\n/* its halo: a wider ring, blurred AFTER masking so both edges go soft */\n.rainbow-button .rainbow-button__glow {\n\tinset: 0;\n\tfilter: blur(var(--glow-blur));\n}\n\n.rainbow-button .rainbow-button__glow > span {\n\tposition: absolute;\n\tinset: calc(-1 * var(--glow-reach));\n\tpadding: var(--glow-reach);\n\tborder-radius: calc(0.75rem + var(--glow-reach));\n}\n\n.rainbow-button:hover .rainbow-button__beam,\n.rainbow-button:focus-visible .rainbow-button__beam {\n\topacity: 1;\n}\n\n.rainbow-button:hover .rainbow-button__glow,\n.rainbow-button:focus-visible .rainbow-button__glow {\n\topacity: 0.6;\n}\n\n.rainbow-button:hover,\n.rainbow-button:focus-visible {\n\tanimation: beam-spin calc(var(--rainbow-speed, 2s) * 1.5) linear infinite;\n}\n\n@keyframes beam-spin {\n\tto {\n\t\t--beam-angle: 360deg;\n\t}\n}\n\n@media (prefers-reduced-motion: reduce) {\n\t.rainbow-button:hover,\n\t.rainbow-button:focus-visible {\n\t\tanimation: none;\n\t\t--beam-angle: 200deg;\n\t}\n}\n","type":"registry:ui","target":"@ui/fancy/components/rainbow-button/rainbow-button.css"}],"meta":{"license":"MIT","package":"fancy-ui-react","version":"0.7.0","source":"https://fancy-ui.rama.app/docs/components/rainbow-button?framework=react"}}