"use client"; import * as React from "react"; import { Slot, Slottable } from "@radix-ui/react-slot"; import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; const buttonVariants = cva( "relative isolate inline-flex items-center justify-center gap-2 overflow-hidden whitespace-nowrap rounded-nested text-sm font-medium ring-offset-background transition-[background-color,border-color,color,box-shadow] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:relative [&_svg]:z-[1]", { variants: { variant: { default: "bg-primary text-primary-foreground shadow-sm hover:bg-primary/92 [--button-origin-overlay:hsl(var(--primary-foreground)/0.32)]", secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/90 [--button-origin-overlay:hsl(var(--foreground)/0.12)]", outline: "border border-input bg-background hover:border-border-strong hover:bg-accent hover:text-accent-foreground [--button-origin-overlay:hsl(var(--foreground)/0.1)]", ghost: "text-muted-foreground hover:bg-muted hover:text-foreground [--button-origin-overlay:hsl(var(--foreground)/0.1)]", link: "rounded-none text-primary underline-offset-4 hover:underline", destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90 [--button-origin-overlay:hsl(var(--destructive-foreground)/0.26)]", }, size: { default: "h-10 px-4 py-2", sm: "h-9 px-3", lg: "h-11 px-6", icon: "h-10 w-10", }, }, defaultVariants: { variant: "default", size: "default", }, }, ); export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { asChild?: boolean; cursorAware?: boolean; } type ButtonOriginStyle = React.CSSProperties & { "--button-origin-size"?: string; "--button-origin-overlay"?: string; }; type ButtonOriginState = { left: string; top: string; scale: number; opacity: number; durationMs: number; easing: string; }; const Button = React.forwardRef( ( { className, variant, size, asChild = false, cursorAware = variant !== "link", onPointerEnter, onPointerLeave, style, children, ...props }, ref, ) => { const Comp = asChild ? Slot : "button"; const originStyle = style as ButtonOriginStyle | undefined; const resolvedStyle: ButtonOriginStyle = { "--button-origin-size": originStyle?.["--button-origin-size"] ?? "0px", ...originStyle, }; const [origin, setOrigin] = React.useState({ left: "50%", top: "50%", scale: 0.35, opacity: 0, durationMs: 180, easing: "cubic-bezier(0.22, 1, 0.36, 1)", }); const updateOrigin = React.useCallback((target: EventTarget | null, clientX: number, clientY: number) => { if (!(target instanceof HTMLElement)) { return 0; } const rect = target.getBoundingClientRect(); const size = Math.max(rect.height * 1.35, Math.min(rect.width * 0.58, 88)); const left = clientX - rect.left - size / 2; const top = clientY - rect.top - size / 2; target.style.setProperty("--button-origin-size", `${size}px`); return { left: `${left}px`, top: `${top}px`, }; }, []); const handlePointerEnter = React.useCallback( (event: React.PointerEvent) => { onPointerEnter?.(event); if (!cursorAware || event.defaultPrevented) { return; } const nextOrigin = updateOrigin(event.currentTarget, event.clientX, event.clientY); if (!nextOrigin) { return; } setOrigin({ left: nextOrigin.left, top: nextOrigin.top, scale: 1, opacity: 1, durationMs: 220, easing: "cubic-bezier(0.16, 1, 0.3, 1)", }); }, [cursorAware, onPointerEnter, updateOrigin], ); const handlePointerLeave = React.useCallback( (event: React.PointerEvent) => { onPointerLeave?.(event); if (!cursorAware || event.defaultPrevented) { return; } const nextOrigin = updateOrigin(event.currentTarget, event.clientX, event.clientY); if (!nextOrigin) { return; } setOrigin({ left: nextOrigin.left, top: nextOrigin.top, scale: 0.4, opacity: 0, durationMs: 160, easing: "cubic-bezier(0.4, 0, 1, 1)", }); }, [cursorAware, onPointerLeave, updateOrigin], ); return ( {cursorAware && variant !== "link" ? ( ); }, ); Button.displayName = "Button"; export { Button, buttonVariants };