UNPKG

aura-glass

Version:

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

320 lines (317 loc) 10.5 kB
'use client'; import { jsxs, Fragment, jsx } from 'react/jsx-runtime'; import { forwardRef, useRef, useState, useEffect, useCallback } from 'react'; import { useMotionValue, useSpring, motion, AnimatePresence } from 'framer-motion'; import { cn } from '../../lib/utilsComprehensive.js'; import '../../primitives/GlassCore.js'; import '../../primitives/glass/GlassAdvanced.js'; import { OptimizedGlassCore } from '../../primitives/OptimizedGlassCore.js'; import '../../primitives/glass/OptimizedGlassAdvanced.js'; import '../../primitives/MotionNative.js'; import '../../primitives/motion/MotionFramer.js'; import { useA11yId } from '../../utils/a11y.js'; import { useMotionPreferenceContext } from '../../contexts/MotionPreferenceContext.js'; const GlassMagneticCursor = /*#__PURE__*/forwardRef(function GlassMagneticCursor({ className, variant = "default", size = 20, color = "var(--glass-color-primary, 0.5)", magnetStrength = 0.3, magnetRadius = 100, showCursor = true, trailLength = 5, glowIntensity = 1, morphTargets = true, hapticFeedback = false, customCursor, respectMotionPreference = true, "aria-label": ariaLabel, "aria-describedby": ariaDescribedBy, role, ...restProps }, ref) { const cursorRef = useRef(null); const [isHovering, setIsHovering] = useState(false); const [targetElement, setTargetElement] = useState(null); const [trail, setTrail] = useState([]); const [ripples, setRipples] = useState([]); const { prefersReducedMotion } = useMotionPreferenceContext(); const cursorId = useA11yId("magnetic-cursor"); const descriptionId = useA11yId("cursor-description"); // Motion values const mouseX = useMotionValue(0); const mouseY = useMotionValue(0); const cursorSize = useMotionValue(size); // Spring physics - respect motion preferences const shouldAnimate = respectMotionPreference ? !prefersReducedMotion : true; const springConfig = { damping: shouldAnimate ? 25 : 50, stiffness: shouldAnimate ? 200 : 100 }; const cursorX = useSpring(mouseX, springConfig); const cursorY = useSpring(mouseY, springConfig); const cursorScale = useSpring(1, { damping: 15, stiffness: 300 }); // Track magnetic elements const magneticElements = useRef([]); // Initialize magnetic elements useEffect(() => { const elements = document.querySelectorAll("[data-magnetic]"); magneticElements.current = Array.from(elements).map(el => ({ element: el, strength: parseFloat(el.getAttribute("data-magnetic-strength") || "") || magnetStrength, radius: parseFloat(el.getAttribute("data-magnetic-radius") || "") || magnetRadius, haptic: el.getAttribute("data-magnetic-haptic") === "true" })); return () => { magneticElements.current = []; }; }, [magnetStrength, magnetRadius]); // Mouse movement handler const handleMouseMove = useCallback(e => { const x = e.clientX; const y = e.clientY; mouseX.set(x); mouseY.set(y); // Check magnetic elements let closestElement = null; let closestDistance = Infinity; let magneticOffset = { x: 0, y: 0 }; magneticElements.current.forEach(({ element, strength, radius }) => { const rect = element.getBoundingClientRect(); const centerX = rect.left + rect.width / 2; const centerY = rect.top + rect.height / 2; const distance = Math.sqrt(Math.pow(x - centerX, 2) + Math.pow(y - centerY, 2)); if (distance < radius && distance < closestDistance) { closestElement = element; closestDistance = distance; // Calculate magnetic pull const pullStrength = (1 - distance / radius) * strength; magneticOffset.x = (centerX - x) * pullStrength; magneticOffset.y = (centerY - y) * pullStrength; } }); // Apply magnetic effect if (closestElement) { mouseX.set(x + magneticOffset.x); mouseY.set(y + magneticOffset.y); setIsHovering(true); setTargetElement(closestElement); // Morph cursor size if (morphTargets) { const rect = closestElement.getBoundingClientRect(); const targetSize = Math.min(rect.width, rect.height) * 0.8; cursorSize.set(targetSize); } // Haptic feedback (if supported) if (hapticFeedback && "vibrate" in navigator) { navigator.vibrate(1); } } else { setIsHovering(false); setTargetElement(null); cursorSize.set(size); } // Update trail if (variant === "trail") { setTrail(prev => { const newTrail = [{ x, y, id: Date.now() }, ...prev.slice(0, trailLength - 1)]; return newTrail; }); } }, [mouseX, mouseY, size, morphTargets, hapticFeedback, variant, trailLength, cursorSize]); // Click handler for ripple effect const handleClick = useCallback(e => { if (variant === "ripple") { const ripple = { x: e.clientX, y: e.clientY, id: Date.now() }; setRipples(prev => [...prev, ripple]); // Remove ripple after animation setTimeout(() => { setRipples(prev => prev.filter(r => r.id !== ripple.id)); }, 1000); } // Scale animation on click cursorScale.set(0.8); setTimeout(() => cursorScale.set(1), 150); }, [variant, cursorScale]); // Mouse enter/leave handlers const handleMouseEnter = useCallback(() => { if (showCursor) { document.body.style.cursor = "none"; } }, [showCursor]); const handleMouseLeave = useCallback(() => { document.body.style.cursor = "auto"; setIsHovering(false); setTargetElement(null); }, []); // Setup event listeners useEffect(() => { window.addEventListener("mousemove", handleMouseMove); window.addEventListener("click", handleClick); window.addEventListener("mouseenter", handleMouseEnter); window.addEventListener("mouseleave", handleMouseLeave); return () => { window.removeEventListener("mousemove", handleMouseMove); window.removeEventListener("click", handleClick); window.removeEventListener("mouseenter", handleMouseEnter); window.removeEventListener("mouseleave", handleMouseLeave); document.body.style.cursor = "auto"; }; }, [handleMouseMove, handleClick, handleMouseEnter, handleMouseLeave]); // Hide default cursor when over magnetic elements useEffect(() => { magneticElements.current.forEach(({ element }) => { element.style.cursor = showCursor ? "none" : "pointer"; }); }, [showCursor]); if (!showCursor) return null; return jsxs(Fragment, { children: [jsxs("span", { id: descriptionId, className: 'sr-only', children: [ariaLabel || `Magnetic cursor (${variant})`, ". Interactive cursor that follows mouse movement", morphTargets ? " and morphs when hovering over magnetic elements" : "", "."] }), jsx(motion.div, { ref: ref || cursorRef, className: cn("fixed pointer-events-none z-[9999] mix-blend-difference", className), id: cursorId, role: role || "presentation", "aria-label": ariaLabel, "aria-describedby": ariaDescribedBy || descriptionId, "aria-hidden": "true", style: shouldAnimate ? { x: cursorX, y: cursorY, scale: cursorScale, width: cursorSize, height: cursorSize } : {}, ...restProps, children: jsx(OptimizedGlassCore, { intensity: "subtle", blur: "medium", children: customCursor || jsx(OptimizedGlassCore, { intensity: isHovering ? "medium" : "subtle", blur: "subtle", className: cn("absolute -translate-x-1/2 -translate-y-1/2", "glass-radius-full transition-all duration-200"), style: { width: cursorSize.get(), height: cursorSize.get(), background: variant === "glow" ? `radial-gradient(circle, ${color} 0%, transparent 70%)` : color, boxShadow: variant === "glow" ? `0 0 ${20 * glowIntensity}px ${color}` : undefined }, "aria-hidden": "true" }) }) }), variant === "trail" && trail.map((point, index) => jsx(motion.div, { className: 'fixed pointer-events-none z-[9998]', initial: { opacity: 0.5, scale: 1 }, animate: { opacity: 0, scale: 0.5 }, exit: { opacity: 0 }, transition: { duration: 0.5 }, style: { left: point.x - size / 2, top: point.y - size / 2, width: size, height: size }, children: jsx(OptimizedGlassCore, { intensity: "subtle", blur: "subtle", className: "glass-w-full glass-h-full glass-radius-full", style: { background: color, opacity: 1 - index / trailLength }, "aria-hidden": "true" }) }, point.id)), jsx(AnimatePresence, { children: variant === "ripple" && ripples.map(ripple => jsx(motion.div, { className: 'fixed pointer-events-none z-[9997]', initial: { scale: 0, opacity: 1 }, animate: { scale: 3, opacity: 0 }, exit: { opacity: 0 }, transition: { duration: 0.8, ease: "easeOut" }, style: { left: ripple.x, top: ripple.y, x: "-50%", y: "-50%" }, children: jsx("div", { className: 'w-20 h-20 glass-radius-full glass-border-2', style: { borderColor: color } }) }, ripple.id)) }), isHovering && targetElement && jsx(motion.div, { className: 'fixed pointer-events-none z-[9996]', initial: { opacity: 0, scale: 0.8 }, animate: { opacity: 1, scale: 1 }, exit: { opacity: 0, scale: 0.8 }, style: { left: targetElement.getBoundingClientRect().left, top: targetElement.getBoundingClientRect().top, width: targetElement.getBoundingClientRect().width, height: targetElement.getBoundingClientRect().height }, children: jsx("div", { className: "glass-w-full glass-h-full glass-radius-lg glass-border glass-border-white/20" }) })] }); }); export { GlassMagneticCursor }; //# sourceMappingURL=GlassMagneticCursor.js.map