File size: 3,125 Bytes
373c769 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
import React from 'react';
import { gsap } from 'gsap';
function FlowingMenu({ items = [], activeItem, onItemClick }) {
return (
<div className="flowing-menu-container">
<nav className="flowing-menu-nav">
{items.map((item, idx) => (
<MenuItem
key={item.id}
link="#"
text={item.label}
image={`https://picsum.photos/600/400?random=${idx + 1}`}
isActive={activeItem === item.id}
onClick={() => onItemClick(item.id)}
icon={item.icon}
/>
))}
</nav>
</div>
);
}
function MenuItem({ link, text, image, isActive, onClick, icon }) {
const itemRef = React.useRef(null);
const marqueeRef = React.useRef(null);
const marqueeInnerRef = React.useRef(null);
const animationDefaults = { duration: 0.6, ease: 'expo' };
const findClosestEdge = (mouseX, mouseY, width, height) => {
const topEdgeDist = (mouseX - width / 2) ** 2 + mouseY ** 2;
const bottomEdgeDist = (mouseX - width / 2) ** 2 + (mouseY - height) ** 2;
return topEdgeDist < bottomEdgeDist ? 'top' : 'bottom';
};
const handleMouseEnter = (ev) => {
if (!itemRef.current || !marqueeRef.current || !marqueeInnerRef.current) return;
const rect = itemRef.current.getBoundingClientRect();
const edge = findClosestEdge(
ev.clientX - rect.left,
ev.clientY - rect.top,
rect.width,
rect.height
);
gsap.timeline({ defaults: animationDefaults })
.set(marqueeRef.current, { y: edge === 'top' ? '-101%' : '101%' })
.set(marqueeInnerRef.current, { y: edge === 'top' ? '101%' : '-101%' })
.to([marqueeRef.current, marqueeInnerRef.current], { y: '0%' });
};
const handleMouseLeave = (ev) => {
if (!itemRef.current || !marqueeRef.current || !marqueeInnerRef.current) return;
const rect = itemRef.current.getBoundingClientRect();
const edge = findClosestEdge(
ev.clientX - rect.left,
ev.clientY - rect.top,
rect.width,
rect.height
);
gsap.timeline({ defaults: animationDefaults })
.to(marqueeRef.current, { y: edge === 'top' ? '-101%' : '101%' })
.to(marqueeInnerRef.current, { y: edge === 'top' ? '101%' : '-101%' });
};
const repeatedMarqueeContent = [];
return (
<div className={`flowing-menu-item ${isActive ? 'active' : ''}`} ref={itemRef}>
<a
className="menu-item-link"
href={link}
onClick={(e) => {
e.preventDefault();
onClick();
}}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<span className="menu-icon">{icon}</span>
<span className="menu-text">{text}</span>
</a>
<div className="marquee-overlay" ref={marqueeRef}>
<div className="marquee-inner" ref={marqueeInnerRef}>
<div className="marquee-content">
{repeatedMarqueeContent}
</div>
</div>
</div>
</div>
);
}
export default FlowingMenu; |