{
    "name": "fillable-button",
    "title": "Fillable Button",
    "description": "A button that fills with color on hover.",
    "type": "registry:ui",
    "dependencies": ["gsap", "@gsap/react"],
    "files": [
        {
            "path": "components/gsap/fillable-button.tsx",
            "type": "registry:ui",
            "target": "~/components/gsap/fillable-button.tsx",
            "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { type VariantProps, cva } from \"class-variance-authority\";\nimport gsap from \"gsap\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst overlayVariants = cva(\n    \"pointer-events-none absolute left-0 top-0 block aspect-square w-[170%] -translate-x-1/2 -translate-y-1/2 rounded-full\",\n    {\n        variants: {\n            variant: {\n                default: \"bg-black dark:bg-white\",\n                outline: \"bg-zinc-300 dark:bg-zinc-600\",\n                destructive: \"bg-destructive\",\n                secondary: \"bg-secondary-foreground\",\n                ghost: \"bg-muted\",\n                link: \"\",\n            },\n        },\n        defaultVariants: {\n            variant: \"default\",\n        },\n    },\n);\n\nconst buttonVariants = cva(\n    \"cursor-pointer inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-colors\",\n    {\n        variants: {\n            variant: {\n                default:\n                    \"relative bg-primary text-primary-foreground dark:hover:text-black overflow-hidden rounded-full! px-6 py-3.5 tracking-tight after:absolute after:inset-0 after:rounded-full after:pointer-events-none after:content-[''] \",\n                outline:\n                    \"relative overflow-hidden rounded-full bg-transparent text-foreground px-6 py-3.5 tracking-tight after:absolute after:inset-0 after:rounded-full after:border-2 after:border-zinc-300 dark:after:border-zinc-600 after:pointer-events-none after:content-[''] \",\n                destructive:\n                    \"relative bg-destructive/10 dark:bg-destructive/20 text-destructive hover:text-background overflow-hidden rounded-full! px-6 py-3.5 tracking-tight\",\n                secondary:\n                    \"relative bg-secondary text-secondary-foreground hover:text-background overflow-hidden rounded-full! px-6 py-3.5 tracking-tight\",\n                ghost: \"relative bg-transparent text-foreground hover:text-accent-foreground overflow-hidden rounded-full! px-6 py-3.5 tracking-tight\",\n                link: \"relative text-primary\",\n            },\n            size: {\n                default: \"h-9 px-4 py-2 has-[>svg]:px-3\",\n                xs: \"h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3\",\n                sm: \"h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5\",\n                lg: \"h-10 rounded-md px-6 has-[>svg]:px-4\",\n                icon: \"size-9 p-0\",\n                \"icon-xs\": \"size-6 rounded-md p-0 [&_svg:not([class*='size-'])]:size-3\",\n                \"icon-sm\": \"size-8 p-0\",\n                \"icon-lg\": \"size-10 p-0\",\n            },\n        },\n        defaultVariants: {\n            variant: \"default\",\n            size: \"default\",\n        },\n    },\n);\n\nfunction FillableButton({\n    className,\n    variant = \"default\",\n    size = \"default\",\n    children,\n    overlayClassname,\n    ...props\n}: React.ComponentProps<\"button\"> &\n    VariantProps<typeof buttonVariants> & {\n        overlayClassname?: string;\n    }) {\n    const buttonRef = React.useRef<HTMLButtonElement>(null);\n    const flairRef = React.useRef<HTMLSpanElement>(null);\n    const underlineRef = React.useRef<HTMLSpanElement>(null);\n\n    React.useEffect(() => {\n        const button = buttonRef.current;\n        if (!button) return;\n\n        if (variant === \"link\") {\n            const underline = underlineRef.current;\n            if (!underline) return;\n\n            const getXPercent = (e: MouseEvent) => {\n                const { left, width } = button!.getBoundingClientRect();\n                return gsap.utils.clamp(0, 100, ((e.clientX - left) / width) * 100);\n            };\n\n            const handleEnter = (e: MouseEvent) => {\n                const xPercent = getXPercent(e);\n                gsap.killTweensOf(underline);\n                gsap.fromTo(\n                    underline,\n                    { scaleX: 0, transformOrigin: `${xPercent}% center` },\n                    { scaleX: 1, duration: 0.3, ease: \"power2.out\" },\n                );\n            };\n\n            const handleLeave = (e: MouseEvent) => {\n                const xPercent = getXPercent(e);\n                gsap.killTweensOf(underline);\n                gsap.to(underline, {\n                    scaleX: 0,\n                    transformOrigin: `${xPercent}% center`,\n                    duration: 0.25,\n                    ease: \"power2.in\",\n                });\n            };\n\n            button.addEventListener(\"mouseenter\", handleEnter);\n            button.addEventListener(\"mouseleave\", handleLeave);\n\n            return () => {\n                button.removeEventListener(\"mouseenter\", handleEnter);\n                button.removeEventListener(\"mouseleave\", handleLeave);\n                gsap.killTweensOf(underline);\n            };\n        }\n\n        const flair = flairRef.current;\n        if (!flair) return;\n\n        const xSet = gsap.quickSetter(flair, \"xPercent\");\n        const ySet = gsap.quickSetter(flair, \"yPercent\");\n\n        function getXY(e: MouseEvent) {\n            if (!button) return;\n            const { left, top, width, height } = button.getBoundingClientRect();\n\n            const xTransformer = gsap.utils.pipe(gsap.utils.mapRange(0, width, 0, 100), gsap.utils.clamp(0, 100));\n\n            const yTransformer = gsap.utils.pipe(gsap.utils.mapRange(0, height, 0, 100), gsap.utils.clamp(0, 100));\n\n            return {\n                x: xTransformer(e.clientX - left),\n                y: yTransformer(e.clientY - top),\n            };\n        }\n\n        function onMouseEnter(e: MouseEvent) {\n            const pos = getXY(e);\n            if (!pos) return;\n            const { x, y } = pos;\n            xSet(x);\n            ySet(y);\n\n            gsap.to(flair, {\n                scale: 1,\n                duration: 0.4,\n                ease: \"power2.out\",\n            });\n        }\n\n        function onMouseLeave(e: MouseEvent) {\n            const pos = getXY(e);\n            if (!pos) return;\n            const { x, y } = pos;\n\n            gsap.killTweensOf(flair);\n\n            gsap.to(flair, {\n                xPercent: x > 90 ? x + 20 : x < 10 ? x - 20 : x,\n                yPercent: y > 90 ? y + 20 : y < 10 ? y - 20 : y,\n                scale: 0,\n                duration: 0.3,\n                ease: \"power2.out\",\n            });\n        }\n\n        function onMouseMove(e: MouseEvent) {\n            const pos = getXY(e);\n            if (!pos) return;\n            const { x, y } = pos;\n\n            gsap.to(flair, {\n                xPercent: x,\n                yPercent: y,\n                duration: 0.4,\n                ease: \"power2\",\n            });\n        }\n\n        button.addEventListener(\"mouseenter\", onMouseEnter);\n        button.addEventListener(\"mouseleave\", onMouseLeave);\n        button.addEventListener(\"mousemove\", onMouseMove);\n\n        return () => {\n            button.removeEventListener(\"mouseenter\", onMouseEnter);\n            button.removeEventListener(\"mouseleave\", onMouseLeave);\n            button.removeEventListener(\"mousemove\", onMouseMove);\n            gsap.killTweensOf(flair);\n        };\n    }, [variant]);\n\n    return (\n        <button\n            ref={buttonRef}\n            data-slot=\"button\"\n            data-variant={variant}\n            data-size={size}\n            className={cn(buttonVariants({ variant, size, className }))}\n            {...props}>\n            {variant !== \"link\" && (\n                <span\n                    ref={flairRef}\n                    className=\"pointer-events-none absolute inset-0 origin-top-left scale-0 will-change-transform\">\n                    <span className={cn(overlayVariants({ variant }), overlayClassname)} />\n                </span>\n            )}\n            <span className=\"relative z-10 flex items-center gap-2\">\n                {children}\n                {variant === \"link\" && (\n                    <span\n                        ref={underlineRef}\n                        className=\"absolute bottom-0 left-0 h-[1.5px] w-full scale-x-0 bg-current\"\n                    />\n                )}\n            </span>\n        </button>\n    );\n}\n\nexport { FillableButton, buttonVariants };\n"
        }
    ]
}
