UNPKG

reactbits-mcp-server

Version:

MCP Server for React Bits - Access 99+ React components with animations, backgrounds, and UI elements

146 lines (128 loc) 4.08 kB
import { useRef, useEffect, useState, useMemo, useId } from "react"; import "./CurvedLoop.css"; const CurvedLoop = ({ marqueeText = "", speed = 2, className, curveAmount = 400, direction = "left", interactive = true, }) => { const text = useMemo(() => { const hasTrailing = /\s|\u00A0$/.test(marqueeText); return ( (hasTrailing ? marqueeText.replace(/\s+$/, "") : marqueeText) + "\u00A0" ); }, [marqueeText]); const measureRef = useRef(null); const textPathRef = useRef(null); const pathRef = useRef(null); const [spacing, setSpacing] = useState(0); const [offset, setOffset] = useState(0); const uid = useId(); const pathId = `curve-${uid}`; const pathD = `M-100,40 Q500,${40 + curveAmount} 1540,40`; const dragRef = useRef(false); const lastXRef = useRef(0); const dirRef = useRef(direction); const velRef = useRef(0); const textLength = spacing; const totalText = textLength ? Array(Math.ceil(1800 / textLength) + 2).fill(text).join('') : text; const ready = spacing > 0; useEffect(() => { if (measureRef.current) setSpacing(measureRef.current.getComputedTextLength()); }, [text, className]); useEffect(() => { if (!spacing || !ready) return; let frame = 0; const step = () => { if (!dragRef.current && textPathRef.current) { const delta = dirRef.current === "right" ? speed : -speed; const currentOffset = parseFloat(textPathRef.current.getAttribute("startOffset") || "0"); let newOffset = currentOffset + delta; const wrapPoint = spacing; if (newOffset <= -wrapPoint) newOffset += wrapPoint; if (newOffset >= wrapPoint) newOffset -= wrapPoint; textPathRef.current.setAttribute("startOffset", newOffset + "px"); setOffset(newOffset); } frame = requestAnimationFrame(step); }; step(); return () => cancelAnimationFrame(frame); }, [spacing, speed, ready]); const onPointerDown = (e) => { if (!interactive) return; dragRef.current = true; lastXRef.current = e.clientX; velRef.current = 0; (e.target).setPointerCapture(e.pointerId); }; const onPointerMove = (e) => { if (!interactive || !dragRef.current || !textPathRef.current) return; const dx = e.clientX - lastXRef.current; lastXRef.current = e.clientX; velRef.current = dx; const currentOffset = parseFloat(textPathRef.current.getAttribute("startOffset") || "0"); let newOffset = currentOffset + dx; const wrapPoint = spacing; if (newOffset <= -wrapPoint) newOffset += wrapPoint; if (newOffset >= wrapPoint) newOffset -= wrapPoint; textPathRef.current.setAttribute("startOffset", newOffset + "px"); setOffset(newOffset); }; const endDrag = () => { if (!interactive) return; dragRef.current = false; dirRef.current = velRef.current > 0 ? "right" : "left"; }; const cursorStyle = interactive ? dragRef.current ? "grabbing" : "grab" : "auto"; return ( <div className="curved-loop-jacket" style={{ visibility: ready ? "visible" : "hidden", cursor: cursorStyle }} onPointerDown={onPointerDown} onPointerMove={onPointerMove} onPointerUp={endDrag} onPointerLeave={endDrag} > <svg className="curved-loop-svg" viewBox="0 0 1440 120"> <text ref={measureRef} xmlSpace="preserve" style={{ visibility: "hidden", opacity: 0, pointerEvents: "none" }} > {text} </text> <defs> <path ref={pathRef} id={pathId} d={pathD} fill="none" stroke="transparent" /> </defs> {ready && ( <text fontWeight="bold" xmlSpace="preserve" className={className}> <textPath ref={textPathRef} href={`#${pathId}`} startOffset={offset + "px"} xmlSpace="preserve"> {totalText} </textPath> </text> )} </svg> </div> ); }; export default CurvedLoop;