Nitish kumar
Upload folder using huggingface_hub
c20f20c verified
'use client';
import { useRef, useState, useLayoutEffect, useCallback } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import { useSceneSelector } from '@/lib/contexts/scene-context';
import { useCanvasStore } from '@/lib/store/canvas';
import type { SlideContent } from '@/lib/types/stage';
import type { PPTElement } from '@/lib/types/slides';
interface SpotlightRect {
x: number;
y: number;
w: number;
h: number;
}
/**
* Spotlight overlay component
*
* Uses DOM measurement (getBoundingClientRect) to compute spotlight position,
* avoiding alignment offsets from percentage coordinate conversion.
*/
export function SpotlightOverlay() {
const spotlightElementId = useCanvasStore.use.spotlightElementId();
const spotlightOptions = useCanvasStore.use.spotlightOptions();
const containerRef = useRef<HTMLDivElement>(null);
const [rect, setRect] = useState<SpotlightRect | null>(null);
const elements = useSceneSelector<SlideContent, PPTElement[]>(
(content) => content.canvas.elements,
);
// Compute target element position in SVG coordinate system via DOM measurement
const measure = useCallback(() => {
if (!spotlightElementId || !containerRef.current) {
setRect(null);
return;
}
const domElement = document.getElementById(`screen-element-${spotlightElementId}`);
if (!domElement) {
setRect(null);
return;
}
// Prefer measuring .element-content (the actual rendered area for auto-height)
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;
}
// Convert to SVG viewBox 0-100 coordinates
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]);
useLayoutEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect -- DOM measurement requires effect
measure();
}, [measure, elements]);
const active = !!spotlightElementId && !!spotlightOptions && !!rect;
const dimness = spotlightOptions?.dimness ?? 0.7;
return (
<div
ref={containerRef}
className="absolute inset-0 z-[100] pointer-events-none overflow-hidden"
>
<AnimatePresence mode="wait">
{active && rect && (
<motion.div
key={`spotlight-${spotlightElementId}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="absolute inset-0"
>
<svg
width="100%"
height="100%"
viewBox="0 0 100 100"
preserveAspectRatio="none"
className="absolute inset-0"
>
<defs>
<mask id={`mask-${spotlightElementId}`}>
{/* White background = show mask layer (dimmed) */}
<rect x="0" y="0" width="100" height="100" fill="white" />
{/* Black rectangle = hide mask layer (highlighted area / cutout) */}
<motion.rect
fill="black"
initial={{
x: rect.x - 8,
y: rect.y - 8,
width: rect.w + 16,
height: rect.h + 16,
rx: 4,
}}
animate={{
x: rect.x - 0.4,
y: rect.y - 0.6,
width: rect.w + 0.8,
height: rect.h + 1.2,
rx: 1,
}}
transition={{
duration: 0.6,
ease: [0.16, 1, 0.3, 1],
}}
/>
</mask>
</defs>
{/* Dimmed Background */}
<rect
width="100"
height="100"
fill={`rgba(0,0,0,${dimness})`}
mask={`url(#mask-${spotlightElementId})`}
className="backdrop-blur-[1.5px]"
/>
{/* THE ONE BORDER - white border */}
<motion.rect
initial={{
x: rect.x - 4,
y: rect.y - 4,
width: rect.w + 8,
height: rect.h + 8,
opacity: 0,
rx: 2,
}}
animate={{
x: rect.x - 0.4,
y: rect.y - 0.6,
width: rect.w + 0.8,
height: rect.h + 1.2,
opacity: 1,
rx: 1,
}}
fill="none"
stroke="rgba(255,255,255,0.7)"
strokeWidth="1.2"
style={{ vectorEffect: 'non-scaling-stroke' } as React.CSSProperties}
transition={{
duration: 0.5,
delay: 0.05,
ease: [0.16, 1, 0.3, 1],
}}
/>
</svg>
</motion.div>
)}
</AnimatePresence>
</div>
);
}