UNPKG

react-gsap-animation-library

Version:

A comprehensive React component library that leverages GSAP for animations with official GSAP-React integration

194 lines (163 loc) 6.29 kB
import React, { useRef, useEffect, useState } from 'react'; import { gsap } from 'gsap'; const AnimatedCursor = ({ size = 20, color = 'rgba(0, 0, 0, 0.5)', hoverColor = 'rgba(0, 0, 0, 0.8)', hoverScale = 1.5, hoverSelectors = ['a', 'button', 'input', '.clickable'], mixBlendMode = '', speed = 0.1, trailLength = 0, trailSpacing = 10, cursorStyle = {} }) => { const cursorRef = useRef(null); const cursorTrailRefs = useRef([]); const mousePosition = useRef({ x: 0, y: 0 }); const cursorPosition = useRef({ x: 0, y: 0 }); const [hovering, setHovering] = useState(false); const animationRef = useRef(null); useEffect(() => { const cursor = cursorRef.current; // Set initial position off-screen mousePosition.current = { x: -100, y: -100 }; cursorPosition.current = { x: -100, y: -100 }; // Create cursor trails if needed if (trailLength > 0) { for (let i = 0; i < trailLength; i++) { const trail = document.createElement('div'); trail.className = 'cursor-trail'; trail.style.width = `${size * (1 - i * 0.1)}px`; trail.style.height = `${size * (1 - i * 0.1)}px`; trail.style.backgroundColor = color; trail.style.borderRadius = '50%'; trail.style.position = 'fixed'; trail.style.pointerEvents = 'none'; trail.style.zIndex = 9999 - i; trail.style.opacity = 1 - i * (0.7 / trailLength); trail.style.transform = 'translate(-50%, -50%)'; trail.style.mixBlendMode = mixBlendMode; document.body.appendChild(trail); cursorTrailRefs.current.push(trail); } } // Update mouse position on move const handleMouseMove = (e) => { mousePosition.current = { x: e.clientX, y: e.clientY }; }; // Handle hover states const handleElementHover = () => { setHovering(true); }; const handleElementLeave = () => { setHovering(false); }; // Add hover listeners to all matching elements const addHoverListeners = () => { const elements = document.querySelectorAll(hoverSelectors.join(', ')); elements.forEach(el => { el.addEventListener('mouseenter', handleElementHover); el.addEventListener('mouseleave', handleElementLeave); }); return elements; }; const elements = addHoverListeners(); // Animate cursor position const animateCursor = () => { // Smooth cursor follow with easing cursorPosition.current.x += (mousePosition.current.x - cursorPosition.current.x) * speed; cursorPosition.current.y += (mousePosition.current.y - cursorPosition.current.y) * speed; // Apply to cursor gsap.set(cursor, { x: cursorPosition.current.x, y: cursorPosition.current.y, backgroundColor: hovering ? hoverColor : color, width: hovering ? size * hoverScale : size, height: hovering ? size * hoverScale : size }); // Apply to trails with delay if (trailLength > 0) { // Store positions history for trail const trailPositions = [{ ...cursorPosition.current }]; cursorTrailRefs.current.forEach((trail, index) => { const delay = (index + 1) * trailSpacing; // Wait until we have enough position history if (trailPositions.length > delay) { const pos = trailPositions[trailPositions.length - delay - 1]; gsap.set(trail, { x: pos.x, y: pos.y, backgroundColor: hovering ? hoverColor : color, width: hovering ? size * (1 - index * 0.1) * hoverScale : size * (1 - index * 0.1), height: hovering ? size * (1 - index * 0.1) * hoverScale : size * (1 - index * 0.1) }); } }); } animationRef.current = requestAnimationFrame(animateCursor); }; document.addEventListener('mousemove', handleMouseMove); animationRef.current = requestAnimationFrame(animateCursor); // Re-add listeners when elements change const mutationObserver = new MutationObserver(() => { // Clean up old listeners elements.forEach(el => { el.removeEventListener('mouseenter', handleElementHover); el.removeEventListener('mouseleave', handleElementLeave); }); // Add new listeners addHoverListeners(); }); mutationObserver.observe(document.body, { childList: true, subtree: true }); return () => { document.removeEventListener('mousemove', handleMouseMove); if (animationRef.current) { cancelAnimationFrame(animationRef.current); } mutationObserver.disconnect(); // Clean up hover listeners elements.forEach(el => { el.removeEventListener('mouseenter', handleElementHover); el.removeEventListener('mouseleave', handleElementLeave); }); // Remove cursor trails cursorTrailRefs.current.forEach(trail => { if (trail && document.body.contains(trail)) { document.body.removeChild(trail); } }); }; }, [size, color, hoverColor, hoverScale, hoverSelectors.join(','), mixBlendMode, speed, trailLength, trailSpacing]); const defaultCursorStyle = { width: size, height: size, borderRadius: '50%', backgroundColor: color, position: 'fixed', pointerEvents: 'none', zIndex: 10000, transform: 'translate(-50%, -50%)', transition: 'width 0.2s, height 0.2s, background-color 0.2s', mixBlendMode, ...cursorStyle }; return ( <> <style> {` /* Hide default cursor */ body { cursor: none; } /* Make sure the cursor works over images */ img, svg, video { pointer-events: none; } `} </style> <div ref={cursorRef} style={defaultCursorStyle} /> </> ); }; export default AnimatedCursor;