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