UNPKG

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