'use client'; import { useRef, useState, useLayoutEffect, useCallback } from 'react'; import { motion, AnimatePresence } from 'motion/react'; import type { SpotlightEffectOptions } from '../types/effects'; interface SpotlightRect { x: number; y: number; w: number; h: number; } export interface SpotlightOverlayProps { options?: SpotlightEffectOptions; /** ID prefix the SlideElement uses on its root div. Default `slide-element-`. */ elementIdPrefix?: string; /** Remeasure when host element data changes without changing the target ID. */ measurementKey?: unknown; } export function SpotlightOverlay({ options, elementIdPrefix = 'slide-element-', measurementKey, }: SpotlightOverlayProps) { const containerRef = useRef(null); const [rect, setRect] = useState(null); const spotlightElementId = options?.elementId; const measure = useCallback(() => { if (!spotlightElementId || !containerRef.current) { setRect(null); return; } const domElement = document.getElementById(`${elementIdPrefix}${spotlightElementId}`); if (!domElement) { setRect(null); return; } const contentEl = domElement.querySelector('.element-content'); const targetEl = contentEl ?? domElement; const containerRect = containerRef.current.getBoundingClientRect(); const targetRect = targetEl.getBoundingClientRect(); if (containerRect.width === 0 || containerRect.height === 0) { setRect(null); return; } setRect({ x: ((targetRect.left - containerRect.left) / containerRect.width) * 100, y: ((targetRect.top - containerRect.top) / containerRect.height) * 100, w: (targetRect.width / containerRect.width) * 100, h: (targetRect.height / containerRect.height) * 100, }); }, [spotlightElementId, elementIdPrefix]); useLayoutEffect(() => { // eslint-disable-next-line react-hooks/set-state-in-effect -- DOM measurement requires effect measure(); const container = containerRef.current; if (!container || !spotlightElementId) return; const root = document.getElementById(`${elementIdPrefix}${spotlightElementId}`); const target = root?.querySelector('.element-content') ?? root; if (!target) return; const observer = new ResizeObserver(measure); observer.observe(container); observer.observe(target); const media = target.querySelectorAll('img, video'); media.forEach((element) => { element.addEventListener('load', measure); element.addEventListener('loadedmetadata', measure); }); return () => { observer.disconnect(); media.forEach((element) => { element.removeEventListener('load', measure); element.removeEventListener('loadedmetadata', measure); }); }; }, [measure, measurementKey, spotlightElementId, elementIdPrefix]); const active = !!spotlightElementId && !!rect; const dimness = options?.dimness ?? 0.7; return (
{active && rect && ( )}
); }