import { useEffect, useRef, RefObject } from 'react'; /** * Among all elements matching the active-thumbnail data attribute, return the * one that is actually visible on screen. Infinite-scroll carousels duplicate * slides as clones, so multiple DOM nodes can carry the marker at the same * time — but only one of them sits inside the carousel's visible area. */ const findVisibleActive = (): HTMLElement | null => { const candidates = document.querySelectorAll( '[data-adl-thumbnail-active]', ); if (candidates.length <= 1) return candidates[0] ?? null; const vw = window.innerWidth; for (const el of candidates) { const r = el.getBoundingClientRect(); // The element's horizontal center must be within the viewport. const cx = r.left + r.width / 2; if (cx >= 0 && cx <= vw) return el; } return null; }; /** * Directly syncs the position, size, border-radius, and background-image of * the visible active thumbnail from the widget DOM onto a portal clone element * via direct style mutation. * * Direct DOM mutation means the clone updates in the same animation frame as * the position read, giving pixel-perfect sync even during CSS transitions. * * The RAF loop only runs while the hook is mounted (i.e. while the post * viewer is open), so the performance impact is negligible. */ const useActiveThumbnailRect = ( cloneRef: RefObject, /** Changing this value (e.g. a new post) resets the loop. */ dep: unknown, ): void => { const rafRef = useRef(0); useEffect(() => { const tick = () => { const clone = cloneRef.current; const source = findVisibleActive(); if (clone && source) { const r = source.getBoundingClientRect(); const bgImage = source.style.backgroundImage || ''; if (bgImage) { clone.style.display = 'block'; clone.style.top = `${r.top}px`; clone.style.left = `${r.left}px`; clone.style.width = `${r.width}px`; clone.style.height = `${r.height}px`; clone.style.backgroundImage = bgImage; clone.style.borderRadius = getComputedStyle(source).borderRadius; } else { clone.style.display = 'none'; } } else if (clone) { clone.style.display = 'none'; } rafRef.current = requestAnimationFrame(tick); }; rafRef.current = requestAnimationFrame(tick); return () => cancelAnimationFrame(rafRef.current); // eslint-disable-next-line react-hooks/exhaustive-deps }, [dep]); }; export default useActiveThumbnailRect;