import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; type OverflowMarqueeProps = { children: React.ReactNode; className?: string; textClassName?: string; title?: string; speedPxPerSec?: number; minDurationSec?: number; }; type MarqueeMetrics = { isOverflowing: boolean; overflowPx: number; durationSec: number; }; const DEFAULT_SPEED_PX_PER_SEC = 24; const DEFAULT_MIN_DURATION_SEC = 4; const OverflowMarquee: React.FC = ({ children, className, textClassName, title, speedPxPerSec = DEFAULT_SPEED_PX_PER_SEC, minDurationSec = DEFAULT_MIN_DURATION_SEC, }) => { const containerRef = useRef(null); const textRef = useRef(null); const [metrics, setMetrics] = useState({ isOverflowing: false, overflowPx: 0, durationSec: minDurationSec, }); const fallbackTitle = useMemo(() => { if (title) return title; return typeof children === 'string' ? children : undefined; }, [children, title]); const recompute = () => { const container = containerRef.current; const text = textRef.current; if (!container || !text) return; // Ensure we measure with current layout. const available = container.clientWidth; const content = text.scrollWidth; const overflowPx = Math.ceil(content - available); if (overflowPx > 1) { const durationSec = Math.max(minDurationSec, overflowPx / Math.max(1, speedPxPerSec)); setMetrics({ isOverflowing: true, overflowPx, durationSec }); } else { // Avoid re-render loops if already not overflowing. setMetrics((prev) => (prev.isOverflowing ? { isOverflowing: false, overflowPx: 0, durationSec: minDurationSec } : prev)); } }; useLayoutEffect(() => { recompute(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [children]); useEffect(() => { recompute(); const container = containerRef.current; const text = textRef.current; if (!container || !text) return; const ro = new ResizeObserver(() => recompute()); ro.observe(container); ro.observe(text); const onResize = () => recompute(); window.addEventListener('resize', onResize); return () => { ro.disconnect(); window.removeEventListener('resize', onResize); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const textStyle: React.CSSProperties | undefined = metrics.isOverflowing ? ({ ['--marquee-distance' as any]: `${metrics.overflowPx}px`, ['--marquee-duration' as any]: `${metrics.durationSec}s`, } satisfies React.CSSProperties) : undefined; return ( {children} ); }; export default OverflowMarquee;