aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
320 lines (317 loc) • 10.5 kB
JavaScript
'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