aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
295 lines (292 loc) • 10 kB
JavaScript
'use client';
import { jsxs, jsx } from 'react/jsx-runtime';
import { useMotionValue, useTransform, motion, AnimatePresence } from 'framer-motion';
import { forwardRef, useState, useRef, useEffect, useCallback } from 'react';
import { useMotionPreference } from '../../hooks/useMotionPreference.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 { useGlassSound } from '../../utils/soundDesign.js';
const GlassOrbitalMenu = /*#__PURE__*/forwardRef(({
items = [],
centerElement,
radius = 120,
itemSize = 48,
isOpen = false,
onOpenChange,
rotationSpeed = 0.5,
autoRotate = false,
hoverExpansion = 1.2,
springTension = 300,
springFriction = 25,
glassConfig = {},
soundEnabled = true,
className = "",
style = {},
...props
}, ref) => {
const [activeItem, setActiveItem] = useState(null);
const [hoveredItem, setHoveredItem] = useState(null);
const [rotation, setRotation] = useState(0);
const containerRef = useRef(null);
const animationRef = useRef();
const {
prefersReducedMotion
} = useMotionPreference();
const menuId = useA11yId();
const {
play
} = useGlassSound();
const rotationValue = useMotionValue(0);
useTransform(rotationValue, [0, 360], [0, 360]);
useEffect(() => {
if (autoRotate && isOpen && !prefersReducedMotion) {
const animate = () => {
setRotation(prev => (prev + rotationSpeed) % 360);
rotationValue.set(rotation);
animationRef.current = requestAnimationFrame(animate);
};
animationRef.current = requestAnimationFrame(animate);
}
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
};
}, [autoRotate, isOpen, rotationSpeed, prefersReducedMotion, rotation, rotationValue]);
const handleCenterClick = useCallback(() => {
onOpenChange?.(!isOpen);
if (soundEnabled) {
play(isOpen ? "close" : "open");
}
}, [isOpen, onOpenChange, soundEnabled, play]);
const handleItemClick = useCallback(item => {
if (item.disabled) return;
setActiveItem(item.id);
item.onClick?.();
if (soundEnabled) {
play("click");
}
setTimeout(() => setActiveItem(null), 200);
}, [soundEnabled, play]);
const handleItemHover = useCallback(itemId => {
setHoveredItem(itemId);
if (soundEnabled && itemId) {
play("hover");
}
}, [soundEnabled, play]);
const calculateItemPosition = useCallback((index, total) => {
const angle = (360 / total * index + rotation) * (Math.PI / 180);
const x = Math.cos(angle) * radius;
const y = Math.sin(angle) * radius;
return {
x,
y,
angle: angle * (180 / Math.PI)
};
}, [radius, rotation]);
const getItemVariants = () => ({
hidden: {
scale: 0,
opacity: 0,
x: 0,
y: 0
},
visible: i => ({
scale: 1,
opacity: 1,
x: calculateItemPosition(i, items.length).x,
y: calculateItemPosition(i, items.length).y,
transition: {
type: "spring",
tension: springTension,
friction: springFriction,
delay: prefersReducedMotion ? 0 : i * 0.1
}
}),
hover: {
scale: hoverExpansion,
transition: {
type: "spring",
tension: springTension * 2,
friction: springFriction
}
},
active: {
scale: 0.9,
transition: {
type: "spring",
tension: springTension * 3,
friction: springFriction
}
}
});
const centerVariants = {
closed: {
rotate: 0,
scale: 1
},
open: {
rotate: prefersReducedMotion ? 0 : 180,
scale: 1.1,
transition: {
type: "spring",
tension: springTension,
friction: springFriction
}
}
};
return jsxs(OptimizedGlassCore, {
ref: ref,
className: `glass-orbital-menu relative ${className}`,
style: {
width: (radius + itemSize) * 2 + 40,
height: (radius + itemSize) * 2 + 40,
...style
},
glassConfig: {
blur: 20,
opacity: 0.8,
saturation: 1.2,
brightness: 1.1,
...glassConfig
},
"aria-label": "Orbital navigation menu",
role: "menu",
id: menuId,
...props,
children: [jsxs("div", {
ref: containerRef,
className: 'absolute inset-0 glass-flex glass-items-center glass-justify-center',
children: [jsx(motion.button, {
className: 'relative z-20 glass-flex glass-items-center glass-justify-center glass-surface glass-radius-full glass-border glass-border-white/20 glass-glass-backdrop-blur-md glass-surface-subtle/10 hover:glass-surface-subtle/15 transition-colors glass-contrast-guard',
style: {
width: itemSize + 8,
height: itemSize + 8
},
variants: centerVariants,
animate: isOpen ? "open" : "closed",
onClick: handleCenterClick,
"aria-expanded": isOpen,
"aria-controls": `${menuId}-items`,
"aria-label": isOpen ? "Close menu" : "Open menu",
children: centerElement || jsxs(motion.div, {
className: 'w-6 h-6 glass-flex glass-flex-col glass-items-center glass-justify-center',
animate: {
rotate: isOpen ? 45 : 0
},
children: [jsx("div", {
className: 'w-4 h-0-5 glass-surface-subtle/70 mb-1'
}), jsx("div", {
className: 'w-4 h-0-5 glass-surface-subtle/70'
})]
})
}), jsx("div", {
id: `${menuId}-items`,
className: 'absolute inset-0 glass-flex glass-items-center glass-justify-center pointer-events-none',
role: "group",
children: jsx(AnimatePresence, {
children: isOpen && items.map((item, index) => {
const position = calculateItemPosition(index, items.length);
const isHovered = hoveredItem === item.id;
const isActive = activeItem === item.id;
return jsx(motion.div, {
className: 'absolute pointer-events-auto',
custom: index,
variants: getItemVariants(),
initial: "hidden",
animate: "visible",
exit: "hidden",
whileHover: !item.disabled ? "hover" : undefined,
style: {
x: position.x,
y: position.y
},
children: jsxs(motion.button, {
className: `
relative flex items-center justify-center glass-surface rounded-full
border border-white/20 glass-backdrop-blur-md transition-all duration-200
${item.disabled ? "bg-white/5 text-white/40 cursor-not-allowed" : "bg-white/10 hover:bg-white/15 text-white/90 hover:text-white"}
${isActive ? "ring-2 ring-white/40" : ""}
`,
style: {
width: itemSize,
height: itemSize
},
onClick: () => handleItemClick(item),
onMouseEnter: () => handleItemHover(item.id),
onMouseLeave: () => handleItemHover(null),
disabled: item.disabled,
role: "menuitem",
"aria-label": item.label,
title: item.shortcut ? `${item.label} (${item.shortcut})` : item.label,
children: [item.icon && jsx("div", {
className: "glass-text-lg",
children: item.icon
}), item.shortcut && jsx("div", {
className: 'absolute -glass-top-2 -right-2 glass-surface-dark/50 text-primary/70 glass-text-xs glass-px-1 glass-py-0.5 glass-radius text-[10px] glass-glass-backdrop-blur-sm glass-contrast-guard',
children: item.shortcut
}), jsx(AnimatePresence, {
children: isHovered && !prefersReducedMotion && jsx(motion.div, {
className: 'absolute top-full mt-2 glass-surface-dark/80 text-primary glass-text-xs glass-px-2 glass-py-1 glass-radius glass-glass-backdrop-blur-sm whitespace-nowrap glass-contrast-guard',
initial: {
opacity: 0,
y: -5
},
animate: {
opacity: 1,
y: 0
},
exit: {
opacity: 0,
y: -5
},
transition: {
duration: 0.15
},
children: item.label
})
})]
})
}, item.id);
})
})
}), jsx(AnimatePresence, {
children: isOpen && jsx(motion.div, {
className: 'absolute glass-border glass-border-white/10 glass-radius-full pointer-events-none',
style: {
width: radius * 2,
height: radius * 2
},
initial: {
opacity: 0,
scale: 0.5
},
animate: {
opacity: 1,
scale: 1
},
exit: {
opacity: 0,
scale: 0.5
},
transition: {
type: "spring",
tension: springTension,
friction: springFriction
}
})
})]
}), jsxs("div", {
className: 'absolute bottom-2 right-2 glass-text-xs text-primary/60',
children: [items.length, " items"]
})]
});
});
GlassOrbitalMenu.displayName = "GlassOrbitalMenu";
export { GlassOrbitalMenu };
//# sourceMappingURL=GlassOrbitalMenu.js.map