function FlowingMenu({ items = [], speed = 15, textColor = '#120F17', bgColor = '#ffffff', marqueeBgColor = '#120F17', marqueeTextColor = '#ffffff', borderColor = '#e5e5e5' }) { const [activeMobileIndex, setActiveMobileIndex] = React.useState(null); const containerRef = React.useRef(null); React.useEffect(() => { if (window.innerWidth >= 900) return; const observer = new IntersectionObserver( (entries) => { entries.forEach(entry => { const idx = Number(entry.target.dataset.index); if (entry.isIntersecting) { setActiveMobileIndex(idx); } else { setActiveMobileIndex(prev => prev === idx ? null : prev); } }); }, { rootMargin: "-45% 0px -45% 0px", threshold: 0 } ); const menuItems = containerRef.current?.querySelectorAll('.menu__item'); menuItems?.forEach(item => observer.observe(item)); return () => observer.disconnect(); }, []); return (
); } function MenuItem({ index, isMobileActive, link, text, marqueeText, image, speed, textColor, marqueeBgColor, marqueeTextColor, borderColor }) { const itemRef = React.useRef(null); const marqueeRef = React.useRef(null); const marqueeInnerRef = React.useRef(null); const animationRef = React.useRef(null); const wasActiveRef = React.useRef(false); const [repetitions, setRepetitions] = React.useState(4); const animationDefaults = { duration: 0.6, ease: 'expo' }; const findClosestEdge = (mouseX, mouseY, width, height) => { const topEdgeDist = distMetric(mouseX, mouseY, width / 2, 0); const bottomEdgeDist = distMetric(mouseX, mouseY, width / 2, height); return topEdgeDist < bottomEdgeDist ? 'top' : 'bottom'; }; const distMetric = (x, y, x2, y2) => { const xDiff = x - x2; const yDiff = y - y2; return xDiff * xDiff + yDiff * yDiff; }; React.useEffect(() => { const calculateRepetitions = () => { if (!marqueeInnerRef.current) return; const marqueeContent = marqueeInnerRef.current.querySelector('.marquee__part'); if (!marqueeContent) return; const contentWidth = marqueeContent.offsetWidth; const viewportWidth = window.innerWidth; const needed = Math.ceil(viewportWidth / contentWidth) + 2; setRepetitions(Math.max(4, needed)); }; calculateRepetitions(); window.addEventListener('resize', calculateRepetitions); return () => window.removeEventListener('resize', calculateRepetitions); }, [text, image]); React.useEffect(() => { const setupMarquee = () => { if (!marqueeInnerRef.current) return; const marqueeContent = marqueeInnerRef.current.querySelector('.marquee__part'); if (!marqueeContent) return; const contentWidth = marqueeContent.offsetWidth; if (contentWidth === 0) return; if (animationRef.current) { animationRef.current.kill(); } animationRef.current = window.gsap.to(marqueeInnerRef.current, { x: -contentWidth, duration: speed, ease: 'none', repeat: -1 }); }; const timer = setTimeout(setupMarquee, 50); return () => { clearTimeout(timer); if (animationRef.current) { animationRef.current.kill(); } }; }, [text, image, repetitions, speed]); const lastYRef = React.useRef(typeof window !== 'undefined' ? window.scrollY : 0); // Mobile scroll auto-hover logic React.useEffect(() => { if (window.innerWidth >= 900) return; const currentY = window.scrollY; const scrollingDown = currentY > lastYRef.current; lastYRef.current = currentY; if (isMobileActive && !wasActiveRef.current) { const edge = scrollingDown ? 'bottom' : 'top'; window.gsap .timeline({ defaults: animationDefaults }) .set(marqueeRef.current, { y: edge === 'top' ? '-101%' : '101%' }, 0) .set(marqueeInnerRef.current, { y: edge === 'top' ? '101%' : '-101%' }, 0) .to([marqueeRef.current, marqueeInnerRef.current], { y: '0%' }, 0); wasActiveRef.current = true; } else if (!isMobileActive && wasActiveRef.current) { const edge = scrollingDown ? 'top' : 'bottom'; window.gsap .timeline({ defaults: animationDefaults }) .to(marqueeRef.current, { y: edge === 'top' ? '-101%' : '101%' }, 0) .to(marqueeInnerRef.current, { y: edge === 'top' ? '101%' : '-101%' }, 0); wasActiveRef.current = false; } }, [isMobileActive]); const handleMouseEnter = ev => { if (window.innerWidth < 900) return; // Disable hover interaction on mobile if (!itemRef.current || !marqueeRef.current || !marqueeInnerRef.current) return; const rect = itemRef.current.getBoundingClientRect(); const x = ev.clientX - rect.left; const y = ev.clientY - rect.top; const edge = findClosestEdge(x, y, rect.width, rect.height); window.gsap .timeline({ defaults: animationDefaults }) .set(marqueeRef.current, { y: edge === 'top' ? '-101%' : '101%' }, 0) .set(marqueeInnerRef.current, { y: edge === 'top' ? '101%' : '-101%' }, 0) .to([marqueeRef.current, marqueeInnerRef.current], { y: '0%' }, 0); }; const handleMouseLeave = ev => { if (window.innerWidth < 900) return; if (!itemRef.current || !marqueeRef.current || !marqueeInnerRef.current) return; const rect = itemRef.current.getBoundingClientRect(); const x = ev.clientX - rect.left; const y = ev.clientY - rect.top; const edge = findClosestEdge(x, y, rect.width, rect.height); window.gsap .timeline({ defaults: animationDefaults }) .to(marqueeRef.current, { y: edge === 'top' ? '-101%' : '101%' }, 0) .to(marqueeInnerRef.current, { y: edge === 'top' ? '101%' : '-101%' }, 0); }; return (
{text}
); }