import { RefObject, useEffect, useRef } from 'react'; import { useStore } from '../services/store'; /** Duration the widget must be continuously visible before the timer condition is met. */ const ENGAGED_DURATION_MS = 1000; /** * Module-level guard: prevents widgetObserved from firing more than once per * widget instance, even when React StrictMode double-mounts in development. */ const engagedViewFired = new Set(); /** * Fires a single `widgetObserved` event when the widget has been ENTIRELY * visible in the viewport for at least 1 second continuously. * * The timer resets whenever any part of the widget leaves the viewport. * * @param widgetRef - Ref attached to the widget root element. */ export default function useWidgetObserved(widgetRef: RefObject): void { const store = useStore(); const { data, triggerEvent } = store; const widgetId = data?.id; const layout = data?.settings?.type; const timerRef = useRef | undefined>(undefined); const firedRef = useRef(false); useEffect(() => { const el = widgetRef.current; if (!widgetId || !el) return () => {}; // Skip setup if the event has already fired for this widget instance. if (engagedViewFired.has(widgetId)) { firedRef.current = true; return () => {}; } // ── Full-visibility timer ────────────────────────────────────────────────── // The timer starts only when the entire widget is visible (threshold: 1.0). // If any part of it leaves the viewport before the second elapses, the // timer is cancelled and will only restart on the next full-visibility entry. const observer = new IntersectionObserver( (entries) => { const [entry] = entries; if (!entry) return; if (entry.isIntersecting && !timerRef.current && !firedRef.current) { timerRef.current = setTimeout(() => { timerRef.current = undefined; if (!firedRef.current) { engagedViewFired.add(widgetId); firedRef.current = true; triggerEvent('widgetObserved', { layout }, widgetId); } }, ENGAGED_DURATION_MS); } else if (!entry.isIntersecting && timerRef.current) { clearTimeout(timerRef.current); timerRef.current = undefined; } }, { threshold: 1.0 }, ); observer.observe(el); return () => { observer.disconnect(); if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = undefined; } }; }, [widgetId, layout, widgetRef, triggerEvent]); }