UNPKG

aura-glass

Version:

A comprehensive glassmorphism design system for React applications with 142+ production-ready components

295 lines (292 loc) 10 kB
'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