'use client' import { useRef, type ButtonHTMLAttributes, type PointerEvent, type ReactNode } from 'react' import { cn } from '../../lib/cn' export type ShimmerTone = 'accent' | 'ink' | 'glass' const TONES: Record = { accent: { shell: 'bg-accent text-accent-ink shadow-[var(--shadow-tile)] hover:bg-accent-strong', sheen: 'bg-white/70', glow: 'rgba(255,255,255,0.55)', }, ink: { shell: 'bg-ink text-ink-inverse shadow-[var(--shadow-float)]', sheen: 'bg-white/35', glow: 'rgba(200,242,78,0.45)', }, glass: { shell: 'border border-line-strong bg-surface text-ink shadow-[var(--shadow-tile)] hover:bg-surface-muted', sheen: 'bg-accent/45', glow: 'rgba(200,242,78,0.5)', }, } const SIZES = { sm: 'h-8 px-3.5 text-[12px]', md: 'h-10 px-5 text-[13px]', lg: 'h-12 px-6 text-[14px]', } as const export type ShimmerButtonProps = ButtonHTMLAttributes & { /** The button label. */ children: ReactNode tone?: ShimmerTone size?: keyof typeof SIZES /** Seconds between sweeps. */ duration?: number /** Sweep only on hover and focus, rather than continuously. */ onHover?: boolean /** Add a soft light that follows the pointer across the face. */ glow?: boolean /** Stretch to the container width. */ fullWidth?: boolean } /** * A call to action with a sheen sweeping across it and a light that follows * the pointer. * * Both effects are one extra span each, clipped by the button, so the label * keeps its own stacking context and stays selectable and readable throughout. * The pointer light is written to CSS custom properties on the element rather * than to React state: a pointer move fires far more often than a frame, and * routing it through state would re-render the whole subtree for a value only * the compositor reads. * * `onHover` exists because a sweep that never stops is fine on one hero button * and exhausting on a toolbar of six. */ export function ShimmerButton({ children, tone = 'accent', size = 'md', duration = 3, onHover = false, glow = true, fullWidth = false, className, onPointerMove, ...props }: ShimmerButtonProps) { const ref = useRef(null) const palette = TONES[tone] const handlePointerMove = (event: PointerEvent) => { onPointerMove?.(event) if (!glow) return const node = ref.current if (!node) return const rect = node.getBoundingClientRect() node.style.setProperty('--glow-x', `${event.clientX - rect.left}px`) node.style.setProperty('--glow-y', `${event.clientY - rect.top}px`) node.style.setProperty('--glow-opacity', '1') } return ( ) }