aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
339 lines (336 loc) • 9.41 kB
JavaScript
'use client';
import { jsx } from 'react/jsx-runtime';
import { forwardRef, useState, useEffect, useMemo } from 'react';
import { motion } from 'framer-motion';
import { cn } from '../../design-system/utilsCore.js';
const IS_TEST_ENV = typeof process !== 'undefined' && process.env?.JEST_WORKER_ID !== undefined;
// --- Utilities to avoid non-animatable background warnings ---
function isSimpleColor(value) {
if (typeof value !== 'string') return false;
const v = value.trim();
if (v === 'transparent') return true;
// rgb/rgba
if (/^rgba?\(/i.test(v)) return true;
// hsl/hsla
if (/^hsla?\(/i.test(v)) return true;
// hex
if (/^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(v)) return true;
// CSS variable (often colors in this lib)
if (/^var\(/i.test(v)) return true;
return false;
}
function coerceBackgroundKey(obj) {
if (!obj || typeof obj !== 'object') return obj;
// Copy to avoid mutating user objects
const copy = Array.isArray(obj) ? obj.map(v => v) : {
...obj
};
for (const key of Object.keys(copy)) {
const val = copy[key];
if (key === 'background') {
if (isSimpleColor(val)) {
// Move to backgroundColor to make it animatable
copy.backgroundColor = val === 'transparent' ? 'rgba(0, 0, 0, 0)' : val;
delete copy.background;
}
} else if (typeof val === 'object' && val) {
copy[key] = coerceBackgroundKey(val);
}
}
return copy;
}
// Normalize easing values to what Framer Motion expects
function normalizeEasingValue(value) {
if (typeof value === 'string') {
const e = value.trim().toLowerCase();
if (e === 'ease-in') return 'easeIn';
if (e === 'ease-out') return 'easeOut';
if (e === 'ease-in-out' || e === 'ease') return 'easeInOut';
if (e === 'linear') return 'linear';
if (e.startsWith('cubic-bezier(') && e.endsWith(')')) {
const nums = e.slice('cubic-bezier('.length, -1).split(',').map(n => parseFloat(n.trim())).filter(n => Number.isFinite(n));
if (nums.length === 4) return nums;
}
}
return value;
}
// Recursively walk an object and normalize any transition.easing/ease occurrences
function normalizeTransitions(obj) {
if (!obj || typeof obj !== 'object') return obj;
const copy = Array.isArray(obj) ? obj.map(v => normalizeTransitions(v)) : {
...obj
};
for (const key of Object.keys(copy)) {
const val = copy[key];
if (key === 'transition') {
if (typeof val === 'string') {
// Strings are not valid for Framer transitions; drop to avoid runtime errors
delete copy[key];
continue;
}
if (val && typeof val === 'object') {
const t = {
...val
};
if ('easing' in t && !('ease' in t)) {
t.ease = normalizeEasingValue(t.easing);
delete t.easing;
} else if ('ease' in t) {
t.ease = normalizeEasingValue(t.ease);
}
copy[key] = t;
}
} else if (typeof val === 'object' && val) {
copy[key] = normalizeTransitions(val);
}
}
return copy;
}
function getVariants(preset) {
switch (preset) {
case 'fadeIn':
return {
initial: {
opacity: 0
},
animate: {
opacity: 1
}
};
case 'slideIn':
return {
initial: {
opacity: 0,
y: 16
},
animate: {
opacity: 1,
y: 0
}
};
case 'slideDown':
return {
initial: {
opacity: 0,
y: -12
},
animate: {
opacity: 1,
y: 0
}
};
case 'slideUp':
return {
initial: {
opacity: 0,
y: 12
},
animate: {
opacity: 1,
y: 0
}
};
case 'slideLeft':
return {
initial: {
opacity: 0,
x: 12
},
animate: {
opacity: 1,
x: 0
}
};
case 'slideRight':
return {
initial: {
opacity: 0,
x: -12
},
animate: {
opacity: 1,
x: 0
}
};
case 'scaleIn':
return {
initial: {
opacity: 0,
scale: 0.96
},
animate: {
opacity: 1,
scale: 1
}
};
case 'rotateIn':
return {
initial: {
opacity: 0,
rotate: -8
},
animate: {
opacity: 1,
rotate: 0
}
};
case 'bounceIn':
return {
initial: {
opacity: 0,
scale: 0.3
},
animate: {
opacity: 1,
scale: 1,
transition: {
type: 'spring',
stiffness: 330,
damping: 20
}
}
};
case 'pulseIn':
return {
initial: {
opacity: 0.8,
scale: 1.04
},
animate: {
opacity: 1,
scale: 1
}
};
default:
return {
initial: {},
animate: {}
};
}
}
const MotionFramer = /*#__PURE__*/forwardRef(({
preset = 'none',
duration = 300,
delay = 0,
easing = 'ease-in-out',
animateOnMount = true,
animateOnHover = false,
animateOnScroll = false,
className,
style,
children,
...props
}, ref) => {
// CRITICAL SSR FIX: Default to true (reduced motion) on server to prevent hydration mismatch
// Server-side can't detect actual preference, so we assume reduced motion for safety
// This prevents flash of animated content for users who prefer reduced motion
const [reduced, setReduced] = useState(true);
useEffect(() => {
if (IS_TEST_ENV) {
return;
}
if (typeof window === 'undefined' || !('matchMedia' in window)) return;
const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
const update = () => setReduced(!!mq.matches);
update();
if ('addEventListener' in mq) {
mq.addEventListener('change', update);
return () => mq.removeEventListener('change', update);
} else {
// @ts-ignore older Safari
mq.addListener(update);
// @ts-ignore older Safari
return () => mq.removeListener(update);
}
}, []);
const effectivePreset = reduced ? 'none' : preset;
const variants = useMemo(() => getVariants(effectivePreset), [effectivePreset]);
const easingMap = {
linear: 'linear',
ease: 'easeInOut',
'ease-in': 'easeIn',
'ease-out': 'easeOut',
'ease-in-out': 'easeInOut'
};
const transition = reduced ? {
duration: 0.0001,
delay: 0
} : {
duration: duration / 1000,
delay: delay / 1000
};
if (easing === 'spring') {
transition.type = 'spring';
} else if (easing) {
transition.ease = easingMap[easing] || 'easeInOut';
}
const baseProps = {
ref,
className: cn('motion-primitive', className),
style,
initial: reduced ? undefined : animateOnMount || animateOnScroll ? 'initial' : undefined,
animate: reduced ? undefined : 'animate',
variants,
transition
};
if (!reduced && animateOnScroll) {
baseProps.initial = 'initial';
baseProps.whileInView = 'animate';
baseProps.viewport = {
once: true,
amount: 0.2
};
}
if (!reduced && animateOnHover) {
baseProps.whileHover = {
scale: 1.015
};
}
// Sanitize incoming motion props to avoid animating unsupported CSS shorthands like `background`
// and to normalize easing names in transition objects passed by callers.
let sanitizedProps = {
...props
};
// Strip non-DOM custom flags that might leak
if ('consciousness' in sanitizedProps) delete sanitizedProps.consciousness;
const coerce = obj => normalizeTransitions(coerceBackgroundKey(obj));
if (sanitizedProps.animate && typeof sanitizedProps.animate === 'object') {
sanitizedProps.animate = coerce(sanitizedProps.animate);
}
if (sanitizedProps.initial && typeof sanitizedProps.initial === 'object') {
sanitizedProps.initial = coerce(sanitizedProps.initial);
}
if (sanitizedProps.exit && typeof sanitizedProps.exit === 'object') {
sanitizedProps.exit = coerce(sanitizedProps.exit);
}
if (sanitizedProps.whileHover && typeof sanitizedProps.whileHover === 'object') {
sanitizedProps.whileHover = coerce(sanitizedProps.whileHover);
}
if (sanitizedProps.whileInView && typeof sanitizedProps.whileInView === 'object') {
sanitizedProps.whileInView = coerce(sanitizedProps.whileInView);
}
if (sanitizedProps.variants && typeof sanitizedProps.variants === 'object') {
sanitizedProps.variants = coerce(sanitizedProps.variants);
}
if (sanitizedProps.transition) {
sanitizedProps = normalizeTransitions(sanitizedProps);
}
if (sanitizedProps.style && typeof sanitizedProps.style === 'object' && 'background' in sanitizedProps.style) {
const bg = sanitizedProps.style.background;
if (isSimpleColor(bg)) {
sanitizedProps.style = {
...sanitizedProps.style,
backgroundColor: bg === 'transparent' ? 'rgba(0, 0, 0, 0)' : bg
};
delete sanitizedProps.style.background;
}
}
return jsx(motion.div, {
...baseProps,
...sanitizedProps,
children: children
});
});
MotionFramer.displayName = 'MotionFramer';
export { MotionFramer };
//# sourceMappingURL=MotionFramer.js.map