'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 (
)
}