{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"interactive-hover-button","type":"registry:ui","title":"InteractiveHoverButton","description":"Button whose dot opens into a circle that fills it on hover, while the label rolls up and out and returns with an arrow","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/interactive-hover-button/InteractiveHoverButton.tsx","content":"\"use client\";\n\nimport { forwardRef } from \"react\";\nimport type { ButtonHTMLAttributes, MouseEvent, ReactNode } 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 \"./interactive-hover-button.css\";\n\ntype BaseProps = {\n\t/** Button label text */\n\ttext?: string;\n\t/** Custom CSS class */\n\tclassName?: string;\n\t/** Button content (overrides text prop) */\n\tchildren?: ReactNode;\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\nexport interface InteractiveHoverButtonProps\n\textends BaseProps, Omit<ButtonHTMLAttributes<HTMLButtonElement>, keyof BaseProps> {}\n\n/*\n * Hover (or keyboard focus): the dot's circle opens until it fills the\n * button, the resting label rolls up and out, and the hover label rolls up\n * into place with its arrow a beat behind. Colours: `--ihb-fill` (the dot\n * and the fill) and `--ihb-fill-foreground` (the hover label), both\n * overridable from `className`. Under reduced motion the hover state arrives\n * instead of travelling.\n */\nexport const InteractiveHoverButton = forwardRef<HTMLButtonElement, InteractiveHoverButtonProps>(\n\t({ text = \"Button\", className, children, onClick, sound = false, ...restProps }, ref) => {\n\t\tconst label = children ?? text;\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: restProps.disabled,\n\t\t});\n\t\tconst composedRef = useComposedRefs(ref, soundRef);\n\n\t\tfunction handleClick(event: MouseEvent<HTMLButtonElement>) {\n\t\t\tif (sound && !restProps.disabled) playCue(\"press\");\n\t\t\tonClick?.(event);\n\t\t}\n\n\t\treturn (\n\t\t\t<button\n\t\t\t\tref={composedRef}\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"ihb group bg-background relative isolate w-auto cursor-pointer overflow-hidden rounded-full border px-6 py-2.5 text-center font-semibold\",\n\t\t\t\t\tclassName\n\t\t\t\t)}\n\t\t\t\tonClick={handleClick}\n\t\t\t\t{...restProps}\n\t\t\t>\n\t\t\t\t{/* The dot and the fill are one layer: a clipped circle that opens. */}\n\t\t\t\t<span className=\"ihb-fill\" aria-hidden=\"true\"></span>\n\n\t\t\t\t<span className=\"ihb-rest\">\n\t\t\t\t\t<span className=\"ihb-dot-space\" aria-hidden=\"true\"></span>\n\t\t\t\t\t<span className=\"ihb-label\">{label}</span>\n\t\t\t\t</span>\n\n\t\t\t\t<span aria-hidden=\"true\" className=\"ihb-hover\">\n\t\t\t\t\t<span className=\"ihb-hover-label\">{label}</span>\n\t\t\t\t\t<svg\n\t\t\t\t\t\tclassName=\"ihb-arrow\"\n\t\t\t\t\t\txmlns=\"http://www.w3.org/2000/svg\"\n\t\t\t\t\t\twidth=\"18\"\n\t\t\t\t\t\theight=\"18\"\n\t\t\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\t\t\tfill=\"none\"\n\t\t\t\t\t\tstroke=\"currentColor\"\n\t\t\t\t\t\tstrokeWidth=\"2\"\n\t\t\t\t\t\tstrokeLinecap=\"round\"\n\t\t\t\t\t\tstrokeLinejoin=\"round\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<path d=\"M5 12h14\" />\n\t\t\t\t\t\t<path d=\"m12 5 7 7-7 7\" />\n\t\t\t\t\t</svg>\n\t\t\t\t</span>\n\t\t\t</button>\n\t\t);\n\t}\n);\n\nInteractiveHoverButton.displayName = \"InteractiveHoverButton\";\n","type":"registry:ui","target":"@ui/fancy/components/interactive-hover-button/InteractiveHoverButton.tsx"},{"path":"react/src/components/interactive-hover-button/index.ts","content":"\"use client\";\n\nexport { InteractiveHoverButton } from \"./InteractiveHoverButton\";\nexport type { InteractiveHoverButtonProps } from \"./InteractiveHoverButton\";\n","type":"registry:ui","target":"@ui/fancy/components/interactive-hover-button/index.ts"},{"path":"react/src/components/interactive-hover-button/interactive-hover-button.css","content":".ihb {\n\t/* Private copies: read the public variables (set from `className`, which a\n\t   rule here would otherwise outrank), else the theme. */\n\t--_ihb-fill: var(--ihb-fill, var(--primary, var(--color-primary, currentColor)));\n\t--_ihb-fill-foreground: var(\n\t\t--ihb-fill-foreground,\n\t\tvar(--primary-foreground, var(--color-primary-foreground, #fff))\n\t);\n\t/* where the dot sits, vertically centred */\n\t--ihb-dot-x: 1.75rem;\n\t--ihb-ease: cubic-bezier(0.22, 1, 0.36, 1);\n}\n\n.ihb-fill {\n\tposition: absolute;\n\tinset: 0;\n\tz-index: -1;\n\tbackground: var(--_ihb-fill);\n\tclip-path: circle(4px at var(--ihb-dot-x) 50%);\n}\n\n.ihb-rest,\n.ihb-hover {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\tgap: 0.5rem;\n}\n\n.ihb-dot-space {\n\twidth: 8px;\n\tflex: none;\n}\n\n.ihb-hover {\n\tposition: absolute;\n\tinset: 0;\n\tcolor: var(--_ihb-fill-foreground);\n\topacity: 0;\n\ttransform: translateY(70%);\n}\n\n.ihb-arrow {\n\tflex: none;\n\topacity: 0;\n\ttransform: translateX(-6px);\n}\n\n/* ---- hover / keyboard focus ---------------------------------------------- */\n\n.ihb:hover .ihb-fill,\n.ihb:focus-visible .ihb-fill {\n\tclip-path: circle(150% at var(--ihb-dot-x) 50%);\n}\n\n.ihb:hover .ihb-rest,\n.ihb:focus-visible .ihb-rest {\n\topacity: 0;\n\ttransform: translateY(-70%);\n\tfilter: blur(4px);\n}\n\n.ihb:hover .ihb-hover,\n.ihb:focus-visible .ihb-hover {\n\topacity: 1;\n\ttransform: none;\n}\n\n.ihb:hover .ihb-arrow,\n.ihb:focus-visible .ihb-arrow {\n\topacity: 1;\n\ttransform: none;\n}\n\n.ihb:focus-visible {\n\toutline: 2px solid var(--_ihb-fill);\n\toutline-offset: 2px;\n}\n\n.ihb:disabled {\n\tcursor: not-allowed;\n\topacity: 0.5;\n}\n\n.ihb:disabled .ihb-fill {\n\tclip-path: circle(4px at var(--ihb-dot-x) 50%);\n}\n\n.ihb:disabled .ihb-rest {\n\topacity: 1;\n\ttransform: none;\n\tfilter: none;\n}\n\n.ihb:disabled .ihb-hover {\n\topacity: 0;\n}\n\n/* ---- motion: only when the visitor has not asked for less ----------------- */\n\n@media (prefers-reduced-motion: no-preference) {\n\t.ihb {\n\t\ttransition: transform 0.2s var(--ihb-ease);\n\t}\n\n\t.ihb:active {\n\t\ttransform: scale(0.97);\n\t}\n\n\t.ihb-fill {\n\t\ttransition: clip-path 0.6s var(--ihb-ease);\n\t}\n\n\t.ihb-rest {\n\t\ttransition:\n\t\t\topacity 0.3s var(--ihb-ease),\n\t\t\ttransform 0.5s var(--ihb-ease),\n\t\t\tfilter 0.5s var(--ihb-ease);\n\t}\n\n\t.ihb-hover {\n\t\ttransition:\n\t\t\topacity 0.3s var(--ihb-ease),\n\t\t\ttransform 0.5s var(--ihb-ease);\n\t}\n\n\t.ihb-arrow {\n\t\ttransition:\n\t\t\topacity 0.3s var(--ihb-ease),\n\t\t\ttransform 0.5s var(--ihb-ease);\n\t}\n\n\t/* On the way in, stagger: label a beat after the fill, arrow after it. */\n\t.ihb:hover .ihb-hover,\n\t.ihb:focus-visible .ihb-hover {\n\t\ttransition-delay: 60ms;\n\t}\n\n\t.ihb:hover .ihb-arrow,\n\t.ihb:focus-visible .ihb-arrow {\n\t\ttransition-delay: 160ms;\n\t}\n}\n","type":"registry:ui","target":"@ui/fancy/components/interactive-hover-button/interactive-hover-button.css"}],"meta":{"license":"MIT","package":"fancy-ui-react","version":"0.7.0","source":"https://fancy-ui.rama.app/docs/components/interactive-hover-button?framework=react"}}