UNPKG

reactbits-mcp-server

Version:

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

300 lines (247 loc) 8.77 kB
import { useEffect, useRef, useCallback, useMemo } from "react"; import { gsap } from "gsap"; import "./TargetCursor.css"; const TargetCursor = ({ targetSelector = ".cursor-target", spinDuration = 2, hideDefaultCursor = true, }) => { const cursorRef = useRef(null); const cornersRef = useRef(null); const spinTl = useRef(null); const constants = useMemo( () => ({ borderWidth: 3, cornerSize: 12, parallaxStrength: 0.00005, }), [] ); const moveCursor = useCallback((x, y) => { if (!cursorRef.current) return; gsap.to(cursorRef.current, { x, y, duration: 0.1, ease: "power3.out", }); }, []); useEffect(() => { if (!cursorRef.current) return; const originalCursor = document.body.style.cursor; if (hideDefaultCursor) { document.body.style.cursor = 'none'; } const cursor = cursorRef.current; cornersRef.current = cursor.querySelectorAll(".target-cursor-corner"); let activeTarget = null; let currentTargetMove = null; let currentLeaveHandler = null; let isAnimatingToTarget = false; let resumeTimeout = null; const cleanupTarget = (target) => { if (currentTargetMove) { target.removeEventListener("mousemove", currentTargetMove); } if (currentLeaveHandler) { target.removeEventListener("mouseleave", currentLeaveHandler); } currentTargetMove = null; currentLeaveHandler = null; }; gsap.set(cursor, { xPercent: -50, yPercent: -50, x: window.innerWidth / 2, y: window.innerHeight / 2, }); const createSpinTimeline = () => { if (spinTl.current) { spinTl.current.kill(); } spinTl.current = gsap .timeline({ repeat: -1 }) .to(cursor, { rotation: "+=360", duration: spinDuration, ease: "none" }); }; createSpinTimeline(); const moveHandler = (e) => moveCursor(e.clientX, e.clientY); window.addEventListener("mousemove", moveHandler); const enterHandler = (e) => { const directTarget = e.target; const allTargets = []; let current = directTarget; while (current && current !== document.body) { if (current.matches(targetSelector)) { allTargets.push(current); } current = current.parentElement; } const target = allTargets[0] || null; if (!target || !cursorRef.current || !cornersRef.current) return; if (activeTarget === target) return; if (activeTarget) { cleanupTarget(activeTarget); } if (resumeTimeout) { clearTimeout(resumeTimeout); resumeTimeout = null; } activeTarget = target; gsap.killTweensOf(cursorRef.current, "rotation"); spinTl.current?.pause(); gsap.set(cursorRef.current, { rotation: 0 }); const updateCorners = (mouseX, mouseY) => { const rect = target.getBoundingClientRect(); const cursorRect = cursorRef.current.getBoundingClientRect(); const cursorCenterX = cursorRect.left + cursorRect.width / 2; const cursorCenterY = cursorRect.top + cursorRect.height / 2; const [tlc, trc, brc, blc] = Array.from(cornersRef.current); const { borderWidth, cornerSize, parallaxStrength } = constants; let tlOffset = { x: rect.left - cursorCenterX - borderWidth, y: rect.top - cursorCenterY - borderWidth, }; let trOffset = { x: rect.right - cursorCenterX + borderWidth - cornerSize, y: rect.top - cursorCenterY - borderWidth, }; let brOffset = { x: rect.right - cursorCenterX + borderWidth - cornerSize, y: rect.bottom - cursorCenterY + borderWidth - cornerSize, }; let blOffset = { x: rect.left - cursorCenterX - borderWidth, y: rect.bottom - cursorCenterY + borderWidth - cornerSize, }; if (mouseX !== undefined && mouseY !== undefined) { const targetCenterX = rect.left + rect.width / 2; const targetCenterY = rect.top + rect.height / 2; const mouseOffsetX = (mouseX - targetCenterX) * parallaxStrength; const mouseOffsetY = (mouseY - targetCenterY) * parallaxStrength; tlOffset.x += mouseOffsetX; tlOffset.y += mouseOffsetY; trOffset.x += mouseOffsetX; trOffset.y += mouseOffsetY; brOffset.x += mouseOffsetX; brOffset.y += mouseOffsetY; blOffset.x += mouseOffsetX; blOffset.y += mouseOffsetY; } const tl = gsap.timeline(); const corners = [tlc, trc, brc, blc]; const offsets = [tlOffset, trOffset, brOffset, blOffset]; corners.forEach((corner, index) => { tl.to( corner, { x: offsets[index].x, y: offsets[index].y, duration: 0.2, ease: "power2.out", }, 0 ); }); }; isAnimatingToTarget = true; updateCorners(); setTimeout(() => { isAnimatingToTarget = false; }, 1); let moveThrottle = null; const targetMove = (ev) => { if (moveThrottle || isAnimatingToTarget) return; moveThrottle = requestAnimationFrame(() => { const mouseEvent = ev; updateCorners(mouseEvent.clientX, mouseEvent.clientY); moveThrottle = null; }); }; const leaveHandler = () => { activeTarget = null; isAnimatingToTarget = false; if (cornersRef.current) { const corners = Array.from(cornersRef.current); gsap.killTweensOf(corners); const { cornerSize } = constants; const positions = [ { x: -cornerSize * 1.5, y: -cornerSize * 1.5 }, { x: cornerSize * 0.5, y: -cornerSize * 1.5 }, { x: cornerSize * 0.5, y: cornerSize * 0.5 }, { x: -cornerSize * 1.5, y: cornerSize * 0.5 }, ]; const tl = gsap.timeline(); corners.forEach((corner, index) => { tl.to( corner, { x: positions[index].x, y: positions[index].y, duration: 0.3, ease: "power3.out", }, 0 ); }); } resumeTimeout = setTimeout(() => { if (!activeTarget && cursorRef.current && spinTl.current) { const currentRotation = gsap.getProperty( cursorRef.current, "rotation" ); const normalizedRotation = currentRotation % 360; spinTl.current.kill(); spinTl.current = gsap .timeline({ repeat: -1 }) .to(cursorRef.current, { rotation: "+=360", duration: spinDuration, ease: "none" }); gsap.to(cursorRef.current, { rotation: normalizedRotation + 360, duration: spinDuration * (1 - normalizedRotation / 360), ease: "none", onComplete: () => { spinTl.current?.restart(); }, }); } resumeTimeout = null; }, 50); cleanupTarget(target); }; currentTargetMove = targetMove; currentLeaveHandler = leaveHandler; target.addEventListener("mousemove", targetMove); target.addEventListener("mouseleave", leaveHandler); }; window.addEventListener("mouseover", enterHandler, { passive: true }); return () => { window.removeEventListener("mousemove", moveHandler); window.removeEventListener("mouseover", enterHandler); if (activeTarget) { cleanupTarget(activeTarget); } console.log("Cleaning up TargetCursor"); spinTl.current?.kill(); document.body.style.cursor = originalCursor; }; }, [targetSelector, spinDuration, moveCursor, constants, hideDefaultCursor]); useEffect(() => { if (!cursorRef.current || !spinTl.current) return; if (spinTl.current.isActive()) { spinTl.current.kill(); spinTl.current = gsap .timeline({ repeat: -1 }) .to(cursorRef.current, { rotation: "+=360", duration: spinDuration, ease: "none" }); } }, [spinDuration]); return ( <div ref={cursorRef} className="target-cursor-wrapper"> <div className="target-cursor-dot" /> <div className="target-cursor-corner corner-tl" /> <div className="target-cursor-corner corner-tr" /> <div className="target-cursor-corner corner-br" /> <div className="target-cursor-corner corner-bl" /> </div> ); }; export default TargetCursor;