reactbits-mcp-server
Version:
MCP Server for React Bits - Access 99+ React components with animations, backgrounds, and UI elements
119 lines (107 loc) • 3.89 kB
JSX
import React from 'react';
import { gsap } from 'gsap';
function FlowingMenu({ items = [] }) {
return (
<div className="w-full h-full overflow-hidden">
<nav className="flex flex-col h-full m-0 p-0">
{items.map((item, idx) => (
<MenuItem key={idx} {...item} />
))}
</nav>
</div>
);
}
function MenuItem({ link, text, image }) {
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 = Array.from({ length: 4 }).map((_, idx) => (
<React.Fragment key={idx}>
<span className="text-[#060010] uppercase font-normal text-[4vh] leading-[1.2] p-[1vh_1vw_0]">
{text}
</span>
<div
className="w-[200px] h-[7vh] my-[2em] mx-[2vw] p-[1em_0] rounded-[50px] bg-cover bg-center"
style={{ backgroundImage: `url(${image})` }}
/>
</React.Fragment>
));
return (
<div className="flex-1 relative overflow-hidden text-center shadow-[0_-1px_0_0_#fff]" ref={itemRef}>
<a
className="flex items-center justify-center h-full relative cursor-pointer uppercase no-underline font-semibold text-white text-[4vh] hover:text-[#060010] focus:text-white focus-visible:text-[#060010]"
href={link}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{text}
</a>
<div
className="absolute top-0 left-0 w-full h-full overflow-hidden pointer-events-none bg-white translate-y-[101%]"
ref={marqueeRef}
>
<div className="h-full w-[200%] flex" ref={marqueeInnerRef}>
<div className="flex items-center relative h-full w-[200%] will-change-transform animate-marquee">
{repeatedMarqueeContent}
</div>
</div>
</div>
</div>
);
}
export default FlowingMenu;
// Note: this is also needed
// /** @type {import('tailwindcss').Config} */
// export default {
// content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
// theme: {
// extend: {
// translate: {
// '101': '101%',
// },
// keyframes: {
// marquee: {
// 'from': { transform: 'translateX(0%)' },
// 'to': { transform: 'translateX(-50%)' }
// }
// },
// animation: {
// marquee: 'marquee 15s linear infinite'
// }
// }
// },
// plugins: [],
// };