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}
{[...Array(repetitions)].map((_, idx) => (
{marqueeText || text}
{image &&
}
))}
);
}