| 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; |