UNPKG

aura-glass

Version:

A comprehensive glassmorphism design system for React applications with 142+ production-ready components

170 lines (167 loc) 5.52 kB
'use client'; import { useRef, useState, useCallback, useEffect } from 'react'; import { createVector2D } from '../../types/common.js'; import { useReducedMotion } from '../useReducedMotion.js'; /** * Hook for magnetic element effect */ const useMagneticElement = (options = {}) => { const { strength = 0.3, range = 100, speed = 0.15, maxDistance = 30, returnSpeed = 0.2, onActivate, onDeactivate, disabled = false } = options; const prefersReducedMotion = useReducedMotion(); const elementRef = useRef(null); const [position, setPosition] = useState(createVector2D(0, 0)); const [isActive, setIsActive] = useState(false); const targetPositionRef = useRef(createVector2D(0, 0)); const currentPositionRef = useRef(createVector2D(0, 0)); const animationFrameRef = useRef(null); /** * Calculate magnetic force */ const calculateMagneticForce = useCallback((mouseX, mouseY, element) => { const rect = element.getBoundingClientRect(); const centerX = rect.left + rect.width / 2; const centerY = rect.top + rect.height / 2; // Calculate distance from center const dx = mouseX - centerX; const dy = mouseY - centerY; const distance = Math.sqrt(dx * dx + dy * dy); // Check if within range if (distance > range) { return null; } // Calculate magnetic pull const pullStrength = (1 - distance / range) * strength; // Calculate target position let targetX = dx * pullStrength; let targetY = dy * pullStrength; // Limit maximum distance const targetDistance = Math.sqrt(targetX * targetX + targetY * targetY); if (targetDistance > maxDistance) { const scale = maxDistance / targetDistance; targetX *= scale; targetY *= scale; } return createVector2D(targetX, targetY); }, [strength, range, maxDistance]); /** * Lerp (linear interpolation) function */ const lerp = useCallback((start, end, factor) => { return start + (end - start) * factor; }, []); /** * Animation loop */ const animate = useCallback(() => { const current = currentPositionRef.current; const target = targetPositionRef.current; // Lerp current position towards target const newX = lerp(current.x, target.x, isActive ? speed : returnSpeed); const newY = lerp(current.y, target.y, isActive ? speed : returnSpeed); currentPositionRef.current = createVector2D(newX, newY); setPosition(createVector2D(newX, newY)); // Continue animation if not at target const distance = Math.sqrt(Math.pow(target.x - newX, 2) + Math.pow(target.y - newY, 2)); if (distance > 0.1) { animationFrameRef.current = requestAnimationFrame(animate); } }, [isActive, speed, returnSpeed, lerp]); /** * Handle mouse move */ const handleMouseMove = useCallback(event => { if (!elementRef.current || disabled || prefersReducedMotion) return; const force = calculateMagneticForce(event.clientX, event.clientY, elementRef.current); if (force) { if (!isActive) { setIsActive(true); onActivate?.(); } targetPositionRef.current = force; // Start animation loop if not already running if (!animationFrameRef.current) { animationFrameRef.current = requestAnimationFrame(animate); } } else { if (isActive) { setIsActive(false); onDeactivate?.(); } // Return to original position targetPositionRef.current = createVector2D(0, 0); if (!animationFrameRef.current) { animationFrameRef.current = requestAnimationFrame(animate); } } }, [disabled, prefersReducedMotion, calculateMagneticForce, isActive, animate, onActivate, onDeactivate]); /** * Handle mouse leave (for document) */ const handleMouseLeave = useCallback(() => { if (isActive) { setIsActive(false); onDeactivate?.(); } targetPositionRef.current = createVector2D(0, 0); if (!animationFrameRef.current) { animationFrameRef.current = requestAnimationFrame(animate); } }, [isActive, animate, onDeactivate]); /** * Set up event listeners */ useEffect(() => { if (disabled || prefersReducedMotion) { return; } window.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseleave', handleMouseLeave); return () => { window.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseleave', handleMouseLeave); if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); animationFrameRef.current = null; } }; }, [handleMouseMove, handleMouseLeave, disabled, prefersReducedMotion]); /** * Get transform style */ const getStyle = useCallback(() => { if (prefersReducedMotion || disabled) { return {}; } return { transform: `translate3d(${position.x}px, ${position.y}px, 0)`, transition: 'none' // Smooth animation handled by RAF }; }, [position, prefersReducedMotion, disabled]); /** * Reset position */ const reset = useCallback(() => { targetPositionRef.current = createVector2D(0, 0); currentPositionRef.current = createVector2D(0, 0); setPosition(createVector2D(0, 0)); setIsActive(false); }, []); return { ref: elementRef, position, isActive, style: getStyle(), reset }; }; export { useMagneticElement }; //# sourceMappingURL=useMagneticElement.js.map