{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "BubbleMenu-JS-CSS", "type": "registry:block", "title": "BubbleMenu", "description": "Floating circular expanding menu with staggered item reveal.", "dependencies": [ "gsap" ], "files": [ { "path": "public/default/src/content/Components/BubbleMenu/BubbleMenu.jsx", "content": "import { useState, useRef, useEffect } from 'react';\r\nimport { gsap } from 'gsap';\r\n\r\nimport './BubbleMenu.css';\r\n\r\nconst DEFAULT_ITEMS = [\r\n {\r\n label: 'home',\r\n href: '#',\r\n ariaLabel: 'Home',\r\n rotation: -8,\r\n hoverStyles: { bgColor: '#3b82f6', textColor: '#ffffff' }\r\n },\r\n {\r\n label: 'about',\r\n href: '#',\r\n ariaLabel: 'About',\r\n rotation: 8,\r\n hoverStyles: { bgColor: '#10b981', textColor: '#ffffff' }\r\n },\r\n {\r\n label: 'projects',\r\n href: '#',\r\n ariaLabel: 'Documentation',\r\n rotation: 8,\r\n hoverStyles: { bgColor: '#f59e0b', textColor: '#ffffff' }\r\n },\r\n {\r\n label: 'blog',\r\n href: '#',\r\n ariaLabel: 'Blog',\r\n rotation: 8,\r\n hoverStyles: { bgColor: '#ef4444', textColor: '#ffffff' }\r\n },\r\n {\r\n label: 'contact',\r\n href: '#',\r\n ariaLabel: 'Contact',\r\n rotation: -8,\r\n hoverStyles: { bgColor: '#8b5cf6', textColor: '#ffffff' }\r\n }\r\n];\r\n\r\nexport default function BubbleMenu({\r\n logo,\r\n onMenuClick,\r\n className,\r\n style,\r\n menuAriaLabel = 'Toggle menu',\r\n menuBg = '#fff',\r\n menuContentColor = '#111',\r\n useFixedPosition = false,\r\n items,\r\n animationEase = 'back.out(1.5)',\r\n animationDuration = 0.5,\r\n staggerDelay = 0.12\r\n}) {\r\n const [isMenuOpen, setIsMenuOpen] = useState(false);\r\n const [showOverlay, setShowOverlay] = useState(false);\r\n\r\n const overlayRef = useRef(null);\r\n const bubblesRef = useRef([]);\r\n const labelRefs = useRef([]);\r\n\r\n const menuItems = items?.length ? items : DEFAULT_ITEMS;\r\n const containerClassName = ['bubble-menu', useFixedPosition ? 'fixed' : 'absolute', className]\r\n .filter(Boolean)\r\n .join(' ');\r\n\r\n const handleToggle = () => {\r\n const nextState = !isMenuOpen;\r\n if (nextState) setShowOverlay(true);\r\n setIsMenuOpen(nextState);\r\n onMenuClick?.(nextState);\r\n };\r\n\r\n useEffect(() => {\r\n const overlay = overlayRef.current;\r\n const bubbles = bubblesRef.current.filter(Boolean);\r\n const labels = labelRefs.current.filter(Boolean);\r\n\r\n if (!overlay || !bubbles.length) return;\r\n\r\n if (isMenuOpen) {\r\n gsap.set(overlay, { display: 'flex' });\r\n gsap.killTweensOf([...bubbles, ...labels]);\r\n gsap.set(bubbles, { scale: 0, transformOrigin: '50% 50%' });\r\n gsap.set(labels, { y: 24, autoAlpha: 0 });\r\n\r\n bubbles.forEach((bubble, i) => {\r\n const delay = i * staggerDelay + gsap.utils.random(-0.05, 0.05);\r\n const tl = gsap.timeline({ delay });\r\n\r\n tl.to(bubble, {\r\n scale: 1,\r\n duration: animationDuration,\r\n ease: animationEase\r\n });\r\n if (labels[i]) {\r\n tl.to(\r\n labels[i],\r\n {\r\n y: 0,\r\n autoAlpha: 1,\r\n duration: animationDuration,\r\n ease: 'power3.out'\r\n },\r\n `-=${animationDuration * 0.9}`\r\n );\r\n }\r\n });\r\n } else if (showOverlay) {\r\n gsap.killTweensOf([...bubbles, ...labels]);\r\n gsap.to(labels, {\r\n y: 24,\r\n autoAlpha: 0,\r\n duration: 0.2,\r\n ease: 'power3.in'\r\n });\r\n gsap.to(bubbles, {\r\n scale: 0,\r\n duration: 0.2,\r\n ease: 'power3.in',\r\n onComplete: () => {\r\n gsap.set(overlay, { display: 'none' });\r\n setShowOverlay(false);\r\n }\r\n });\r\n }\r\n }, [isMenuOpen, showOverlay, animationEase, animationDuration, staggerDelay]);\r\n\r\n useEffect(() => {\r\n const handleResize = () => {\r\n if (isMenuOpen) {\r\n const bubbles = bubblesRef.current.filter(Boolean);\r\n const isDesktop = window.innerWidth >= 900;\r\n\r\n bubbles.forEach((bubble, i) => {\r\n const item = menuItems[i];\r\n if (bubble && item) {\r\n const rotation = isDesktop ? (item.rotation ?? 0) : 0;\r\n gsap.set(bubble, { rotation });\r\n }\r\n });\r\n }\r\n };\r\n\r\n window.addEventListener('resize', handleResize);\r\n return () => window.removeEventListener('resize', handleResize);\r\n }, [isMenuOpen, menuItems]);\r\n\r\n return (\r\n <>\r\n \r\n {showOverlay && (\r\n