aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
327 lines (324 loc) • 9.75 kB
JavaScript
'use client';
import { useRef, useState, useCallback, useEffect } from 'react';
import { useAccessibleAnimation } from './useAccessibilitySettings.js';
const DEFAULT_OPTIONS = {
scale: 1.02,
duration: 200,
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
damping: 0.8,
stiffness: 100,
mass: 1,
enableHover: true,
enableClick: true,
enableDrag: false,
threshold: 5,
onInteractionStart: () => {},
onInteractionEnd: () => {}
};
/**
* Enhanced physics interaction hook with spring animations and accessibility
*/
function usePhysicsInteraction(options = {}) {
const mergedOptions = {
...DEFAULT_OPTIONS,
...options
};
const {
scale,
duration,
easing,
damping,
stiffness,
mass,
enableHover,
enableClick,
enableDrag,
threshold,
onInteractionStart,
onInteractionEnd
} = mergedOptions;
const ref = useRef(null);
const animationFrameRef = useRef();
const isDraggingRef = useRef(false);
const startPositionRef = useRef({
x: 0,
y: 0
});
useRef({
x: 0,
y: 0
});
const [physicsState, setPhysicsState] = useState({
isInteracting: false,
velocity: {
x: 0,
y: 0
},
position: {
x: 0,
y: 0
},
scale: 1,
rotation: 0
});
const {
shouldAnimate,
transitionDuration
} = useAccessibleAnimation();
// Spring physics calculation
const calculateSpringForce = useCallback((current, target, velocity) => {
const force = -stiffness * (current - target) - damping * velocity;
const acceleration = force / mass;
const newVelocity = velocity + acceleration * 0.016; // 60fps
const newPosition = current + newVelocity * 0.016;
return {
position: newPosition,
velocity: newVelocity
};
}, [stiffness, damping, mass]);
// Apply transform with physics
const applyTransform = useCallback((element, state) => {
if (!shouldAnimate) return;
const transforms = [];
if (state.position) {
transforms.push(`translate3d(${state.position.x}px, ${state.position.y}px, 0)`);
}
if (state.scale && state.scale !== 1) {
transforms.push(`scale(${state.scale})`);
}
if (state.rotation && state.rotation !== 0) {
transforms.push(`rotate(${state.rotation}deg)`);
}
element.style.transform = transforms.length > 0 ? transforms.join(' ') : 'none';
element.style.transition = shouldAnimate ? `transform ${transitionDuration}ms ${easing}` : 'none';
}, [shouldAnimate, transitionDuration, easing]);
// Animation loop for spring physics
const animateSpring = useCallback(() => {
const element = ref.current;
if (!element || !physicsState.isInteracting) return;
setPhysicsState(prevState => {
const targetX = isDraggingRef.current ? prevState.position.x : 0;
const targetY = isDraggingRef.current ? prevState.position.y : 0;
const targetScale = prevState.isInteracting ? scale : 1;
const newX = calculateSpringForce(prevState.position.x, targetX, prevState.velocity.x);
const newY = calculateSpringForce(prevState.position.y, targetY, prevState.velocity.y);
const newScale = calculateSpringForce(prevState.scale, targetScale, 0);
const newState = {
...prevState,
position: {
x: newX.position,
y: newY.position
},
velocity: {
x: newX.velocity,
y: newY.velocity
},
scale: newScale.position
};
applyTransform(element, newState);
// Check if animation should continue
const isStable = Math.abs(newX.velocity) < 0.01 && Math.abs(newY.velocity) < 0.01 && Math.abs(newState.scale - targetScale) < 0.001;
if (isStable && !isDraggingRef.current) {
return {
...newState,
isInteracting: false
};
}
return newState;
});
if (physicsState.isInteracting) {
animationFrameRef.current = requestAnimationFrame(animateSpring);
}
}, [physicsState.isInteracting, scale, calculateSpringForce, applyTransform]);
// Start interaction
const startInteraction = useCallback(() => {
setPhysicsState(prev => ({
...prev,
isInteracting: true
}));
onInteractionStart();
if (shouldAnimate && !animationFrameRef.current) {
animateSpring();
}
}, [shouldAnimate, animateSpring, onInteractionStart]);
// End interaction
const endInteraction = useCallback(() => {
isDraggingRef.current = false;
onInteractionEnd();
// Let spring animation finish naturally
setTimeout(() => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
animationFrameRef.current = undefined;
}
}, duration);
}, [duration, onInteractionEnd]);
// Mouse/Touch event handlers
const handlePointerDown = useCallback(e => {
if (!enableClick && !enableDrag) return;
startPositionRef.current = {
x: e.clientX,
y: e.clientY
};
if (enableDrag) {
isDraggingRef.current = true;
startInteraction();
}
}, [enableClick, enableDrag, startInteraction]);
const handlePointerMove = useCallback(e => {
if (!isDraggingRef.current || !enableDrag) return;
const deltaX = e.clientX - startPositionRef.current.x;
const deltaY = e.clientY - startPositionRef.current.y;
if (Math.abs(deltaX) > threshold || Math.abs(deltaY) > threshold) {
setPhysicsState(prev => ({
...prev,
position: {
x: deltaX * 0.5,
y: deltaY * 0.5
} // Damped movement
}));
}
}, [enableDrag, threshold]);
const handlePointerUp = useCallback(() => {
if (isDraggingRef.current) {
endInteraction();
}
}, [endInteraction]);
const handleMouseEnter = useCallback(() => {
if (!enableHover || !shouldAnimate) return;
const element = ref.current;
if (!element) return;
startInteraction();
applyTransform(element, {
scale
});
}, [enableHover, shouldAnimate, startInteraction, applyTransform, scale]);
const handleMouseLeave = useCallback(() => {
if (!enableHover || !shouldAnimate) return;
const element = ref.current;
if (!element) return;
endInteraction();
applyTransform(element, {
scale: 1,
position: {
x: 0,
y: 0
}
});
}, [enableHover, shouldAnimate, endInteraction, applyTransform]);
const handleClick = useCallback(() => {
if (!enableClick || !shouldAnimate) return;
const element = ref.current;
if (!element) return;
// Quick scale animation for click feedback
applyTransform(element, {
scale: scale * 0.95
});
setTimeout(() => {
applyTransform(element, {
scale: 1
});
}, 100);
}, [enableClick, shouldAnimate, applyTransform, scale]);
// Set up event listeners
useEffect(() => {
const element = ref.current;
if (!element) return;
// Mouse events
if (enableHover) {
element.addEventListener('mouseenter', handleMouseEnter, {
passive: true
});
element.addEventListener('mouseleave', handleMouseLeave, {
passive: true
});
}
if (enableClick) {
element.addEventListener('click', handleClick, {
passive: true
});
}
// Pointer events for drag
if (enableDrag) {
element.addEventListener('pointerdown', handlePointerDown, {
passive: true
});
document.addEventListener('pointermove', handlePointerMove, {
passive: true
});
document.addEventListener('pointerup', handlePointerUp, {
passive: true
});
}
// Keyboard support for accessibility
const handleKeyDown = e => {
if (e.key === 'Enter' || e.key === ' ') {
handleClick();
}
};
element.addEventListener('keydown', handleKeyDown);
return () => {
// Cleanup event listeners
element.removeEventListener('mouseenter', handleMouseEnter);
element.removeEventListener('mouseleave', handleMouseLeave);
element.removeEventListener('click', handleClick);
element.removeEventListener('pointerdown', handlePointerDown);
element.removeEventListener('keydown', handleKeyDown);
document.removeEventListener('pointermove', handlePointerMove);
document.removeEventListener('pointerup', handlePointerUp);
// Cleanup animation
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
};
}, [enableHover, enableClick, enableDrag, handleMouseEnter, handleMouseLeave, handleClick, handlePointerDown, handlePointerMove, handlePointerUp]);
return {
ref,
physicsState,
isInteracting: physicsState.isInteracting,
startInteraction,
endInteraction
};
}
/**
* Simplified physics interaction hook for basic hover effects
*/
function useSimplePhysicsHover(scale = 1.02, duration = 200) {
return usePhysicsInteraction({
scale,
duration,
enableHover: true,
enableClick: false,
enableDrag: false
});
}
/**
* Physics interaction hook optimized for buttons
*/
function usePhysicsButton(options = {}) {
return usePhysicsInteraction({
scale: 0.95,
duration: 150,
enableHover: true,
enableClick: true,
enableDrag: false,
...options
});
}
/**
* Physics interaction hook for draggable elements
*/
function usePhysicsDrag(options = {}) {
return usePhysicsInteraction({
scale: 1.05,
duration: 300,
enableHover: false,
enableClick: false,
enableDrag: true,
damping: 0.9,
stiffness: 150,
...options
});
}
export { usePhysicsButton, usePhysicsDrag, usePhysicsInteraction, useSimplePhysicsHover };
//# sourceMappingURL=usePhysicsInteraction.js.map