reactbits-mcp-server
Version:
MCP Server for React Bits - Access 99+ React components with animations, backgrounds, and UI elements
320 lines (298 loc) • 10.2 kB
JSX
import { useCallback, useEffect, useRef } from "react";
import gsap from "gsap";
const Cubes = ({
gridSize = 10,
cubeSize,
maxAngle = 45,
radius = 3,
easing = "power3.out",
duration = { enter: 0.3, leave: 0.6 },
cellGap,
borderStyle = "1px solid #fff",
faceColor = "#060010",
shadow = false,
autoAnimate = true,
rippleOnClick = true,
rippleColor = "#fff",
rippleSpeed = 2,
}) => {
const sceneRef = useRef(null);
const rafRef = useRef(null);
const idleTimerRef = useRef(null);
const userActiveRef = useRef(false);
const simPosRef = useRef({ x: 0, y: 0 });
const simTargetRef = useRef({ x: 0, y: 0 });
const simRAFRef = useRef(null);
const colGap =
typeof cellGap === "number"
? `${cellGap}px`
: (cellGap)?.col !== undefined
? `${(cellGap).col}px`
: "5%";
const rowGap =
typeof cellGap === "number"
? `${cellGap}px`
: (cellGap)?.row !== undefined
? `${(cellGap).row}px`
: "5%";
const enterDur = duration.enter;
const leaveDur = duration.leave;
const tiltAt = useCallback(
(rowCenter, colCenter) => {
if (!sceneRef.current) return;
sceneRef.current
.querySelectorAll(".cube")
.forEach((cube) => {
const r = +cube.dataset.row;
const c = +cube.dataset.col;
const dist = Math.hypot(r - rowCenter, c - colCenter);
if (dist <= radius) {
const pct = 1 - dist / radius;
const angle = pct * maxAngle;
gsap.to(cube, {
duration: enterDur,
ease: easing,
overwrite: true,
rotateX: -angle,
rotateY: angle,
});
} else {
gsap.to(cube, {
duration: leaveDur,
ease: "power3.out",
overwrite: true,
rotateX: 0,
rotateY: 0,
});
}
});
},
[radius, maxAngle, enterDur, leaveDur, easing]
);
const onPointerMove = useCallback(
(e) => {
userActiveRef.current = true;
if (idleTimerRef.current) clearTimeout(idleTimerRef.current);
const rect = sceneRef.current.getBoundingClientRect();
const cellW = rect.width / gridSize;
const cellH = rect.height / gridSize;
const colCenter = (e.clientX - rect.left) / cellW;
const rowCenter = (e.clientY - rect.top) / cellH;
if (rafRef.current) cancelAnimationFrame(rafRef.current);
rafRef.current = requestAnimationFrame(() =>
tiltAt(rowCenter, colCenter)
);
idleTimerRef.current = setTimeout(() => {
userActiveRef.current = false;
}, 3000);
},
[gridSize, tiltAt]
);
const resetAll = useCallback(() => {
if (!sceneRef.current) return;
sceneRef.current.querySelectorAll(".cube").forEach((cube) =>
gsap.to(cube, {
duration: leaveDur,
rotateX: 0,
rotateY: 0,
ease: "power3.out",
})
);
}, [leaveDur]);
const onClick = useCallback(
(e) => {
if (!rippleOnClick || !sceneRef.current) return;
const rect = sceneRef.current.getBoundingClientRect();
const cellW = rect.width / gridSize;
const cellH = rect.height / gridSize;
const colHit = Math.floor((e.clientX - rect.left) / cellW);
const rowHit = Math.floor((e.clientY - rect.top) / cellH);
const baseRingDelay = 0.15;
const baseAnimDur = 0.3;
const baseHold = 0.6;
const spreadDelay = baseRingDelay / rippleSpeed;
const animDuration = baseAnimDur / rippleSpeed;
const holdTime = baseHold / rippleSpeed;
const rings = {};
sceneRef.current
.querySelectorAll(".cube")
.forEach((cube) => {
const r = +cube.dataset.row;
const c = +cube.dataset.col;
const dist = Math.hypot(r - rowHit, c - colHit);
const ring = Math.round(dist);
if (!rings[ring]) rings[ring] = [];
rings[ring].push(cube);
});
Object.keys(rings)
.map(Number)
.sort((a, b) => a - b)
.forEach((ring) => {
const delay = ring * spreadDelay;
const faces = rings[ring].flatMap((cube) =>
Array.from(cube.querySelectorAll(".cube-face"))
);
gsap.to(faces, {
backgroundColor: rippleColor,
duration: animDuration,
delay,
ease: "power3.out",
});
gsap.to(faces, {
backgroundColor: faceColor,
duration: animDuration,
delay: delay + animDuration + holdTime,
ease: "power3.out",
});
});
},
[rippleOnClick, gridSize, faceColor, rippleColor, rippleSpeed]
);
useEffect(() => {
if (!autoAnimate || !sceneRef.current) return;
simPosRef.current = {
x: Math.random() * gridSize,
y: Math.random() * gridSize,
};
simTargetRef.current = {
x: Math.random() * gridSize,
y: Math.random() * gridSize,
};
const speed = 0.02;
const loop = () => {
if (!userActiveRef.current) {
const pos = simPosRef.current;
const tgt = simTargetRef.current;
pos.x += (tgt.x - pos.x) * speed;
pos.y += (tgt.y - pos.y) * speed;
tiltAt(pos.y, pos.x);
if (Math.hypot(pos.x - tgt.x, pos.y - tgt.y) < 0.1) {
simTargetRef.current = {
x: Math.random() * gridSize,
y: Math.random() * gridSize,
};
}
}
simRAFRef.current = requestAnimationFrame(loop);
};
simRAFRef.current = requestAnimationFrame(loop);
return () => {
if (simRAFRef.current != null) cancelAnimationFrame(simRAFRef.current);
};
}, [autoAnimate, gridSize, tiltAt]);
useEffect(() => {
const el = sceneRef.current;
if (!el) return;
el.addEventListener("pointermove", onPointerMove);
el.addEventListener("pointerleave", resetAll);
el.addEventListener("click", onClick);
return () => {
el.removeEventListener("pointermove", onPointerMove);
el.removeEventListener("pointerleave", resetAll);
el.removeEventListener("click", onClick);
if (rafRef.current != null) cancelAnimationFrame(rafRef.current);
if (idleTimerRef.current) clearTimeout(idleTimerRef.current);
};
}, [onPointerMove, resetAll, onClick]);
const cells = Array.from({ length: gridSize });
const sceneStyle = {
gridTemplateColumns: cubeSize
? `repeat(${gridSize}, ${cubeSize}px)`
: `repeat(${gridSize}, 1fr)`,
gridTemplateRows: cubeSize
? `repeat(${gridSize}, ${cubeSize}px)`
: `repeat(${gridSize}, 1fr)`,
columnGap: colGap,
rowGap: rowGap,
perspective: "99999999px",
gridAutoRows: "1fr",
};
const wrapperStyle = {
"--cube-face-border": borderStyle,
"--cube-face-bg": faceColor,
"--cube-face-shadow":
shadow === true ? "0 0 6px rgba(0,0,0,.5)" : shadow || "none",
...(cubeSize
? {
width: `${gridSize * cubeSize}px`,
height: `${gridSize * cubeSize}px`,
}
: {}),
};
return (
<div
className="relative w-1/2 max-md:w-11/12 aspect-square"
style={wrapperStyle}
>
<div ref={sceneRef} className="grid w-full h-full" style={sceneStyle}>
{cells.map((_, r) =>
cells.map((__, c) => (
<div
key={`${r}-${c}`}
className="cube relative w-full h-full aspect-square [transform-style:preserve-3d]"
data-row={r}
data-col={c}
>
<span className="absolute pointer-events-none -inset-9" />
<div
className="cube-face absolute inset-0 flex items-center justify-center"
style={{
background: "var(--cube-face-bg)",
border: "var(--cube-face-border)",
boxShadow: "var(--cube-face-shadow)",
transform: "translateY(-50%) rotateX(90deg)",
}}
/>
<div
className="cube-face absolute inset-0 flex items-center justify-center"
style={{
background: "var(--cube-face-bg)",
border: "var(--cube-face-border)",
boxShadow: "var(--cube-face-shadow)",
transform: "translateY(50%) rotateX(-90deg)",
}}
/>
<div
className="cube-face absolute inset-0 flex items-center justify-center"
style={{
background: "var(--cube-face-bg)",
border: "var(--cube-face-border)",
boxShadow: "var(--cube-face-shadow)",
transform: "translateX(-50%) rotateY(-90deg)",
}}
/>
<div
className="cube-face absolute inset-0 flex items-center justify-center"
style={{
background: "var(--cube-face-bg)",
border: "var(--cube-face-border)",
boxShadow: "var(--cube-face-shadow)",
transform: "translateX(50%) rotateY(90deg)",
}}
/>
<div
className="cube-face absolute inset-0 flex items-center justify-center"
style={{
background: "var(--cube-face-bg)",
border: "var(--cube-face-border)",
boxShadow: "var(--cube-face-shadow)",
transform: "rotateY(-90deg) translateX(50%) rotateY(90deg)",
}}
/>
<div
className="cube-face absolute inset-0 flex items-center justify-center"
style={{
background: "var(--cube-face-bg)",
border: "var(--cube-face-border)",
boxShadow: "var(--cube-face-shadow)",
transform: "rotateY(90deg) translateX(-50%) rotateY(-90deg)",
}}
/>
</div>
))
)}
</div>
</div>
);
};
export default Cubes;