'use client'; import * as React from 'react'; import { Slot } from '@radix-ui/react-slot'; import { cva, type VariantProps } from 'class-variance-authority'; import { cn } from '@/lib/utils'; const buttonVariants = cva( 'group inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium transition-all duration-fast ease-standard disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed [&_svg]:pointer-events-none [&_svg:not([class*="size-"])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-2 focus-visible:ring-offset-bg-primary min-h-[44px] min-w-[44px] touch-manipulation relative overflow-hidden select-none', { variants: { variant: { primary: 'bg-brand text-white shadow-sm hover:bg-brand-hover hover:shadow-brand-hover hover:-translate-y-px active:translate-y-0 active:scale-[0.97] active:shadow-sm transition-all duration-fast ease-standard', secondary: 'bg-dark-bg text-white shadow-sm hover:bg-dark-bg-secondary hover:shadow-md hover:-translate-y-px active:translate-y-0 active:scale-[0.97] active:shadow-sm transition-all duration-fast ease-standard', outline: 'border border-border-accent bg-transparent text-text-primary hover:bg-dark-bg hover:text-white hover:border-dark-bg hover:-translate-y-px hover:shadow-md active:translate-y-0 active:scale-[0.97] active:shadow-sm transition-all duration-fast ease-standard', ghost: 'text-text-secondary hover:bg-bg-secondary hover:text-text-primary active:scale-[0.97] transition-all duration-fast ease-standard', link: 'text-text-primary underline-offset-4 hover:text-brand relative after:absolute after:bottom-0 after:left-0 after:h-[2px] after:w-0 hover:after:w-full after:bg-brand after:transition-all after:duration-normal after:ease-standard', destructive: 'bg-error text-white shadow-sm hover:bg-error-hover hover:-translate-y-px active:translate-y-0 active:scale-[0.97] transition-all duration-fast ease-standard', }, size: { sm: 'h-9 rounded-full px-3 text-xs font-medium', default: 'h-11 rounded-full px-5 py-2 font-medium', lg: 'h-12 rounded-full px-6 text-base font-semibold', xl: 'h-14 rounded-full px-8 text-base font-semibold', icon: 'h-11 w-11 rounded-full', }, }, defaultVariants: { variant: 'primary', size: 'default', }, } ); export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { asChild?: boolean; showRipple?: boolean; } const Button = React.forwardRef( ({ className, variant, size, asChild = false, onClick, children, showRipple = true, ...props }, ref) => { const [ripples, setRipples] = React.useState<{ x: number; y: number; id: number }[]>([]); const handleClick = (e: React.MouseEvent) => { if (showRipple && variant !== 'link' && variant !== 'ghost') { const button = e.currentTarget; const rect = button.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; const id = Date.now(); setRipples((prev) => [...prev, { x, y, id }]); setTimeout(() => { setRipples((prev) => prev.filter((r) => r.id !== id)); }, 600); } onClick?.(e); }; // asChild 模式:Slot 要求单个 React 元素子节点,因此跳过 ripple 和包裹 span。 // 直接把 children 交给 Slot,由 Slot 克隆并注入 className/onClick 等 props。 if (asChild) { return ( {children} ); } return ( ); } ); Button.displayName = 'Button'; export { Button, buttonVariants };