reactbits-mcp-server
Version:
MCP Server for React Bits - Access 99+ React components with animations, backgrounds, and UI elements
183 lines (160 loc) • 5.52 kB
JSX
import { useRef, useEffect, useState } from "react";
import "./GooeyNav.css";
const GooeyNav = ({
items,
animationTime = 600,
particleCount = 15,
particleDistances = [90, 10],
particleR = 100,
timeVariance = 300,
colors = [1, 2, 3, 1, 2, 3, 1, 4],
initialActiveIndex = 0,
}) => {
const containerRef = useRef(null);
const navRef = useRef(null);
const filterRef = useRef(null);
const textRef = useRef(null);
const [activeIndex, setActiveIndex] = useState(initialActiveIndex);
const noise = (n = 1) => n / 2 - Math.random() * n;
const getXY = (distance, pointIndex, totalPoints) => {
const angle =
((360 + noise(8)) / totalPoints) * pointIndex * (Math.PI / 180);
return [distance * Math.cos(angle), distance * Math.sin(angle)];
};
const createParticle = (i, t, d, r) => {
let rotate = noise(r / 10);
return {
start: getXY(d[0], particleCount - i, particleCount),
end: getXY(d[1] + noise(7), particleCount - i, particleCount),
time: t,
scale: 1 + noise(0.2),
color: colors[Math.floor(Math.random() * colors.length)],
rotate:
rotate > 0 ? (rotate + r / 20) * 10 : (rotate - r / 20) * 10,
};
};
const makeParticles = (element) => {
const d = particleDistances;
const r = particleR;
const bubbleTime = animationTime * 2 + timeVariance;
element.style.setProperty("--time", `${bubbleTime}ms`);
for (let i = 0; i < particleCount; i++) {
const t = animationTime * 2 + noise(timeVariance * 2);
const p = createParticle(i, t, d, r);
element.classList.remove("active");
setTimeout(() => {
const particle = document.createElement("span");
const point = document.createElement("span");
particle.classList.add("particle");
particle.style.setProperty("--start-x", `${p.start[0]}px`);
particle.style.setProperty("--start-y", `${p.start[1]}px`);
particle.style.setProperty("--end-x", `${p.end[0]}px`);
particle.style.setProperty("--end-y", `${p.end[1]}px`);
particle.style.setProperty("--time", `${p.time}ms`);
particle.style.setProperty("--scale", `${p.scale}`);
particle.style.setProperty(
"--color",
`var(--color-${p.color}, white)`
);
particle.style.setProperty("--rotate", `${p.rotate}deg`);
point.classList.add("point");
particle.appendChild(point);
element.appendChild(particle);
requestAnimationFrame(() => {
element.classList.add("active");
});
setTimeout(() => {
try {
element.removeChild(particle);
} catch {
// Do nothing
}
}, t);
}, 30);
}
};
const updateEffectPosition = (element) => {
if (!containerRef.current || !filterRef.current || !textRef.current)
return;
const containerRect = containerRef.current.getBoundingClientRect();
const pos = element.getBoundingClientRect();
const styles = {
left: `${pos.x - containerRect.x}px`,
top: `${pos.y - containerRect.y}px`,
width: `${pos.width}px`,
height: `${pos.height}px`,
};
Object.assign(filterRef.current.style, styles);
Object.assign(textRef.current.style, styles);
textRef.current.innerText = element.innerText;
};
const handleClick = (e, index) => {
const liEl = e.currentTarget;
if (activeIndex === index) return;
setActiveIndex(index);
updateEffectPosition(liEl);
if (filterRef.current) {
const particles = filterRef.current.querySelectorAll(".particle");
particles.forEach((p) => filterRef.current.removeChild(p));
}
if (textRef.current) {
textRef.current.classList.remove("active");
void textRef.current.offsetWidth;
textRef.current.classList.add("active");
}
if (filterRef.current) {
makeParticles(filterRef.current);
}
};
const handleKeyDown = (e, index) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
const liEl = e.currentTarget.parentElement;
if (liEl) {
handleClick({ currentTarget: liEl }, index);
}
}
};
useEffect(() => {
if (!navRef.current || !containerRef.current) return;
const activeLi = navRef.current.querySelectorAll("li")[activeIndex];
if (activeLi) {
updateEffectPosition(activeLi);
textRef.current?.classList.add("active");
}
const resizeObserver = new ResizeObserver(() => {
const currentActiveLi =
navRef.current?.querySelectorAll("li")[activeIndex];
if (currentActiveLi) {
updateEffectPosition(currentActiveLi);
}
});
resizeObserver.observe(containerRef.current);
return () => resizeObserver.disconnect();
}, [activeIndex]);
return (
<div className="gooey-nav-container" ref={containerRef}>
<nav>
<ul ref={navRef}>
{items.map((item, index) => (
<li
key={index}
className={activeIndex === index ? "active" : ""}
>
<a
href={item.href}
onClick={(e) => handleClick(e, index)}
onKeyDown={(e) => handleKeyDown(e, index)}
>
{item.label}
</a>
</li>
))}
</ul>
</nav>
<span className="effect filter" ref={filterRef} />
<span className="effect text" ref={textRef} />
</div>
);
};
export default GooeyNav;