import { useEffect, useRef, type CSSProperties, type ReactNode } from 'react'; import { prefersReducedMotion } from './utils.js'; export interface SpotlightProps { /** Slot rendered inside the spotlight wrapper. */ children: ReactNode; /** Spotlight radius in px. Default 320. */ size?: number; /** Gradient center colour. Defaults to `var(--bl-accent)` with .22 alpha. */ color?: string; /** Background base colour beneath the spotlight. Defaults to transparent. */ baseColor?: string; /** Bypass motion entirely (renders static `baseColor`). */ disableMotion?: boolean; className?: string; style?: CSSProperties; } /** * `` — radial gradient that follows the pointer. * * Wraps any block; on `pointermove` the gradient centre tracks the cursor * via two CSS custom properties (`--bl-spot-x` / `--bl-spot-y`). The CSS * variable update is the cheapest possible re-paint trigger — no React * re-renders, no setState in the listener. * * Honours `prefers-reduced-motion` (renders the static base layer * with the spotlight pinned to the centre). * * @example * ```tsx * *

Hover me

*
* ``` */ export function Spotlight({ children, size = 320, color = 'color-mix(in srgb, var(--bl-accent, #6366f1) 22%, transparent)', baseColor = 'transparent', disableMotion, className, style, }: SpotlightProps) { const ref = useRef(null); const reduced = disableMotion ?? prefersReducedMotion(); useEffect(() => { if (reduced) return; const el = ref.current; if (!el) return; const onMove = (e: PointerEvent) => { const rect = el.getBoundingClientRect(); el.style.setProperty('--bl-spot-x', `${e.clientX - rect.left}px`); el.style.setProperty('--bl-spot-y', `${e.clientY - rect.top}px`); }; const onLeave = () => { el.style.removeProperty('--bl-spot-x'); el.style.removeProperty('--bl-spot-y'); }; el.addEventListener('pointermove', onMove); el.addEventListener('pointerleave', onLeave); return () => { el.removeEventListener('pointermove', onMove); el.removeEventListener('pointerleave', onLeave); }; }, [reduced]); return (
{children}
); }