{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "BubbleMenu-TS-TW",
	"title": "BubbleMenu",
	"description": "Floating circular expanding menu with staggered item reveal.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "BubbleMenu/BubbleMenu.tsx",
			"content": "import type { CSSProperties, ReactNode } from 'react';\nimport { useEffect, useRef, useState } from 'react';\nimport { gsap } from 'gsap';\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\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      <style>{`\n        .bubble-menu .menu-line {\n          transition: transform 0.3s ease, opacity 0.3s ease;\n          transform-origin: center;\n        }\n        .bubble-menu-items .pill-list .pill-col:nth-child(4):nth-last-child(2) {\n          margin-left: calc(100% / 6);\n        }\n        .bubble-menu-items .pill-list .pill-col:nth-child(4):last-child {\n          margin-left: calc(100% / 3);\n        }\n        @media (min-width: 900px) {\n          .bubble-menu-items .pill-link {\n            transform: rotate(var(--item-rot));\n          }\n          .bubble-menu-items .pill-link:hover {\n            transform: rotate(var(--item-rot)) scale(1.06);\n            background: var(--hover-bg) !important;\n            color: var(--hover-color) !important;\n          }\n          .bubble-menu-items .pill-link:active {\n            transform: rotate(var(--item-rot)) scale(.94);\n          }\n        }\n        @media (max-width: 899px) {\n          .bubble-menu-items {\n            padding-top: 120px;\n            align-items: flex-start;\n          }\n          .bubble-menu-items .pill-list {\n            row-gap: 16px;\n          }\n          .bubble-menu-items .pill-list .pill-col {\n            flex: 0 0 100% !important;\n            margin-left: 0 !important;\n            overflow: visible;\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 !important;\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          .bubble-menu-items .pill-link:active {\n            transform: scale(.94);\n          }\n        }\n      `}</style>\n\n      <nav className={containerClassName} style={style} aria-label=\"Main navigation\">\n        <div\n          className={[\n            'bubble logo-bubble',\n            'inline-flex items-center justify-center',\n            'rounded-full',\n            'bg-white',\n            'shadow-[0_4px_16px_rgba(0,0,0,0.12)]',\n            'pointer-events-auto',\n            'h-12 md:h-14',\n            'px-4 md:px-8',\n            'gap-2',\n            'will-change-transform'\n          ].join(' ')}\n          aria-label=\"Logo\"\n          style={{\n            background: menuBg,\n            minHeight: '48px',\n            borderRadius: '9999px'\n          }}\n        >\n          <span\n            className={['logo-content', 'inline-flex items-center justify-center', 'w-[120px] h-full'].join(' ')}\n            style={\n              {\n                ['--logo-max-height']: '60%',\n                ['--logo-max-width']: '100%'\n              } as CSSProperties\n            }\n          >\n            {typeof logo === 'string' ? (\n              <img src={logo} alt=\"Logo\" className=\"bubble-logo max-h-[60%] max-w-full object-contain block\" />\n            ) : (\n              logo\n            )}\n          </span>\n        </div>\n\n        <button\n          type=\"button\"\n          className={[\n            'bubble toggle-bubble menu-btn',\n            isMenuOpen ? 'open' : '',\n            'inline-flex flex-col items-center justify-center',\n            'rounded-full',\n            'bg-white',\n            'shadow-[0_4px_16px_rgba(0,0,0,0.12)]',\n            'pointer-events-auto',\n            'w-12 h-12 md:w-14 md:h-14',\n            'border-0 cursor-pointer p-0',\n            'will-change-transform'\n          ].join(' ')}\n          onClick={handleToggle}\n          aria-label={menuAriaLabel}\n          aria-pressed={isMenuOpen}\n          style={{ background: menuBg }}\n        >\n          <span\n            className=\"menu-line block mx-auto rounded-[2px]\"\n            style={{\n              width: 26,\n              height: 2,\n              background: menuContentColor,\n              transform: isMenuOpen ? 'translateY(4px) rotate(45deg)' : 'none'\n            }}\n          />\n          <span\n            className=\"menu-line short block mx-auto rounded-[2px]\"\n            style={{\n              marginTop: '6px',\n              width: 26,\n              height: 2,\n              background: menuContentColor,\n              transform: isMenuOpen ? 'translateY(-4px) rotate(-45deg)' : 'none'\n            }}\n          />\n        </button>\n      </nav>\n\n      {showOverlay && (\n        <div\n          ref={overlayRef}\n          className={[\n            'bubble-menu-items',\n            useFixedPosition ? 'fixed' : 'absolute',\n            'inset-0',\n            'flex items-center justify-center',\n            'pointer-events-none',\n            'z-[1000]'\n          ].join(' ')}\n          aria-hidden={!isMenuOpen}\n        >\n          <ul\n            className={[\n              'pill-list',\n              'list-none m-0 px-6',\n              'w-full max-w-[1600px] mx-auto',\n              'flex flex-wrap',\n              'gap-x-0 gap-y-1',\n              'pointer-events-auto'\n            ].join(' ')}\n            role=\"menu\"\n            aria-label=\"Menu links\"\n          >\n            {menuItems.map((item, idx) => (\n              <li\n                key={idx}\n                role=\"none\"\n                className={[\n                  'pill-col',\n                  'flex justify-center items-stretch',\n                  '[flex:0_0_calc(100%/3)]',\n                  'box-border'\n                ].join(' ')}\n              >\n                <a\n                  role=\"menuitem\"\n                  href={item.href}\n                  aria-label={item.ariaLabel || item.label}\n                  className={[\n                    'pill-link',\n                    'w-full',\n                    'rounded-[999px]',\n                    'no-underline',\n                    'bg-white',\n                    'text-inherit',\n                    'shadow-[0_4px_14px_rgba(0,0,0,0.10)]',\n                    'flex items-center justify-center',\n                    'relative',\n                    'transition-[background,color] duration-300 ease-in-out',\n                    'box-border',\n                    'whitespace-nowrap overflow-hidden'\n                  ].join(' ')}\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                      background: 'var(--pill-bg)',\n                      color: 'var(--pill-color)',\n                      minHeight: 'var(--pill-min-h, 160px)',\n                      padding: 'clamp(1.5rem, 3vw, 8rem) 0',\n                      fontSize: 'clamp(1.5rem, 4vw, 4rem)',\n                      fontWeight: 400,\n                      lineHeight: 0,\n                      willChange: 'transform',\n                      height: 10\n                    } as CSSProperties\n                  }\n                  ref={el => {\n                    if (el) bubblesRef.current[idx] = el;\n                  }}\n                >\n                  <span\n                    className=\"pill-label inline-block\"\n                    style={{\n                      willChange: 'transform, opacity',\n                      height: '1.2em',\n                      lineHeight: 1.2\n                    }}\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"
	]
}