{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "BubbleMenu-TS-CSS",
	"title": "BubbleMenu",
	"description": "Floating circular expanding menu with staggered item reveal.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:file",
			"path": "BubbleMenu.css",
			"target": "@components/BubbleMenu.css",
			"content": ".bubble-menu {\n  left: 0;\n  right: 0;\n  top: 2em;\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  gap: 16px;\n  padding: 0 2em;\n  pointer-events: none;\n  z-index: 99;\n}\n\n.bubble-menu.fixed {\n  position: fixed;\n}\n\n.bubble-menu.absolute {\n  position: absolute;\n}\n\n.bubble-menu .bubble {\n  --bubble-size: 48px;\n  width: var(--bubble-size);\n  height: var(--bubble-size);\n  border-radius: 50%;\n  background: #fff;\n  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  pointer-events: auto;\n}\n\n.bubble-menu .logo-bubble,\n.bubble-menu .toggle-bubble {\n  will-change: transform;\n}\n\n.bubble-menu .logo-bubble {\n  width: auto;\n  min-height: var(--bubble-size);\n  height: var(--bubble-size);\n  padding: 0 16px;\n  border-radius: calc(var(--bubble-size) / 2);\n  gap: 8px;\n}\n\n.bubble-menu .toggle-bubble {\n  width: var(--bubble-size);\n  height: var(--bubble-size);\n}\n\n.bubble-menu .bubble-logo {\n  max-height: 60%;\n  max-width: 100%;\n  object-fit: contain;\n  display: block;\n}\n\n.bubble-menu .logo-content {\n  --logo-max-height: 60%;\n  --logo-max-width: 100%;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  width: 120px;\n  height: 100%;\n}\n\n.bubble-menu .logo-content > .bubble-logo,\n.bubble-menu .logo-content > img,\n.bubble-menu .logo-content > svg {\n  max-height: var(--logo-max-height);\n  max-width: var(--logo-max-width);\n}\n\n.bubble-menu .menu-btn {\n  border: none;\n  background: #fff;\n  cursor: pointer;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  padding: 0;\n}\n\n.bubble-menu .menu-line {\n  width: 26px;\n  height: 2px;\n  background: #111;\n  border-radius: 2px;\n  display: block;\n  margin: 0 auto;\n  transition:\n    transform 0.3s ease,\n    opacity 0.3s ease;\n  transform-origin: center;\n}\n\n.bubble-menu .menu-line + .menu-line {\n  margin-top: 6px;\n}\n\n.bubble-menu .menu-btn.open .menu-line:first-child {\n  transform: translateY(4px) rotate(45deg);\n}\n\n.bubble-menu .menu-btn.open .menu-line:last-child {\n  transform: translateY(-4px) rotate(-45deg);\n}\n\n@media (min-width: 768px) {\n  .bubble-menu .bubble {\n    --bubble-size: 56px;\n  }\n\n  .bubble-menu .logo-bubble {\n    padding: 0 16px;\n  }\n}\n\n.bubble-menu-items {\n  position: absolute;\n  inset: 0;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  pointer-events: none;\n  z-index: 98;\n}\n\n.bubble-menu-items.fixed {\n  position: fixed;\n}\n\n.bubble-menu-items.absolute {\n  position: absolute;\n}\n\n.bubble-menu-items .pill-list {\n  list-style: none;\n  margin: 0;\n  padding: 0 24px;\n  display: flex;\n  flex-wrap: wrap;\n  gap: 0;\n  row-gap: 4px;\n  width: 100%;\n  max-width: 1600px;\n  margin-left: auto;\n  margin-right: auto;\n  pointer-events: auto;\n  justify-content: stretch;\n}\n\n.bubble-menu-items .pill-list .pill-spacer {\n  width: 100%;\n  height: 0;\n  pointer-events: none;\n}\n\n.bubble-menu-items .pill-list .pill-col {\n  display: flex;\n  justify-content: center;\n  align-items: stretch;\n  flex: 0 0 calc(100% / 3);\n  box-sizing: border-box;\n}\n\n.bubble-menu-items .pill-list .pill-col:nth-child(4):nth-last-child(2) {\n  margin-left: calc(100% / 6);\n}\n\n.bubble-menu-items .pill-list .pill-col:nth-child(4):last-child {\n  margin-left: calc(100% / 3);\n}\n\n.bubble-menu-items .pill-link {\n  --pill-bg: #ffffff;\n  --pill-color: #111;\n  --pill-border: rgba(0, 0, 0, 0.12);\n  --item-rot: 0deg;\n  --pill-min-h: 160px;\n  --hover-bg: #f3f4f6;\n  --hover-color: #111;\n  width: 100%;\n  min-height: var(--pill-min-h);\n  padding: clamp(1.5rem, 3vw, 8rem) 0;\n  font-size: clamp(1.5rem, 4vw, 4rem);\n  font-weight: 400;\n  line-height: 0;\n  border-radius: 999px;\n  background: var(--pill-bg);\n  color: var(--pill-color);\n  text-decoration: none;\n  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.1);\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  position: relative;\n  transition:\n    background 0.3s ease,\n    color 0.3s ease;\n  will-change: transform;\n  box-sizing: border-box;\n  white-space: nowrap;\n  overflow: hidden;\n  height: 10px;\n}\n\n@media (min-width: 900px) {\n  .bubble-menu-items .pill-link {\n    transform: rotate(var(--item-rot));\n  }\n\n  .bubble-menu-items .pill-link:hover {\n    transform: rotate(var(--item-rot)) scale(1.06);\n    background: var(--hover-bg);\n    color: var(--hover-color);\n  }\n\n  .bubble-menu-items .pill-link:active {\n    transform: rotate(var(--item-rot)) scale(0.94);\n  }\n}\n\n.bubble-menu-items .pill-link .pill-label {\n  display: inline-block;\n  will-change: transform, opacity;\n  height: 1.2em;\n  line-height: 1.2;\n}\n\n@media (max-width: 899px) {\n  .bubble-menu-items {\n    padding-top: 0px;\n    align-items: flex-start;\n    padding-top: 120px;\n  }\n\n  .bubble-menu-items .pill-list {\n    row-gap: 16px;\n  }\n\n  .bubble-menu-items .pill-list .pill-col {\n    flex: 0 0 100%;\n    margin-left: 0 !important;\n    overflow: visible;\n  }\n\n  .bubble-menu-items .pill-link {\n    font-size: clamp(1.2rem, 3vw, 4rem);\n    padding: clamp(1rem, 2vw, 2rem) 0;\n    min-height: 80px;\n  }\n\n  .bubble-menu-items .pill-link:hover {\n    transform: scale(1.06);\n    background: var(--hover-bg);\n    color: var(--hover-color);\n  }\n\n  .bubble-menu-items .pill-link:active {\n    transform: scale(0.94);\n  }\n}\n"
		},
		{
			"type": "registry:component",
			"path": "BubbleMenu.tsx",
			"content": "import type { CSSProperties, ReactNode } from 'react';\nimport { useState, useRef, useEffect } from 'react';\nimport { gsap } from 'gsap';\n\nimport './BubbleMenu.css';\n\ntype MenuItem = {\n  label: string;\n  href: string;\n  ariaLabel?: string;\n  rotation?: number;\n  hoverStyles?: {\n    bgColor?: string;\n    textColor?: string;\n  };\n};\n\nexport type BubbleMenuProps = {\n  logo: ReactNode | string;\n  onMenuClick?: (open: boolean) => void;\n  className?: string;\n  style?: CSSProperties;\n  menuAriaLabel?: string;\n  menuBg?: string;\n  menuContentColor?: string;\n  useFixedPosition?: boolean;\n  items?: MenuItem[];\n  animationEase?: string;\n  animationDuration?: number;\n  staggerDelay?: number;\n};\n\nconst DEFAULT_ITEMS: MenuItem[] = [\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}: BubbleMenuProps) {\n  const [isMenuOpen, setIsMenuOpen] = useState(false);\n  const [showOverlay, setShowOverlay] = useState(false);\n\n  const overlayRef = useRef<HTMLDivElement>(null);\n  const bubblesRef = useRef<HTMLAnchorElement[]>([]);\n  const labelRefs = useRef<HTMLSpanElement[]>([]);\n\n  const menuItems = items?.length ? items : DEFAULT_ITEMS;\n  const containerClassName = ['bubble-menu', useFixedPosition ? 'fixed' : 'absolute', className]\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\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\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\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\n    window.addEventListener('resize', handleResize);\n    return () => window.removeEventListener('resize', handleResize);\n  }, [isMenuOpen, menuItems]);\n\n  return (\n    <>\n      <nav className={containerClassName} style={style} aria-label=\"Main navigation\">\n        <div className=\"bubble logo-bubble\" aria-label=\"Logo\" style={{ background: menuBg }}>\n          <span className=\"logo-content\">\n            {typeof logo === 'string' ? <img src={logo} alt=\"Logo\" className=\"bubble-logo\" /> : logo}\n          </span>\n        </div>\n\n        <button\n          type=\"button\"\n          className={`bubble toggle-bubble menu-btn ${isMenuOpen ? 'open' : ''}`}\n          onClick={handleToggle}\n          aria-label={menuAriaLabel}\n          aria-pressed={isMenuOpen}\n          style={{ background: menuBg }}\n        >\n          <span className=\"menu-line\" style={{ background: menuContentColor }} />\n          <span className=\"menu-line short\" style={{ background: menuContentColor }} />\n        </button>\n      </nav>\n      {showOverlay && (\n        <div\n          ref={overlayRef}\n          className={`bubble-menu-items ${useFixedPosition ? 'fixed' : 'absolute'}`}\n          aria-hidden={!isMenuOpen}\n        >\n          <ul className=\"pill-list\" role=\"menu\" aria-label=\"Menu links\">\n            {menuItems.map((item, idx) => (\n              <li key={idx} role=\"none\" className=\"pill-col\">\n                <a\n                  role=\"menuitem\"\n                  href={item.href}\n                  aria-label={item.ariaLabel || item.label}\n                  className=\"pill-link\"\n                  style={\n                    {\n                      '--item-rot': `${item.rotation ?? 0}deg`,\n                      '--pill-bg': menuBg,\n                      '--pill-color': menuContentColor,\n                      '--hover-bg': item.hoverStyles?.bgColor || '#f3f4f6',\n                      '--hover-color': item.hoverStyles?.textColor || menuContentColor\n                    } as CSSProperties\n                  }\n                  ref={el => {\n                    if (el) bubblesRef.current[idx] = el;\n                  }}\n                >\n                  <span\n                    className=\"pill-label\"\n                    ref={el => {\n                      if (el) labelRefs.current[idx] = el;\n                    }}\n                  >\n                    {item.label}\n                  </span>\n                </a>\n              </li>\n            ))}\n          </ul>\n        </div>\n      )}\n    </>\n  );\n}\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"gsap@^3.13.0"
	]
}