aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
224 lines (221 loc) • 6.49 kB
JavaScript
'use client';
import { useRef, useState, useCallback, useEffect } from 'react';
import { useReducedMotion } from '../useReducedMotion.js';
/**
* Hook for ambient tilt effects
*/
const useAmbientTilt = (options = {}) => {
const {
maxTilt = 15,
reverse = false,
perspective = 1000,
scale = 1.05,
speed = 300,
easing = 'cubic-bezier(0.03, 0.98, 0.52, 0.99)',
glare = false,
gyroscope = false,
onTilt
} = options;
const prefersReducedMotion = useReducedMotion();
const containerRef = useRef(null);
const [tiltState, setTiltState] = useState({
tiltX: 0,
tiltY: 0,
glareOpacity: 0,
glarePosition: {
x: 50,
y: 50
}
});
const [isHovering, setIsHovering] = useState(false);
const animationFrameRef = useRef(null);
/**
* Calculate tilt from cursor position
*/
const calculateTilt = useCallback((clientX, clientY, element) => {
const rect = element.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
// Calculate offset from center
const offsetX = clientX - centerX;
const offsetY = clientY - centerY;
// Calculate tilt angles
let tiltX = offsetY / (rect.height / 2) * maxTilt;
let tiltY = offsetX / (rect.width / 2) * maxTilt;
if (reverse) {
tiltX = -tiltX;
tiltY = -tiltY;
}
// Calculate glare position (as percentage)
const glareX = (clientX - rect.left) / rect.width * 100;
const glareY = (clientY - rect.top) / rect.height * 100;
return {
tiltX: -tiltX,
// Invert X for natural feel
tiltY: tiltY,
glarePosition: {
x: glareX,
y: glareY
}
};
}, [maxTilt, reverse]);
/**
* Handle mouse move
*/
const handleMouseMove = useCallback(event => {
if (!containerRef.current || prefersReducedMotion) return;
const {
tiltX,
tiltY,
glarePosition
} = calculateTilt(event.clientX, event.clientY, containerRef.current);
setTiltState({
tiltX,
tiltY,
glareOpacity: glare ? 0.3 : 0,
glarePosition
});
onTilt?.(tiltX, tiltY);
}, [calculateTilt, glare, onTilt, prefersReducedMotion]);
/**
* Handle mouse enter
*/
const handleMouseEnter = useCallback(() => {
setIsHovering(true);
}, []);
/**
* Handle mouse leave
*/
const handleMouseLeave = useCallback(() => {
setIsHovering(false);
setTiltState({
tiltX: 0,
tiltY: 0,
glareOpacity: 0,
glarePosition: {
x: 50,
y: 50
}
});
onTilt?.(0, 0);
}, [onTilt]);
/**
* Handle device orientation (gyroscope)
*/
const handleDeviceOrientation = useCallback(event => {
if (!gyroscope || prefersReducedMotion) return;
// Beta is the front-to-back tilt in degrees (-180 to 180)
// Gamma is the left-to-right tilt in degrees (-90 to 90)
const beta = event.beta || 0;
const gamma = event.gamma || 0;
const tiltX = Math.max(-maxTilt, Math.min(maxTilt, beta / 90 * maxTilt));
const tiltY = Math.max(-maxTilt, Math.min(maxTilt, gamma / 90 * maxTilt));
setTiltState(prev => ({
...prev,
tiltX: reverse ? -tiltX : tiltX,
tiltY: reverse ? -tiltY : tiltY
}));
onTilt?.(tiltX, tiltY);
}, [gyroscope, maxTilt, reverse, onTilt, prefersReducedMotion]);
/**
* Set up event listeners
*/
useEffect(() => {
const element = containerRef.current;
if (!element) return;
element.addEventListener('mousemove', handleMouseMove);
element.addEventListener('mouseenter', handleMouseEnter);
element.addEventListener('mouseleave', handleMouseLeave);
// Add device orientation listener if gyroscope is enabled
if (gyroscope && window.DeviceOrientationEvent) {
window.addEventListener('deviceorientation', handleDeviceOrientation);
}
return () => {
element.removeEventListener('mousemove', handleMouseMove);
element.removeEventListener('mouseenter', handleMouseEnter);
element.removeEventListener('mouseleave', handleMouseLeave);
if (gyroscope) {
window.removeEventListener('deviceorientation', handleDeviceOrientation);
}
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
};
}, [handleMouseMove, handleMouseEnter, handleMouseLeave, handleDeviceOrientation, gyroscope]);
/**
* Get transform style
*/
const getTransformStyle = useCallback(() => {
if (prefersReducedMotion) {
return {
transform: 'none'
};
}
const scaleValue = isHovering ? scale : 1;
return {
transform: `perspective(${perspective}px) rotateX(${tiltState.tiltX}deg) rotateY(${tiltState.tiltY}deg) scale3d(${scaleValue}, ${scaleValue}, ${scaleValue})`,
transition: `transform ${speed}ms ${easing}`,
transformStyle: 'preserve-3d'
};
}, [tiltState, isHovering, perspective, scale, speed, easing, prefersReducedMotion]);
/**
* Get glare style
*/
const getGlareStyle = useCallback(() => {
if (!glare || prefersReducedMotion) {
return {
display: 'none'
};
}
return {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
background: `radial-gradient(circle at ${tiltState.glarePosition.x}% ${tiltState.glarePosition.y}%, rgba(255, 255, 255, ${tiltState.glareOpacity}), transparent 50%)`,
pointerEvents: 'none',
transition: `opacity ${speed}ms ${easing}`,
borderRadius: 'inherit'
};
}, [glare, tiltState, speed, easing, prefersReducedMotion]);
/**
* Reset tilt
*/
const reset = useCallback(() => {
setTiltState({
tiltX: 0,
tiltY: 0,
glareOpacity: 0,
glarePosition: {
x: 50,
y: 50
}
});
setIsHovering(false);
onTilt?.(0, 0);
}, [onTilt]);
/**
* Set manual tilt
*/
const setTilt = useCallback((tiltX, tiltY) => {
setTiltState(prev => ({
...prev,
tiltX: Math.max(-maxTilt, Math.min(maxTilt, tiltX)),
tiltY: Math.max(-maxTilt, Math.min(maxTilt, tiltY))
}));
onTilt?.(tiltX, tiltY);
}, [maxTilt, onTilt]);
return {
ref: containerRef,
tiltX: tiltState.tiltX,
tiltY: tiltState.tiltY,
isHovering,
transformStyle: getTransformStyle(),
glareStyle: getGlareStyle(),
reset,
setTilt
};
};
export { useAmbientTilt };
//# sourceMappingURL=useAmbientTilt.js.map