\n \n Metaball navigation\n
\n\n Point at a link. A pool of lime stretches across the bar, sheds a few droplets on the way,\n and settles under the label. Open a menu and the card below re-forms around it.\n
\n{menu.feature.title}
\n\n {menu.feature.description}\n
\n closeMenu()}\n className={cn(\n 'group/feature mt-auto inline-flex items-center gap-1.5 pt-3 text-[12px] font-medium text-[#c6f56f]',\n focusRing\n )}\n >\n {menu.feature.cta ?? 'Explore'}\nimport ArcCarousel from '@/components/tweenui/arc-carousel';\n\nexport default function Example() {\n return <ArcCarousel />;\n}"
}
+ },
+ "navbar-gooey-bloom": {
+ "react": {
+ "code": "import NavbarGooeyBloom from '@/components/tweenui/navbar-gooey-bloom';\n\nexport default function Example() {\n return import NavbarGooeyBloom from '@/components/tweenui/navbar-gooey-bloom';\n\nexport default function Example() {\n return <NavbarGooeyBloom />;\n}"
+ }
}
};
@@ -705,5 +711,11 @@ export const manual: Record\n {active ? `${current + 1} of ${count}: ${active.title}` : ''}\n
\n\n \n \n'use client';\n\nimport {\n useCallback,\n useEffect,\n useRef,\n useState,\n type ComponentPropsWithoutRef,\n type KeyboardEvent,\n type MouseEvent,\n} from 'react';\nimport { useGSAP } from '@gsap/react';\nimport gsap from 'gsap';\nimport { CustomEase } from 'gsap/CustomEase';\nimport { ScrollTrigger } from 'gsap/ScrollTrigger';\nimport { cn } from '@/lib/utils';\n\ngsap.registerPlugin(useGSAP, CustomEase, ScrollTrigger);\n\nconst prefersReducedMotion = () =>\n typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\nexport interface ArcCarouselItem {\n title: string;\n image: string;\n /** Chip on the top left of the card. */\n tag?: string;\n /** Line under the title, e.g. a client and year or a price. */\n meta?: string;\n href?: string;\n}\n\nexport interface ArcCarouselProps extends ComponentPropsWithoutRef<'section'> {\n /** Eight or more keep the wrap seamless. */\n items?: ArcCarouselItem[];\n /** Accessible name for the carousel. */\n label?: string;\n /** Step on its own; holds on hover, keyboard focus and while off-screen. */\n autoplay?: boolean;\n /** Time each card holds the centre while autoplaying, in ms. */\n interval?: number;\n}\n\n// Card position by distance from the centre: px across, px down, degrees of turn\nconst SLOTS = [\n { x: 0, y: 0, r: 0 },\n { x: 359, y: 26, r: 8.77 },\n { x: 711, y: 113, r: 18.83 },\n { x: 1033, y: 243, r: 25.05 },\n];\nconst ACTIVE_SCALE = 1.05;\nconst MOVE_DURATION = 0.8;\nconst MOVE_EASE = CustomEase.create('arc-move', 'M0,0 C0.98,0.3 0.3,1 1,1');\nconst SWIPE = 50;\n\n/** Where a card sits at a (fractional) distance from the centre. Past the last slot it fades out. */\nconst slot = (offset: number) => {\n const t = Math.abs(offset);\n const sign = Math.sign(offset);\n const i = Math.min(Math.floor(t), SLOTS.length - 2);\n const a = SLOTS[i];\n const b = SLOTS[i + 1];\n const f = t - i;\n return {\n x: (a.x + (b.x - a.x) * f) * sign,\n y: a.y + (b.y - a.y) * f,\n rotation: (a.r + (b.r - a.r) * f) * sign,\n scale: 1 + (ACTIVE_SCALE - 1) * gsap.utils.clamp(0, 1, 1 - t),\n opacity: gsap.utils.clamp(0, 1, SLOTS.length - t),\n zIndex: 100 - Math.round(t * 10),\n };\n};\n\nconst pad2 = (n: number) => String(n).padStart(2, '0');\n\nconst photo = (id: string, width = 640) =>\n `https://images.unsplash.com/photo-${id}?w=${width}&h=${Math.round(width * (430 / 312))}&fit=crop&q=80&auto=format`;\nconst srcSetOf = (src: string) =>\n src.includes('images.unsplash.com')\n ? `${src.replace(/([?&])w=\\d+&h=\\d+/, '$1w=320&h=441')} 1x, ${src} 2x`\n : undefined;\n\nconst DEFAULT_ITEMS: ArcCarouselItem[] = [\n {\n title: 'Nightfall',\n tag: 'Astronomy app',\n meta: 'Stellar Co. · 2026',\n image: photo('1519681393784-d120267933ba'),\n },\n {\n title: 'Chroma Identity',\n tag: 'Brand identity',\n meta: 'Chroma Studio · 2026',\n image: photo('1541701494587-cb58502866ab'),\n },\n {\n title: 'Alpine Lodge',\n tag: 'Hospitality',\n meta: 'Moraine Hotels · 2026',\n image: photo('1493246507139-91e8fad9978e'),\n },\n {\n title: 'Metro Pulse',\n tag: 'Data visualisation',\n meta: 'City of Lakes · 2025',\n image: photo('1477959858617-67f85cf4f1df'),\n },\n {\n title: 'Orbit Motion',\n tag: '3D & motion',\n meta: 'Orbit Labs · 2025',\n image: photo('1618005198919-d3d4b5a92ead'),\n },\n {\n title: 'Open Road',\n tag: 'Campaign film',\n meta: 'Westward · 2025',\n image: photo('1500530855697-b586d89ba3ee'),\n },\n {\n title: 'Still Water',\n tag: 'Wellness app',\n meta: 'Calm Harbor · 2024',\n image: photo('1534447677768-be436bb09401'),\n },\n {\n title: 'Lago Retreats',\n tag: 'Travel platform',\n meta: 'Lago Travel · 2024',\n image: photo('1501785888041-af3ef285b470'),\n },\n];\n\nconst STEP = cn(\n 'grid size-11 place-items-center rounded-full border border-[#9fd4d6]/25 text-white',\n 'transition-[background-color,border-color,color] duration-300 ease-[cubic-bezier(0.32,0.72,0,1)] hover:border-[#c6f56f] hover:bg-[#c6f56f] hover:text-[#03110f]',\n 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#c6f56f] motion-reduce:transition-none'\n);\nconst FADE =\n 'pointer-events-none absolute inset-y-0 z-2 w-[clamp(2.5rem,14cqw,12rem)] from-[#03110f] via-[#03110f]/70 to-transparent';\n\nconst Caret = ({ flip }: { flip?: boolean }) => (\n <svg\n viewBox=\"0 0 9 16\"\n aria-hidden=\"true\"\n className={cn('h-4 w-[8.5px]', flip && 'rotate-180')}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.6\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"M7.5 1.5 1.5 8l6 6.5\" />\n </svg>\n);\n\nconst PauseIcon = () => (\n <svg viewBox=\"0 0 16 16\" aria-hidden=\"true\" className=\"size-3.5\" fill=\"currentColor\">\n <rect x=\"3.5\" y=\"2.5\" width=\"3\" height=\"11\" rx=\"1\" />\n <rect x=\"9.5\" y=\"2.5\" width=\"3\" height=\"11\" rx=\"1\" />\n </svg>\n);\n\nconst PlayIcon = () => (\n <svg viewBox=\"0 0 16 16\" aria-hidden=\"true\" className=\"size-3.5\" fill=\"currentColor\">\n <path d=\"M4.5 2.9v10.2a1 1 0 0 0 1.53.85l8.1-5.1a1 1 0 0 0 0-1.7l-8.1-5.1A1 1 0 0 0 4.5 2.9Z\" />\n </svg>\n);\n\nconst ArrowUpRight = ({ className }: { className?: string }) => (\n <svg\n viewBox=\"0 0 16 16\"\n aria-hidden=\"true\"\n className={className}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.6\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"M4.5 11.5 11.5 4.5M5.5 4.5h6v6\" />\n </svg>\n);\n\nconst ArcCarousel = ({\n items = DEFAULT_ITEMS,\n label = 'Featured projects',\n autoplay = true,\n interval = 5000,\n className,\n ...props\n}: ArcCarouselProps) => {\n const count = items.length;\n const [position, setPosition] = useState(0);\n const [playing, setPlaying] = useState(autoplay);\n const [held, setHeld] = useState(false);\n const rootRef = useRef<HTMLElement>(null);\n const ringRef = useRef<SVGCircleElement>(null);\n const cardRefs = useRef<(HTMLDivElement | null)[]>([]);\n const track = useRef({ pos: 0 });\n const clockRef = useRef<gsap.core.Tween | null>(null);\n const swipe = useRef({ tracking: false, swiped: false, x: 0, y: 0 });\n const holds = useRef({ hover: false, focus: false, offscreen: false });\n\n const current = count ? gsap.utils.wrap(0, count, position) : 0;\n const active = items[current];\n const offsetOf = (index: number, pos: number) =>\n gsap.utils.wrap(-count / 2, count / 2, index - pos);\n\n const step = useCallback((direction: number) => setPosition((p) => p + direction), []);\n\n // `playing` is the viewer's choice; a hold is a pause they did not ask for\n const hold = useCallback((key: keyof typeof holds.current, on: boolean) => {\n holds.current[key] = on;\n setHeld(Object.values(holds.current).some(Boolean));\n }, []);\n\n // Every card is placed from one number, so a step is a single tween and the wrap needs no copies\n useGSAP(\n () => {\n const cards = cardRefs.current.filter((card): card is HTMLDivElement => card !== null);\n const render = () =>\n cards.forEach((card, i) => {\n const s = slot(offsetOf(i, track.current.pos));\n gsap.set(card, { ...s, pointerEvents: s.opacity > 0.5 ? 'auto' : 'none' });\n });\n\n gsap.to(track.current, {\n pos: position,\n duration: prefersReducedMotion() ? 0 : MOVE_DURATION,\n ease: MOVE_EASE,\n overwrite: true,\n onUpdate: render,\n });\n },\n { scope: rootRef, dependencies: [position, count] }\n );\n\n // The media query is client-only, so reduced motion applies after hydration\n useEffect(() => {\n setPlaying(autoplay && !prefersReducedMotion());\n }, [autoplay]);\n\n // The clock is the progress ring: it fills over `interval`, then steps. Any step restarts it\n useGSAP(\n () => {\n const ring = ringRef.current;\n if (!ring || !playing || count < 2) return;\n clockRef.current = gsap.fromTo(\n ring,\n // The attribute, not the CSS property, so the dash stays in `pathLength` units\n { attr: { 'stroke-dashoffset': 1 } },\n {\n attr: { 'stroke-dashoffset': 0 },\n duration: interval / 1000,\n ease: 'none',\n onComplete: () => step(1),\n }\n );\n },\n { scope: rootRef, dependencies: [playing, position, interval, count], revertOnUpdate: true }\n );\n\n useEffect(() => {\n if (held) clockRef.current?.pause();\n else clockRef.current?.resume();\n }, [held, position, playing]);\n\n useGSAP(\n () => {\n if (!autoplay) return;\n ScrollTrigger.create({\n trigger: rootRef.current,\n onToggle: (self) => hold('offscreen', !self.isActive),\n });\n },\n { scope: rootRef, dependencies: [autoplay, hold] }\n );\n\n // A swipe can end outside the list\n useEffect(() => {\n const onUp = (event: PointerEvent) => {\n const s = swipe.current;\n if (!s.tracking) return;\n s.tracking = false;\n const dx = event.clientX - s.x;\n if (Math.abs(dx) > SWIPE && Math.abs(dx) > Math.abs(event.clientY - s.y)) {\n s.swiped = true;\n step(dx < 0 ? 1 : -1);\n }\n };\n const onCancel = () => {\n swipe.current.tracking = false;\n };\n window.addEventListener('pointerup', onUp);\n window.addEventListener('pointercancel', onCancel);\n return () => {\n window.removeEventListener('pointerup', onUp);\n window.removeEventListener('pointercancel', onCancel);\n };\n }, [step]);\n\n // After a swipe, or on a side card, a click steps the arc instead of following the card's link\n const onClickCapture = (event: MouseEvent<HTMLDivElement>) => {\n if (swipe.current.swiped) {\n swipe.current.swiped = false;\n event.preventDefault();\n event.stopPropagation();\n return;\n }\n if (event.detail === 0) return;\n const index = cardRefs.current.findIndex((card) => card?.contains(event.target as Node));\n const offset = index < 0 ? 0 : Math.round(offsetOf(index, position));\n if (offset !== 0) {\n event.preventDefault();\n event.stopPropagation();\n step(Math.sign(offset));\n }\n };\n\n const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return;\n event.preventDefault();\n step(event.key === 'ArrowRight' ? 1 : -1);\n };\n\n return (\n <section\n ref={rootRef}\n aria-label={label}\n className={cn(\n '@container relative isolate flex w-full flex-col items-center gap-4 overflow-hidden bg-[#03110f] py-16 text-white',\n '@max-[48rem]:gap-8 @max-[48rem]:py-12',\n className\n )}\n {...props}\n >\n <div\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute top-[62%] left-1/2 -z-10 size-[min(120cqw,70rem)] -translate-x-1/2 -translate-y-1/2 rounded-full bg-[radial-gradient(closest-side,color-mix(in_oklch,#045f64_45%,transparent),color-mix(in_oklch,#045f64_10%,transparent)_55%,transparent)]\"\n />\n\n {/* Isolated, so the cards' z-indexes stay under the edge fades */}\n <div\n className=\"relative isolate z-1 h-150 w-78 @max-[64rem]:h-134 @max-[48rem]:h-72.5\"\n onPointerEnter={(event) => event.pointerType !== 'touch' && hold('hover', true)}\n onPointerLeave={() => hold('hover', false)}\n >\n <div className=\"absolute inset-0 origin-[50%_0] @max-[64rem]:scale-80 @max-[48rem]:scale-62\">\n <div\n role=\"group\"\n tabIndex={0}\n aria-roledescription=\"carousel\"\n aria-label={label}\n className=\"absolute top-16 left-0 h-107.5 w-78 touch-pan-y rounded-[8px] outline-none select-none focus-visible:ring-2 focus-visible:ring-[#c6f56f] focus-visible:ring-offset-8 focus-visible:ring-offset-[#03110f] @max-[48rem]:top-2\"\n onPointerDown={(event) => {\n swipe.current = { tracking: true, swiped: false, x: event.clientX, y: event.clientY };\n }}\n onDragStart={(event) => event.preventDefault()}\n onClickCapture={onClickCapture}\n onKeyDown={onKeyDown}\n onFocus={(event) => {\n let visible = true;\n try {\n visible = event.target.matches(':focus-visible');\n } catch {\n // No :focus-visible support: treat every focus as visible\n }\n if (visible) hold('focus', true);\n }}\n onBlur={(event) => {\n if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {\n hold('focus', false);\n }\n }}\n >\n {items.map((item, i) => {\n const isActive = i === current;\n const Card = item.href ? 'a' : 'div';\n // Also set in markup, so the arc is in place before hydration\n const first = slot(offsetOf(i, 0));\n return (\n <div\n key={`${item.title}-${i}`}\n ref={(el) => {\n cardRefs.current[i] = el;\n }}\n role=\"group\"\n aria-roledescription=\"slide\"\n aria-label={`${i + 1} of ${count}: ${item.title}`}\n aria-hidden={isActive ? undefined : true}\n data-active={isActive ? '' : undefined}\n className=\"group/card absolute top-0 left-0 h-107.5 w-78 will-change-transform\"\n style={{\n transform: `translate(${first.x}px, ${first.y}px) rotate(${first.rotation}deg) scale(${first.scale})`,\n opacity: first.opacity,\n zIndex: first.zIndex,\n }}\n >\n <Card\n {...(item.href ? { href: item.href } : {})}\n tabIndex={item.href && isActive ? 0 : -1}\n draggable={false}\n className={cn(\n 'relative block size-full overflow-hidden rounded-[8px] bg-[#071d1b]',\n 'shadow-[0_30px_60px_-24px_rgb(0_0_0/0.75)]',\n 'focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[#c6f56f]'\n )}\n >\n <img\n src={item.image}\n srcSet={srcSetOf(item.image)}\n alt=\"\"\n width={312}\n height={430}\n draggable={false}\n decoding=\"async\"\n loading={Math.abs(offsetOf(i, 0)) <= 2 ? 'eager' : 'lazy'}\n className=\"absolute inset-0 size-full object-cover\"\n />\n <span\n aria-hidden=\"true\"\n className=\"absolute inset-0 bg-[#03110f]/40 transition-opacity duration-500 ease-[cubic-bezier(0.32,0.72,0,1)] group-data-active/card:opacity-0 motion-reduce:transition-none\"\n />\n <span\n aria-hidden=\"true\"\n className=\"absolute inset-x-0 bottom-0 h-3/5 bg-linear-to-t from-[#03110f] via-[#03110f]/60 to-transparent\"\n />\n\n <span className=\"absolute inset-x-4 top-4 flex items-center justify-between gap-3\">\n {item.tag ? (\n <span className=\"truncate rounded-full bg-[#03110f]/70 px-3 py-1.5 text-[11px] leading-none font-medium tracking-[0.12em] text-white uppercase ring-1 ring-white/10\">\n {item.tag}\n </span>\n ) : (\n <span />\n )}\n <span className=\"shrink-0 text-xs font-medium text-white/70 tabular-nums\">\n {pad2(i + 1)}\n <span className=\"text-white/35\"> / {pad2(count)}</span>\n </span>\n </span>\n\n <span className=\"absolute inset-x-0 bottom-0 flex items-end justify-between gap-4 p-5\">\n <span className=\"grid min-w-0 gap-1.5\">\n <span className=\"truncate text-[22px] leading-tight font-semibold tracking-[-0.025em] text-white\">\n {item.title}\n </span>\n {item.meta && (\n <span className=\"truncate text-sm text-[#9fd4d6]/80\">{item.meta}</span>\n )}\n </span>\n {item.href && (\n <span\n aria-hidden=\"true\"\n className={cn(\n 'grid size-11 shrink-0 place-items-center rounded-full bg-white/10 text-white ring-1 ring-white/20',\n 'transition-[background-color,color,box-shadow] duration-500 ease-[cubic-bezier(0.32,0.72,0,1)] motion-reduce:transition-none',\n 'group-data-active/card:bg-[#c6f56f] group-data-active/card:text-[#03110f] group-data-active/card:ring-transparent'\n )}\n >\n <ArrowUpRight className=\"size-4 transition-transform duration-300 ease-[cubic-bezier(0.32,0.72,0,1)] group-hover/card:rotate-45 motion-reduce:transition-none\" />\n </span>\n )}\n </span>\n\n <span\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute inset-0 rounded-[8px] ring-1 ring-white/10 transition-shadow duration-500 ease-[cubic-bezier(0.32,0.72,0,1)] ring-inset group-data-active/card:ring-2 group-data-active/card:ring-[#c6f56f]/80 motion-reduce:transition-none\"\n />\n </Card>\n </div>\n );\n })}\n </div>\n </div>\n </div>\n\n <div className=\"relative z-3 flex items-center gap-2\">\n <button\n className={STEP}\n type=\"button\"\n aria-label=\"Previous project\"\n onClick={() => step(-1)}\n >\n <Caret />\n </button>\n <button className={STEP} type=\"button\" aria-label=\"Next project\" onClick={() => step(1)}>\n <Caret flip />\n </button>\n {autoplay && (\n <button\n className={cn(STEP, 'relative ml-2')}\n type=\"button\"\n aria-label={playing ? 'Pause autoplay' : 'Start autoplay'}\n onClick={() => setPlaying((on) => !on)}\n >\n {playing ? <PauseIcon /> : <PlayIcon />}\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 44 44\"\n className={cn(\n 'pointer-events-none absolute -inset-px -rotate-90 transition-opacity duration-300',\n playing ? 'opacity-100' : 'opacity-0'\n )}\n >\n <circle\n ref={ringRef}\n cx=\"22\"\n cy=\"22\"\n r=\"21\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.5\"\n pathLength={1}\n strokeDasharray=\"1\"\n strokeDashoffset=\"1\"\n className=\"text-[#c6f56f]\"\n />\n </svg>\n </button>\n )}\n </div>\n\n {/* Quiet while autoplaying, so a screen reader is not interrupted every few seconds */}\n <p aria-live={playing && !held ? 'off' : 'polite'} className=\"sr-only\">\n {active ? `${current + 1} of ${count}: ${active.title}` : ''}\n </p>\n\n <div aria-hidden=\"true\" className={cn(FADE, 'left-0 bg-linear-to-r')} />\n <div aria-hidden=\"true\" className={cn(FADE, 'right-0 bg-linear-to-l')} />\n </section>\n );\n};\n\nexport default ArcCarousel;\n"
}
+ },
+ "navbar-gooey-bloom": {
+ "react": {
+ "code": "'use client';\n\nimport {\n useCallback,\n useEffect,\n useId,\n useRef,\n useState,\n type ComponentPropsWithoutRef,\n type MouseEventHandler,\n type ReactNode,\n} from 'react';\nimport { useGSAP } from '@gsap/react';\nimport gsap from 'gsap';\nimport { cn } from '@/lib/utils';\nimport DotBloomButton from './dot-bloom-button';\n\ngsap.registerPlugin(useGSAP);\n\nconst prefersReducedMotion = () =>\n typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\nexport type NavbarGooeyBloomIcon =\n 'bloom' | 'drop' | 'orbit' | 'spark' | 'layers' | 'wave' | 'bolt' | 'shield';\n\nexport interface NavbarGooeyBloomLink {\n label: string;\n href: string;\n description?: string;\n icon?: NavbarGooeyBloomIcon;\n}\n\nexport interface NavbarGooeyBloomFeature {\n title: string;\n description: string;\n href: string;\n cta?: string;\n image?: string;\n}\n\nexport interface NavbarGooeyBloomMenu {\n label: string;\n href?: string;\n columns?: NavbarGooeyBloomLink[][];\n feature?: NavbarGooeyBloomFeature;\n}\n\nexport interface NavbarGooeyBloomProps extends Omit<\n ComponentPropsWithoutRef<'section'>,\n 'children'\n> {\n brand?: string;\n logo?: ReactNode;\n homeHref?: string;\n menus?: NavbarGooeyBloomMenu[];\n currentHref?: string;\n actionLabel?: string;\n onActionClick?: MouseEventHandler\n \n Metaball navigation\n
\n\n Point at a link. A pool of lime stretches across the bar, sheds a few droplets on the way,\n and settles under the label. Open a menu and the card below re-forms around it.\n
\n{menu.feature.title}
\n\n {menu.feature.description}\n
\n closeMenu()}\n className={cn(\n 'group/feature mt-auto inline-flex items-center gap-1.5 pt-3 text-[12px] font-medium text-[#c6f56f]',\n focusRing\n )}\n >\n {menu.feature.cta ?? 'Explore'}\n'use client';\n\nimport {\n useCallback,\n useEffect,\n useId,\n useRef,\n useState,\n type ComponentPropsWithoutRef,\n type MouseEventHandler,\n type ReactNode,\n} from 'react';\nimport { useGSAP } from '@gsap/react';\nimport gsap from 'gsap';\nimport { cn } from '@/lib/utils';\nimport DotBloomButton from './dot-bloom-button';\n\ngsap.registerPlugin(useGSAP);\n\nconst prefersReducedMotion = () =>\n typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\nexport type NavbarGooeyBloomIcon =\n 'bloom' | 'drop' | 'orbit' | 'spark' | 'layers' | 'wave' | 'bolt' | 'shield';\n\nexport interface NavbarGooeyBloomLink {\n label: string;\n href: string;\n description?: string;\n icon?: NavbarGooeyBloomIcon;\n}\n\nexport interface NavbarGooeyBloomFeature {\n title: string;\n description: string;\n href: string;\n cta?: string;\n image?: string;\n}\n\nexport interface NavbarGooeyBloomMenu {\n label: string;\n href?: string;\n columns?: NavbarGooeyBloomLink[][];\n feature?: NavbarGooeyBloomFeature;\n}\n\nexport interface NavbarGooeyBloomProps extends Omit<\n ComponentPropsWithoutRef<'section'>,\n 'children'\n> {\n brand?: string;\n logo?: ReactNode;\n homeHref?: string;\n menus?: NavbarGooeyBloomMenu[];\n currentHref?: string;\n actionLabel?: string;\n onActionClick?: MouseEventHandler<HTMLButtonElement>;\n children?: ReactNode;\n}\n\nconst CLOSE_DELAY = 160;\nconst HOVER_INTENT = 70;\nconst HIDE_AFTER = 220;\nconst PANEL_GAP = 12;\nconst PANEL_RADIUS = 26;\nconst DROP_SIZE = 22;\nconst EDGE_LEAD = 520;\nconst EDGE_TRAIL = 170;\nconst TAIL_PULL = 110;\nconst PANEL_LEAD = 300;\nconst PANEL_TRAIL = 140;\nconst PANEL_PULL = 260;\nconst BUMP_RADIUS = 14;\nconst BUMP_RISE = 6;\nconst BUMP_PULL = 150;\nconst ICON_SIZE = 32;\nconst ICON_DROP = 14;\nconst SCROLL_TOLERANCE = 6;\nconst DESKTOP_QUERY = '(min-width: 1024px)';\n\nconst CONTAINER = 'mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8';\nconst EASE_OUT_EXPO = 'ease-[cubic-bezier(0.16,1,0.3,1)]';\nconst focusRing =\n 'focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[#c6f56f]';\n\nconst TWEEN_MARK = (\n <svg aria-hidden=\"true\" viewBox=\"0 0 24 24\" className=\"size-7 shrink-0\">\n <rect width=\"24\" height=\"24\" rx=\"6\" fill=\"#045f64\" />\n <path\n d=\"M5 19 C 9.5 8.9, 5 5, 19 5\"\n fill=\"none\"\n stroke=\"#c6f56f\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n />\n <circle cx=\"19\" cy=\"5\" r=\"1.8\" fill=\"#c6f56f\" />\n </svg>\n);\n\nconst ICON_PATHS: Record<NavbarGooeyBloomIcon, ReactNode> = {\n bloom: (\n <>\n <circle cx=\"12\" cy=\"9\" r=\"3.6\" />\n <circle cx=\"8.6\" cy=\"15\" r=\"3.6\" />\n <circle cx=\"15.4\" cy=\"15\" r=\"3.6\" />\n </>\n ),\n drop: <path d=\"M12 3.5c3.2 3.4 5.5 6 5.5 9a5.5 5.5 0 1 1-11 0c0-3 2.3-5.6 5.5-9Z\" />,\n orbit: (\n <>\n <circle cx=\"12\" cy=\"12\" r=\"3.1\" />\n <ellipse cx=\"12\" cy=\"12\" rx=\"9\" ry=\"4.4\" transform=\"rotate(-24 12 12)\" />\n </>\n ),\n spark: <path d=\"M12 3.5l2 6.5 6.5 2-6.5 2-2 6.5-2-6.5-6.5-2 6.5-2 2-6.5Z\" />,\n layers: (\n <>\n <path d=\"M12 4 4 8.5l8 4.5 8-4.5L12 4Z\" />\n <path d=\"m4 12.5 8 4.5 8-4.5\" />\n </>\n ),\n wave: <path d=\"M3 13.5c2-3.6 4-3.6 6 0s4 3.6 6 0 4-3.6 6 0\" />,\n bolt: <path d=\"M13.5 3 6 13.5h5l-1 7.5 8-11h-5l.5-7Z\" />,\n shield: <path d=\"M12 3.5 19 6v6c0 4.2-3 7-7 8.5-4-1.5-7-4.3-7-8.5V6l7-2.5Z\" />,\n};\n\nconst DEFAULT_MENUS: NavbarGooeyBloomMenu[] = [\n {\n label: 'Work',\n columns: [\n [\n {\n label: 'Case studies',\n href: '#',\n description: 'Shipped products, start to finish.',\n icon: 'bloom',\n },\n {\n label: 'Experiments',\n href: '#',\n description: 'Small ideas, pushed too far.',\n icon: 'spark',\n },\n {\n label: 'Playground',\n href: '#',\n description: 'Live code you can poke at.',\n icon: 'orbit',\n },\n {\n label: 'Archive',\n href: '#',\n description: 'Everything since 2019.',\n icon: 'layers',\n },\n ],\n [\n {\n label: 'Motion kit',\n href: '#',\n description: 'Easing, springs and timing.',\n icon: 'wave',\n },\n {\n label: 'Interface kit',\n href: '#',\n description: 'Bars, panels and overlays.',\n icon: 'drop',\n },\n {\n label: 'Brand systems',\n href: '#',\n description: 'Marks that know how to move.',\n icon: 'shield',\n },\n {\n label: 'Performance',\n href: '#',\n description: 'Sixty frames, no excuses.',\n icon: 'bolt',\n },\n ],\n ],\n feature: {\n title: 'Bloom 2.0',\n description: 'The metaball engine under this bar, documented end to end.',\n href: '#',\n cta: 'Read the notes',\n },\n },\n {\n label: 'Components',\n columns: [\n [\n {\n label: 'Navbars',\n href: '/components',\n description: 'Eleven ways to hold a menu.',\n icon: 'layers',\n },\n {\n label: 'Buttons',\n href: '/components',\n description: 'Press, roll, bloom.',\n icon: 'drop',\n },\n {\n label: 'Carousels',\n href: '/components',\n description: 'Arcs, shutters and reels.',\n icon: 'orbit',\n },\n {\n label: 'Loaders',\n href: '/components',\n description: 'Waiting, but on purpose.',\n icon: 'spark',\n },\n {\n label: 'Type',\n href: '/components',\n description: 'Letters that misbehave.',\n icon: 'wave',\n },\n ],\n ],\n },\n {\n label: 'Studio',\n columns: [\n [\n { label: 'About', href: '#', description: 'Four people, one room.', icon: 'bloom' },\n { label: 'Team', href: '#', description: 'Who makes what.', icon: 'shield' },\n { label: 'Careers', href: '#', description: 'Two seats open.', icon: 'bolt' },\n { label: 'Press', href: '#', description: 'Logos and words.', icon: 'layers' },\n ],\n [\n { label: 'Contact', href: '#', description: 'Say hello properly.', icon: 'drop' },\n {\n label: 'Changelog',\n href: '#',\n description: 'What moved this week.',\n icon: 'wave',\n },\n { label: 'License', href: '#', description: 'What you may ship.', icon: 'orbit' },\n {\n label: 'Support',\n href: '#',\n description: 'We answer on weekdays.',\n icon: 'spark',\n },\n ],\n ],\n },\n { label: 'Journal', href: '#' },\n];\n\nconst cleanHref = (href: string) => href.split(/[?#]/)[0]?.replace(/\\/+$/, '') || '/';\n\nconst isExternal = (href: string) => /^https?:/.test(href);\n\nconst externalProps = (href: string) =>\n isExternal(href) ? { target: '_blank', rel: 'noopener noreferrer' } : {};\n\nconst clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max);\n\nconst panelWidth = (menu: NavbarGooeyBloomMenu) => {\n const columns = menu.columns?.length ?? 0;\n if (menu.feature) return columns > 1 ? 880 : 720;\n return columns > 1 ? 720 : 360;\n};\n\nconst panelGrid = (menu: NavbarGooeyBloomMenu) => {\n const columns = menu.columns?.length ?? 0;\n if (menu.feature) return columns > 1 ? 'grid-cols-[1fr_1fr_15rem]' : 'grid-cols-[1fr_15rem]';\n return columns > 1 ? 'grid-cols-2' : 'grid-cols-1';\n};\n\ninterface Spring {\n x: number;\n v: number;\n t: number;\n}\n\ninterface GooMotion {\n left: Spring;\n right: Spring;\n tail: Spring;\n panelLeft: Spring;\n panelRight: Spring;\n panelHeight: Spring;\n bump: Spring;\n rowTop: Spring;\n rowBottom: Spring;\n rowLeft: Spring;\n rowWidth: Spring;\n iconTop: Spring;\n iconBottom: Spring;\n iconTail: Spring;\n iconOffset: number;\n look: { scale: number; alpha: number };\n hover: { alpha: number };\n pillShown: boolean;\n panelShown: boolean;\n hoverShown: boolean;\n barBottom: number;\n awake: boolean;\n}\n\nconst spring = (): Spring => ({ x: 0, v: 0, t: 0 });\n\nconst place = (target: Spring, value: number) => {\n target.x = value;\n target.v = 0;\n target.t = value;\n};\n\nconst snap = (target: Spring) => {\n target.x = target.t;\n target.v = 0;\n};\n\nconst stepSpring = (target: Spring, stiffness: number, dt: number) => {\n const force = stiffness * (target.t - target.x) - 2 * Math.sqrt(stiffness) * target.v;\n target.v += force * dt;\n target.x += target.v * dt;\n};\n\nconst atRest = (target: Spring, epsilon: number) =>\n Math.abs(target.t - target.x) < epsilon && Math.abs(target.v) < epsilon;\n\nconst createMotion = (): GooMotion => ({\n left: spring(),\n right: spring(),\n tail: spring(),\n panelLeft: spring(),\n panelRight: spring(),\n panelHeight: spring(),\n bump: spring(),\n rowTop: spring(),\n rowBottom: spring(),\n rowLeft: spring(),\n rowWidth: spring(),\n iconTop: spring(),\n iconBottom: spring(),\n iconTail: spring(),\n iconOffset: 0,\n look: { scale: 0.4, alpha: 0 },\n hover: { alpha: 0 },\n pillShown: false,\n panelShown: false,\n hoverShown: false,\n barBottom: 60,\n awake: false,\n});\n\nconst springsOf = (motion: GooMotion) => [\n motion.left,\n motion.right,\n motion.tail,\n motion.panelLeft,\n motion.panelRight,\n motion.panelHeight,\n motion.bump,\n motion.rowTop,\n motion.rowBottom,\n motion.rowLeft,\n motion.rowWidth,\n motion.iconTop,\n motion.iconBottom,\n motion.iconTail,\n];\n\nconst settleMotion = (motion: GooMotion) => {\n const center = (motion.left.t + motion.right.t) / 2;\n motion.tail.t = center;\n motion.bump.t = center;\n motion.iconTail.t = (motion.iconTop.t + motion.iconBottom.t) / 2;\n springsOf(motion).forEach(snap);\n};\n\nconst motionAtRest = (motion: GooMotion) =>\n springsOf(motion).every((item) => atRest(item, 0.05)) &&\n !gsap.isTweening(motion.look) &&\n !gsap.isTweening(motion.hover);\n\nconst steer = (start: Spring, end: Spring) => {\n const heading = start.t + end.t - start.x - end.x;\n return {\n start: heading > 1 ? EDGE_TRAIL : EDGE_LEAD,\n end: heading < -1 ? EDGE_TRAIL : EDGE_LEAD,\n };\n};\n\nconst px = (value: number) => Math.round(value * 100) / 100;\n\nconst roundedRect = (left: number, right: number, top: number, height: number) => {\n const radius = px(Math.max(0, Math.min(PANEL_RADIUS, (right - left) / 2, height / 2)));\n const bottom = px(top + height);\n const l = px(left);\n const r = px(right);\n return `M${px(l + radius)},${top}H${px(r - radius)}A${radius},${radius} 0 0 1 ${r},${px(top + radius)}V${px(bottom - radius)}A${radius},${radius} 0 0 1 ${px(r - radius)},${bottom}H${px(l + radius)}A${radius},${radius} 0 0 1 ${l},${px(bottom - radius)}V${px(top + radius)}A${radius},${radius} 0 0 1 ${px(l + radius)},${top}Z`;\n};\n\nconst ArrowIcon = ({ className }: { className?: string }) => {\n return (\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.8\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"M5 12h13M13 6l6 6-6 6\" />\n </svg>\n );\n};\n\nconst ChevronIcon = ({ className }: { className?: string }) => {\n return (\n <svg aria-hidden=\"true\" viewBox=\"0 0 10 6\" fill=\"none\" className={className}>\n <path\n d=\"M1 1l4 4 4-4\"\n stroke=\"currentColor\"\n strokeWidth=\"1.4\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n );\n};\n\nconst MenuGlyph = ({ icon, className }: { icon: NavbarGooeyBloomIcon; className?: string }) => {\n return (\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n {ICON_PATHS[icon]}\n </svg>\n );\n};\n\nconst FeatureArt = () => {\n const rawId = useId().replace(/[^a-zA-Z0-9_-]/g, '');\n const gooId = `feature-goo-${rawId}`;\n const limeId = `feature-lime-${rawId}`;\n\n return (\n <span aria-hidden=\"true\" className=\"absolute inset-0 overflow-hidden bg-[#081f1a]\">\n <span className=\"absolute inset-0 bg-[radial-gradient(120%_130%_at_50%_-10%,#17493c,#081f1a_70%)]\" />\n <span className=\"absolute inset-0 [background-image:radial-gradient(rgb(159_212_214/0.16)_1px,transparent_1px)] [mask-image:radial-gradient(72%_72%_at_50%_50%,#000,transparent)] [background-size:10px_10px] opacity-60\" />\n <span className=\"absolute top-1/2 left-1/2 size-32 -translate-x-1/2 -translate-y-1/2 rounded-full bg-[#c6f56f]/15 blur-2xl\" />\n <svg viewBox=\"0 0 240 112\" className=\"absolute inset-0 size-full\">\n <defs>\n <filter id={gooId} x=\"-25%\" y=\"-80%\" width=\"150%\" height=\"260%\">\n <feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"3\" result=\"blur\" />\n <feColorMatrix\n in=\"blur\"\n mode=\"matrix\"\n values=\"1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 18 -7\"\n result=\"goo\"\n />\n <feBlend in=\"SourceGraphic\" in2=\"goo\" />\n </filter>\n <linearGradient id={limeId} x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"0\" stopColor=\"#e9ffc2\" />\n <stop offset=\"0.45\" stopColor=\"#c6f56f\" />\n <stop offset=\"1\" stopColor=\"#a6df58\" />\n </linearGradient>\n </defs>\n <g stroke=\"#9fd4d6\" fill=\"none\">\n <ellipse cx=\"120\" cy=\"56\" rx=\"88\" ry=\"30\" strokeOpacity=\"0.14\" />\n <ellipse cx=\"120\" cy=\"56\" rx=\"62\" ry=\"42\" strokeOpacity=\"0.07\" />\n </g>\n <g filter={`url(#${gooId})`} fill={`url(#${limeId})`}>\n <rect x=\"74\" y=\"42\" width=\"84\" height=\"30\" rx=\"15\" />\n <circle cx=\"66\" cy=\"60\" r=\"8.5\" />\n <circle cx=\"162\" cy=\"52\" r=\"7\" />\n <circle cx=\"168\" cy=\"63\" r=\"4.5\" />\n </g>\n </svg>\n </span>\n );\n};\n\nconst SamplePage = () => {\n return (\n <div id=\"top\" className=\"[container-type:size] relative h-full overflow-hidden bg-[#03110f]\">\n <span\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute inset-0 bg-[radial-gradient(46%_60%_at_50%_-10%,rgb(198_245_111/0.12),transparent_70%),radial-gradient(50%_60%_at_90%_20%,rgb(4_95_100/0.55),transparent_72%),radial-gradient(45%_55%_at_4%_60%,rgb(4_95_100/0.3),transparent_70%)]\"\n />\n <span\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute inset-0 bg-[repeating-linear-gradient(90deg,rgb(159_212_214/0.05)_0_1px,transparent_1px_56px),repeating-linear-gradient(0deg,rgb(159_212_214/0.04)_0_1px,transparent_1px_56px)] [mask-image:radial-gradient(ellipse_70%_60%_at_50%_30%,black,transparent)]\"\n />\n\n <section className=\"relative mx-auto flex h-full max-w-7xl flex-col justify-center px-4 pt-[clamp(5rem,16cqh,8rem)] pb-[clamp(1.5rem,5cqh,3.5rem)] sm:px-6 lg:px-8\">\n <p className=\"inline-flex w-fit items-center gap-2.5 rounded-full bg-white/[0.04] py-[clamp(0.4rem,1.1cqh,0.55rem)] pr-4 pl-2.5 text-[10px] leading-none font-medium tracking-[0.22em] text-[#9fd4d6] uppercase ring-1 ring-white/10\">\n <span\n aria-hidden=\"true\"\n className=\"size-2 rounded-full bg-[#c6f56f] shadow-[0_0_6px_#c6f56f,0_0_16px_#c6f56f]\"\n />\n Metaball navigation\n </p>\n <h2 className=\"mt-[clamp(1.1rem,3.4cqh,1.75rem)] max-w-[36rem] text-[clamp(2.35rem,7.8cqh,4.75rem)] leading-[0.95] font-medium tracking-[-0.045em] text-balance text-[#e8f3f2]\">\n The menu that <span className=\"text-[#c6f56f]\">melts</span> between links.\n </h2>\n <p className=\"mt-[clamp(0.9rem,2.4cqh,1.5rem)] max-w-[36rem] text-[clamp(0.84rem,1.9cqh,1rem)] leading-relaxed text-pretty text-[#9fd4d6]/70\">\n Point at a link. A pool of lime stretches across the bar, sheds a few droplets on the way,\n and settles under the label. Open a menu and the card below re-forms around it.\n </p>\n <div className=\"mt-[clamp(1.4rem,4cqh,2.25rem)] flex flex-wrap items-center gap-3\">\n <a\n href=\"#top\"\n className={cn(\n 'inline-flex h-[clamp(2.5rem,6.2cqh,2.75rem)] items-center gap-2 rounded-full bg-[#c6f56f] px-[clamp(1rem,2.4cqh,1.25rem)] text-[clamp(0.82rem,1.8cqh,0.875rem)] font-medium text-[#03110f] transition-transform duration-300 active:scale-[0.97]',\n EASE_OUT_EXPO,\n focusRing\n )}\n >\n See it again\n <ArrowIcon className=\"size-4\" />\n </a>\n <a\n href=\"#top\"\n className={cn(\n 'inline-flex h-[clamp(2.5rem,6.2cqh,2.75rem)] items-center rounded-full border border-[#9fd4d6]/20 px-[clamp(1rem,2.4cqh,1.25rem)] text-[clamp(0.82rem,1.8cqh,0.875rem)] text-[#e8f3f2]/80 transition-colors duration-300 hover:border-[#9fd4d6]/45 hover:text-[#e8f3f2]',\n focusRing\n )}\n >\n Read the props\n </a>\n </div>\n </section>\n </div>\n );\n};\n\nconst NavbarGooeyBloom = ({\n brand = 'Tween UI',\n logo = TWEEN_MARK,\n homeHref = '/',\n menus = DEFAULT_MENUS,\n currentHref = '/',\n actionLabel = 'Get started',\n onActionClick,\n children,\n className,\n ...props\n}: NavbarGooeyBloomProps) => {\n const [openKey, setOpenKey] = useState<string | null>(null);\n const [mobileOpen, setMobileOpen] = useState(false);\n const [accordion, setAccordion] = useState<string | null>(null);\n\n const rawId = useId().replace(/[^a-zA-Z0-9_-]/g, '');\n const gooId = `goo-${rawId}`;\n const linkGooId = `link-goo-${rawId}`;\n const panelGooId = `panel-goo-${rawId}`;\n const panelId = `panel-${rawId}`;\n const mobileId = `mobile-${rawId}`;\n\n const rootRef = useRef<HTMLElement>(null);\n const barRef = useRef<HTMLDivElement>(null);\n const shellRef = useRef<HTMLDivElement>(null);\n const scrollerRef = useRef<HTMLDivElement>(null);\n const panelRef = useRef<HTMLDivElement>(null);\n const bodyRef = useRef<HTMLDivElement>(null);\n const shapeRef = useRef<SVGPathElement>(null);\n const bumpRef = useRef<SVGCircleElement>(null);\n const hoverRef = useRef<{ row: HTMLElement; blob: HTMLElement; drop: HTMLElement } | null>(null);\n const pillRef = useRef<HTMLSpanElement>(null);\n const coreRef = useRef<HTMLSpanElement>(null);\n const dropRef = useRef<HTMLSpanElement>(null);\n const inkRefs = useRef<Array<HTMLSpanElement | null>>([]);\n const inkBoxes = useRef<Array<{ left: number; width: number } | null>>([]);\n const motion = useRef<GooMotion>(createMotion());\n const activeRef = useRef<HTMLElement | null>(null);\n const sectionRefs = useRef<Record<string, HTMLDivElement | null>>({});\n const triggerRefs = useRef<Record<string, HTMLButtonElement | null>>({});\n const closeTimer = useRef<number | null>(null);\n const hoverTimer = useRef<number | null>(null);\n const pendingHover = useRef<{ menu: NavbarGooeyBloomMenu; el: HTMLElement } | null>(null);\n const pointerPos = useRef({ x: 0, y: 0 });\n const mobileRef = useRef<HTMLDivElement>(null);\n const toggleRef = useRef<HTMLButtonElement>(null);\n const mobileCloseRef = useRef<HTMLButtonElement>(null);\n const openRef = useRef<string | null>(null);\n const skipFocusOpen = useRef(false);\n\n const current = (href: string) => {\n if (href.startsWith('#')) return false;\n const link = cleanHref(href);\n const page = cleanHref(currentHref);\n return link === page || (link !== '/' && page.startsWith(`${link}/`));\n };\n\n const { contextSafe } = useGSAP(\n () => {\n if (prefersReducedMotion()) return;\n const bar = barRef.current;\n if (!bar) return;\n const intro = bar.querySelectorAll('[data-intro]');\n gsap.fromTo(\n bar,\n { y: -28, autoAlpha: 0 },\n {\n y: 0,\n autoAlpha: 1,\n duration: 1,\n ease: 'expo.out',\n clearProps: 'transform,opacity,visibility',\n }\n );\n gsap.fromTo(\n intro,\n { y: -8, autoAlpha: 0 },\n {\n y: 0,\n autoAlpha: 1,\n duration: 0.7,\n ease: 'power3.out',\n stagger: 0.06,\n delay: 0.12,\n clearProps: 'transform,opacity,visibility',\n }\n );\n },\n { scope: rootRef }\n );\n\n const measureInk = useCallback(() => {\n const bar = barRef.current;\n if (!bar) return;\n const barRect = bar.getBoundingClientRect();\n inkRefs.current.forEach((ink, index) => {\n const host = ink?.parentElement;\n if (!host) return;\n const box = host.getBoundingClientRect();\n inkBoxes.current[index] = { left: box.left - barRect.left, width: box.width };\n });\n }, []);\n\n const draw = useCallback(() => {\n const m = motion.current;\n const left = m.left.x;\n const right = m.right.x;\n const width = Math.max(right - left, 0);\n const stretch = width - (m.right.t - m.left.t);\n const thin = 1 - clamp(stretch / 320, 0, 0.18);\n const { scale, alpha } = m.look;\n const pill = pillRef.current;\n const core = coreRef.current;\n const drop = dropRef.current;\n\n if (pill && core && drop) {\n pill.style.width = `${width}px`;\n pill.style.transform = `translate3d(${left}px,0,0)`;\n pill.style.opacity = `${alpha}`;\n core.style.transform = `scale(${scale},${scale * thin})`;\n drop.style.transform = `translate3d(${m.tail.x - DROP_SIZE / 2}px,0,0) scale(${scale})`;\n drop.style.opacity = `${alpha}`;\n }\n\n inkRefs.current.forEach((ink, index) => {\n const box = inkBoxes.current[index];\n if (!ink || !box) return;\n const start = clamp(left - box.left, 0, box.width);\n const end = clamp(right - box.left, 0, box.width);\n ink.style.clipPath = `inset(-4px ${box.width - end}px -4px ${start}px)`;\n ink.style.opacity = `${alpha}`;\n });\n\n const hover = hoverRef.current;\n if (hover) {\n const alpha = m.hover.alpha;\n const iconHeight = Math.max(m.iconBottom.x - m.iconTop.x, 0);\n const iconStretch = iconHeight - (m.iconBottom.t - m.iconTop.t);\n const iconWidth = ICON_SIZE * (1 - clamp(iconStretch / 160, 0, 0.3));\n const iconX = m.rowLeft.x + m.iconOffset;\n hover.row.style.transform = `translate3d(${m.rowLeft.x}px,${m.rowTop.x}px,0)`;\n hover.row.style.width = `${Math.max(m.rowWidth.x, 0)}px`;\n hover.row.style.height = `${Math.max(m.rowBottom.x - m.rowTop.x, 0)}px`;\n hover.row.style.opacity = `${alpha}`;\n hover.blob.style.transform = `translate3d(${iconX + (ICON_SIZE - iconWidth) / 2}px,${m.iconTop.x}px,0)`;\n hover.blob.style.width = `${iconWidth}px`;\n hover.blob.style.height = `${iconHeight}px`;\n hover.blob.style.opacity = `${alpha}`;\n hover.drop.style.transform = `translate3d(${iconX + (ICON_SIZE - ICON_DROP) / 2}px,${m.iconTail.x - ICON_DROP / 2}px,0)`;\n hover.drop.style.opacity = `${alpha}`;\n }\n\n const shape = shapeRef.current;\n const bump = bumpRef.current;\n const body = bodyRef.current;\n if (!m.panelShown || !shape || !bump || !body) return;\n const top = m.barBottom + PANEL_GAP;\n const height = Math.max(m.panelHeight.x, 0);\n const panelWidth = Math.max(m.panelRight.x - m.panelLeft.x, 0);\n const bumpRadius = Math.max(0, Math.min(BUMP_RADIUS, panelWidth / 4, height / 3));\n shape.setAttribute('d', roundedRect(m.panelLeft.x, m.panelRight.x, top, height));\n const edge = Math.min(PANEL_RADIUS, panelWidth / 2) + bumpRadius;\n const bumpX =\n panelWidth > edge * 2\n ? clamp(m.bump.x, m.panelLeft.x + edge, m.panelRight.x - edge)\n : m.panelLeft.x + panelWidth / 2;\n bump.setAttribute('cx', `${px(bumpX)}`);\n bump.setAttribute('cy', `${px(top + bumpRadius - BUMP_RISE * (bumpRadius / BUMP_RADIUS))}`);\n bump.setAttribute('r', `${px(bumpRadius)}`);\n body.style.top = `${top}px`;\n body.style.clipPath = `path('${roundedRect(m.panelLeft.x, m.panelRight.x, 0, height)}')`;\n }, []);\n\n useEffect(() => {\n const tick = (_time: number, deltaTime: number) => {\n const m = motion.current;\n if (!m.awake) return;\n const elapsed = Math.min(deltaTime / 1000, 0.05);\n const steps = Math.max(1, Math.ceil(elapsed * 120));\n const dt = elapsed / steps;\n const pill = steer(m.left, m.right);\n const row = steer(m.rowTop, m.rowBottom);\n const icon = steer(m.iconTop, m.iconBottom);\n const panelHeading = m.panelLeft.t + m.panelRight.t - m.panelLeft.x - m.panelRight.x;\n const panelLeftPull = panelHeading > 1 ? PANEL_TRAIL : PANEL_LEAD;\n const panelRightPull = panelHeading < -1 ? PANEL_TRAIL : PANEL_LEAD;\n\n for (let step = 0; step < steps; step += 1) {\n stepSpring(m.left, pill.start, dt);\n stepSpring(m.right, pill.end, dt);\n const center = (m.left.x + m.right.x) / 2;\n m.tail.t = center;\n stepSpring(m.tail, TAIL_PULL, dt);\n if (m.panelShown) {\n m.bump.t = center;\n stepSpring(m.panelLeft, panelLeftPull, dt);\n stepSpring(m.panelRight, panelRightPull, dt);\n stepSpring(m.panelHeight, PANEL_PULL, dt);\n stepSpring(m.bump, BUMP_PULL, dt);\n }\n stepSpring(m.rowTop, row.start, dt);\n stepSpring(m.rowBottom, row.end, dt);\n stepSpring(m.rowLeft, EDGE_LEAD, dt);\n stepSpring(m.rowWidth, EDGE_LEAD, dt);\n stepSpring(m.iconTop, icon.start, dt);\n stepSpring(m.iconBottom, icon.end, dt);\n m.iconTail.t = (m.iconTop.x + m.iconBottom.x) / 2;\n stepSpring(m.iconTail, TAIL_PULL, dt);\n }\n\n const center = (m.left.x + m.right.x) / 2;\n m.tail.x = m.right.x - m.left.x > 4 ? clamp(m.tail.x, m.left.x + 2, m.right.x - 2) : center;\n m.iconTail.x = clamp(m.iconTail.x, m.iconTop.x - 2, m.iconBottom.x + 2);\n\n if (motionAtRest(m)) {\n settleMotion(m);\n m.awake = false;\n }\n draw();\n };\n\n gsap.ticker.add(tick);\n return () => gsap.ticker.remove(tick);\n }, [draw]);\n\n const hideBlob = contextSafe(() => {\n const m = motion.current;\n if (!m.pillShown) return;\n m.pillShown = false;\n const center = (m.left.t + m.right.t) / 2;\n m.left.t = center - 20;\n m.right.t = center + 20;\n gsap.killTweensOf(m.look);\n if (prefersReducedMotion()) {\n m.look.scale = 0.4;\n m.look.alpha = 0;\n settleMotion(m);\n draw();\n return;\n }\n gsap.to(m.look, { scale: 0.4, alpha: 0, duration: 0.32, ease: 'power2.inOut' });\n m.awake = true;\n });\n\n const moveBlob = contextSafe((el: HTMLElement) => {\n const bar = barRef.current;\n if (!bar) return;\n const m = motion.current;\n const base = bar.getBoundingClientRect();\n const box = el.getBoundingClientRect();\n const left = box.left - base.left;\n const center = left + box.width / 2;\n activeRef.current = el;\n measureInk();\n gsap.killTweensOf(m.look);\n\n if (!m.pillShown && m.look.alpha < 0.05) {\n place(m.left, center - 20);\n place(m.right, center + 20);\n place(m.tail, center);\n m.look.scale = 0.4;\n }\n m.pillShown = true;\n m.left.t = left;\n m.right.t = left + box.width;\n\n if (prefersReducedMotion()) {\n m.look.scale = 1;\n m.look.alpha = 1;\n settleMotion(m);\n draw();\n return;\n }\n gsap.to(m.look, { scale: 1, alpha: 1, duration: 0.45, ease: 'power3.out' });\n m.awake = true;\n });\n\n const resetHover = () => {\n const m = motion.current;\n const hover = hoverRef.current;\n gsap.killTweensOf(m.hover);\n m.hover.alpha = 0;\n m.hoverShown = false;\n hoverRef.current = null;\n if (hover) gsap.set([hover.row, hover.blob, hover.drop], { opacity: 0 });\n };\n\n const moveHover = contextSafe((link: HTMLElement) => {\n const section = link.closest<HTMLElement>('[data-section]');\n const icon = link.querySelector<HTMLElement>('[data-icon]');\n const row = section?.querySelector<HTMLElement>('[data-hover-row]');\n const blob = section?.querySelector<HTMLElement>('[data-hover-blob]');\n const drop = section?.querySelector<HTMLElement>('[data-hover-drop]');\n if (!section || !icon || !row || !blob || !drop) return;\n\n const m = motion.current;\n if (hoverRef.current && hoverRef.current.row !== row) resetHover();\n const fresh = !hoverRef.current || (!m.hoverShown && m.hover.alpha < 0.05);\n hoverRef.current = { row, blob, drop };\n\n const base = section.getBoundingClientRect();\n const box = link.getBoundingClientRect();\n const tile = icon.getBoundingClientRect();\n const top = box.top - base.top;\n const left = box.left - base.left;\n const iconTop = tile.top - base.top;\n const iconCenter = iconTop + tile.height / 2;\n m.iconOffset = tile.left - box.left;\n gsap.killTweensOf(m.hover);\n\n if (fresh) {\n place(m.rowTop, top);\n place(m.rowBottom, top + box.height);\n place(m.rowLeft, left);\n place(m.rowWidth, box.width);\n place(m.iconTop, iconCenter - 4);\n place(m.iconBottom, iconCenter + 4);\n place(m.iconTail, iconCenter);\n }\n m.hoverShown = true;\n m.rowTop.t = top;\n m.rowBottom.t = top + box.height;\n m.rowLeft.t = left;\n m.rowWidth.t = box.width;\n m.iconTop.t = iconTop;\n m.iconBottom.t = iconTop + tile.height;\n\n if (prefersReducedMotion()) {\n m.hover.alpha = 1;\n settleMotion(m);\n draw();\n return;\n }\n gsap.to(m.hover, { alpha: 1, duration: 0.3, ease: 'power2.out' });\n m.awake = true;\n });\n\n const hideHover = contextSafe(() => {\n const m = motion.current;\n if (!m.hoverShown) return;\n m.hoverShown = false;\n const center = (m.iconTop.t + m.iconBottom.t) / 2;\n m.iconTop.t = center - 4;\n m.iconBottom.t = center + 4;\n gsap.killTweensOf(m.hover);\n if (prefersReducedMotion()) {\n m.hover.alpha = 0;\n settleMotion(m);\n draw();\n return;\n }\n gsap.to(m.hover, { alpha: 0, duration: 0.28, ease: 'power2.out' });\n m.awake = true;\n });\n\n const getLayout = useCallback((trigger: HTMLElement, section: HTMLElement) => {\n const bar = barRef.current;\n const width = section.offsetWidth;\n const height = section.offsetHeight;\n if (!bar) return { left: 0, width, height, center: width / 2 };\n const barRect = bar.getBoundingClientRect();\n const triggerRect = trigger.getBoundingClientRect();\n const center = triggerRect.left - barRect.left + triggerRect.width / 2;\n const min = 8 - barRect.left;\n const max = window.innerWidth - 8 - width - barRect.left;\n return { left: clamp(center - width / 2, Math.min(min, max), max), width, height, center };\n }, []);\n\n const sectionNodes = () =>\n Object.values(sectionRefs.current).filter((node): node is HTMLDivElement => Boolean(node));\n\n const openMenu = contextSafe((menu: NavbarGooeyBloomMenu, trigger: HTMLElement) => {\n const section = sectionRefs.current[menu.label];\n const panel = panelRef.current;\n const shell = shellRef.current;\n if (!section || !panel || !shell) return;\n if (openRef.current === menu.label) return;\n\n const previous = openRef.current;\n const previousSection = previous ? sectionRefs.current[previous] : null;\n openRef.current = menu.label;\n setOpenKey(menu.label);\n\n const m = motion.current;\n const layout = getLayout(trigger, section);\n const reduced = prefersReducedMotion();\n const items = section.querySelectorAll('[data-item]');\n m.barBottom = shell.offsetHeight;\n resetHover();\n\n gsap.killTweensOf([panel, ...sectionNodes()]);\n sectionNodes().forEach((node) => {\n if (node !== section && node !== previousSection) gsap.set(node, { opacity: 0 });\n });\n gsap.set(section, { left: layout.left, opacity: 1 });\n gsap.set(panel, { opacity: 1 });\n\n if (!m.panelShown) {\n place(m.panelLeft, layout.center - 12);\n place(m.panelRight, layout.center + 12);\n place(m.panelHeight, 24);\n place(m.bump, layout.center);\n m.panelShown = true;\n }\n m.panelLeft.t = layout.left;\n m.panelRight.t = layout.left + layout.width;\n m.panelHeight.t = layout.height;\n\n if (reduced) {\n if (previousSection && previousSection !== section) {\n gsap.set(previousSection, { opacity: 0 });\n }\n gsap.set(items, { y: 0, opacity: 1 });\n settleMotion(m);\n draw();\n return;\n }\n\n m.awake = true;\n if (previousSection && previousSection !== section) {\n gsap.to(previousSection, { opacity: 0, duration: 0.14, ease: 'power2.out' });\n }\n if (items.length) {\n gsap.fromTo(\n items,\n { y: 10, opacity: 0 },\n {\n y: 0,\n opacity: 1,\n duration: 0.42,\n ease: 'power3.out',\n stagger: 0.02,\n delay: previous ? 0.05 : 0.12,\n overwrite: 'auto',\n }\n );\n }\n });\n\n const closeMenu = contextSafe((options?: { clearHighlight?: boolean }) => {\n cancelHover();\n if (options?.clearHighlight) {\n hideBlob();\n }\n if (!openRef.current) return;\n openRef.current = null;\n setOpenKey(null);\n setAccordion(null);\n\n const panel = panelRef.current;\n if (!panel) return;\n const m = motion.current;\n const sections = sectionNodes();\n const center = (m.left.t + m.right.t) / 2;\n gsap.killTweensOf([panel, ...sections]);\n m.panelLeft.t = center - 12;\n m.panelRight.t = center + 12;\n m.panelHeight.t = 24;\n\n const finish = () => {\n m.panelShown = false;\n resetHover();\n gsap.set(sections, { opacity: 0 });\n gsap.set(panel, { opacity: 0 });\n };\n\n if (prefersReducedMotion()) {\n finish();\n return;\n }\n m.awake = true;\n gsap.to(sections, { opacity: 0, duration: 0.12, ease: 'power2.out' });\n gsap.to(panel, {\n opacity: 0,\n duration: 0.24,\n delay: 0.08,\n ease: 'power2.in',\n onComplete: finish,\n });\n });\n\n const cancelClose = () => {\n if (closeTimer.current !== null) {\n window.clearTimeout(closeTimer.current);\n closeTimer.current = null;\n }\n };\n\n const scheduleClose = contextSafe(() => {\n cancelHover();\n cancelClose();\n closeTimer.current = window.setTimeout(() => {\n const bar = barRef.current;\n if (bar) {\n const rect = bar.getBoundingClientRect();\n const { x, y } = pointerPos.current;\n if (\n x >= rect.left - 2 &&\n x <= rect.right + 2 &&\n y >= rect.top - 2 &&\n y <= rect.bottom + 2\n ) {\n return;\n }\n }\n closeMenu({ clearHighlight: true });\n }, CLOSE_DELAY);\n });\n\n const cancelHover = () => {\n if (hoverTimer.current !== null) {\n window.clearTimeout(hoverTimer.current);\n hoverTimer.current = null;\n }\n pendingHover.current = null;\n };\n\n const handleTriggerEnter = (menu: NavbarGooeyBloomMenu, el: HTMLElement) => {\n cancelClose();\n moveBlob(el);\n pendingHover.current = { menu, el };\n if (hoverTimer.current !== null) window.clearTimeout(hoverTimer.current);\n hoverTimer.current = window.setTimeout(() => {\n hoverTimer.current = null;\n const next = pendingHover.current;\n pendingHover.current = null;\n if (!next) return;\n if (next.menu.columns?.length) openMenu(next.menu, next.el);\n else closeMenu();\n }, HOVER_INTENT);\n };\n\n useEffect(() => {\n const scroller = scrollerRef.current;\n const bar = barRef.current;\n if (!scroller || !bar) return;\n let last = scroller.scrollTop;\n let hidden = false;\n\n const onScroll = () => {\n const y = scroller.scrollTop;\n const delta = y - last;\n if (Math.abs(delta) < SCROLL_TOLERANCE) return;\n last = y;\n const next = delta > 0 && y > HIDE_AFTER && !openRef.current;\n if (next === hidden) return;\n hidden = next;\n gsap.to(bar, {\n yPercent: next ? -170 : 0,\n duration: prefersReducedMotion() ? 0 : next ? 0.5 : 0.7,\n ease: next ? 'power3.in' : 'expo.out',\n overwrite: 'auto',\n });\n };\n\n scroller.addEventListener('scroll', onScroll, { passive: true });\n return () => scroller.removeEventListener('scroll', onScroll);\n }, []);\n\n useEffect(() => {\n const onResize = () => {\n const bar = barRef.current;\n const shell = shellRef.current;\n if (!bar || !shell) return;\n const m = motion.current;\n measureInk();\n const active = activeRef.current;\n if (active && m.pillShown) {\n const base = bar.getBoundingClientRect();\n const box = active.getBoundingClientRect();\n m.left.t = box.left - base.left;\n m.right.t = m.left.t + box.width;\n }\n const key = openRef.current;\n const section = key ? sectionRefs.current[key] : null;\n const trigger = key ? triggerRefs.current[key] : null;\n if (section && trigger) {\n const layout = getLayout(trigger, section);\n m.barBottom = shell.offsetHeight;\n m.panelLeft.t = layout.left;\n m.panelRight.t = layout.left + layout.width;\n m.panelHeight.t = layout.height;\n gsap.set(section, { left: layout.left });\n }\n settleMotion(m);\n draw();\n };\n\n window.addEventListener('resize', onResize);\n return () => window.removeEventListener('resize', onResize);\n }, [draw, getLayout, measureInk]);\n\n useEffect(() => {\n if (!openKey) return;\n const desktop = window.matchMedia(DESKTOP_QUERY);\n\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key !== 'Escape') return;\n const key = openRef.current;\n closeMenu({ clearHighlight: true });\n if (key) {\n skipFocusOpen.current = true;\n triggerRefs.current[key]?.focus();\n skipFocusOpen.current = false;\n }\n };\n\n const onPointerDown = (event: PointerEvent) => {\n if (!barRef.current?.contains(event.target as Node)) {\n closeMenu({ clearHighlight: true });\n }\n };\n\n const onDesktopChange = (event: MediaQueryListEvent) => {\n if (!event.matches) closeMenu({ clearHighlight: true });\n };\n\n document.addEventListener('keydown', onKeyDown);\n document.addEventListener('pointerdown', onPointerDown);\n desktop.addEventListener('change', onDesktopChange);\n return () => {\n document.removeEventListener('keydown', onKeyDown);\n document.removeEventListener('pointerdown', onPointerDown);\n desktop.removeEventListener('change', onDesktopChange);\n };\n }, [openKey, closeMenu]);\n\n useEffect(() => {\n const menu = mobileRef.current;\n if (!menu) return;\n const reduced = prefersReducedMotion();\n const items = menu.querySelectorAll('[data-mobile-item]');\n\n if (mobileOpen) {\n gsap.set(menu, { visibility: 'visible' });\n gsap.fromTo(\n menu,\n { opacity: 0 },\n { opacity: 1, duration: reduced ? 0 : 0.28, ease: 'power2.out' }\n );\n gsap.fromTo(\n items,\n { y: 22, opacity: 0 },\n {\n y: 0,\n opacity: 1,\n duration: reduced ? 0 : 0.45,\n ease: 'power3.out',\n stagger: reduced ? 0 : 0.035,\n delay: reduced ? 0 : 0.08,\n }\n );\n mobileCloseRef.current?.focus();\n } else {\n gsap.to(menu, {\n opacity: 0,\n duration: reduced ? 0 : 0.22,\n ease: 'power2.out',\n onComplete: () => gsap.set(menu, { visibility: 'hidden' }),\n });\n }\n }, [mobileOpen]);\n\n useEffect(() => {\n if (!mobileOpen) return;\n const desktop = window.matchMedia(DESKTOP_QUERY);\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key !== 'Escape') return;\n setMobileOpen(false);\n toggleRef.current?.focus();\n };\n const onChange = (event: MediaQueryListEvent) => {\n if (event.matches) setMobileOpen(false);\n };\n document.addEventListener('keydown', onKeyDown);\n desktop.addEventListener('change', onChange);\n return () => {\n document.removeEventListener('keydown', onKeyDown);\n desktop.removeEventListener('change', onChange);\n };\n }, [mobileOpen]);\n\n useEffect(() => {\n return () => {\n cancelClose();\n if (hoverTimer.current !== null) window.clearTimeout(hoverTimer.current);\n pendingHover.current = null;\n };\n }, []);\n\n return (\n <section\n ref={rootRef}\n aria-label={`${brand} site`}\n className={cn(\n 'relative isolate h-svh w-full overflow-hidden bg-[#03110f] text-[#e8f3f2]',\n className\n )}\n {...props}\n >\n <svg aria-hidden=\"true\" className=\"pointer-events-none absolute size-0\">\n <defs>\n <filter\n id={gooId}\n x=\"-40%\"\n y=\"-250%\"\n width=\"180%\"\n height=\"600%\"\n colorInterpolationFilters=\"sRGB\"\n >\n <feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"5\" result=\"blur\" />\n <feColorMatrix\n in=\"blur\"\n mode=\"matrix\"\n values=\"1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 22 -9\"\n result=\"goo\"\n />\n <feBlend in=\"SourceGraphic\" in2=\"goo\" />\n </filter>\n <filter\n id={linkGooId}\n x=\"-10%\"\n y=\"-10%\"\n width=\"120%\"\n height=\"120%\"\n colorInterpolationFilters=\"sRGB\"\n >\n <feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"3.5\" result=\"blur\" />\n <feColorMatrix\n in=\"blur\"\n mode=\"matrix\"\n values=\"1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 20 -8\"\n result=\"goo\"\n />\n <feComposite in=\"SourceGraphic\" in2=\"goo\" operator=\"atop\" />\n </filter>\n <filter\n id={panelGooId}\n x=\"-15%\"\n y=\"-15%\"\n width=\"130%\"\n height=\"130%\"\n colorInterpolationFilters=\"sRGB\"\n >\n <feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"7\" result=\"blur\" />\n <feColorMatrix\n in=\"blur\"\n mode=\"matrix\"\n values=\"1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 22 -10\"\n />\n </filter>\n </defs>\n </svg>\n\n <header\n className={cn(CONTAINER, 'pointer-events-none absolute inset-x-0 top-0 z-30 pt-5')}\n onBlur={(event) => {\n if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {\n closeMenu({ clearHighlight: true });\n }\n }}\n >\n <div\n ref={barRef}\n className=\"pointer-events-auto relative mx-auto w-full max-w-[26rem] md:max-w-[40rem] lg:max-w-[61.25rem] xl:max-w-[73.75rem]\"\n >\n <div\n ref={shellRef}\n onPointerEnter={cancelClose}\n onPointerLeave={(event) => {\n pointerPos.current = { x: event.clientX, y: event.clientY };\n scheduleClose();\n if (!openRef.current) hideBlob();\n }}\n className=\"relative flex h-15 items-center justify-between gap-3 rounded-full border border-[#9fd4d6]/12 bg-[#071d1b]/70 px-3 shadow-[0_24px_60px_-32px_rgb(0_0_0/0.95)] backdrop-blur-xl\"\n >\n <span\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute inset-0 overflow-hidden rounded-full\"\n >\n <span\n className=\"absolute inset-0\"\n style={{\n filter: `url(#${gooId}) drop-shadow(0 0 14px rgb(198 245 111 / 0.2))`,\n }}\n >\n <span\n ref={pillRef}\n className=\"absolute top-1/2 left-0 h-10 w-0 -translate-y-1/2 opacity-0\"\n >\n <span\n ref={coreRef}\n className=\"block size-full rounded-full bg-[radial-gradient(120%_160%_at_30%_0%,#eaffc4,#c6f56f_48%,#a8e35a)]\"\n />\n </span>\n <span\n ref={dropRef}\n style={{ width: DROP_SIZE, height: DROP_SIZE }}\n className=\"absolute top-1/2 left-0 -translate-y-1/2 rounded-full bg-[#c6f56f] opacity-0\"\n />\n </span>\n </span>\n\n <a\n href={homeHref}\n data-intro=\"\"\n aria-label={`${brand} home`}\n className={cn(\n 'relative z-10 flex shrink-0 items-center gap-2.5 rounded-full pr-1',\n focusRing\n )}\n >\n {logo}\n <span className=\"hidden text-[17px] font-medium tracking-tight sm:block\">\n {brand}\n </span>\n </a>\n\n <nav aria-label=\"Primary\" className=\"pointer-events-auto relative hidden lg:block\">\n <ul className=\"relative z-10 flex items-center\">\n {menus.map((menu, index) => {\n const panelMenu = Boolean(menu.columns?.length);\n const isOpen = openKey === menu.label;\n const labelClass = cn(\n 'relative flex h-10 items-center rounded-full px-3.5 text-[13.5px] font-medium tracking-[-0.01em] transition-colors duration-200 text-[#e8f3f2]/75 hover:text-[#e8f3f2]'\n );\n const chevronClass = cn(\n 'w-[9px] transition-transform duration-400',\n EASE_OUT_EXPO,\n isOpen && 'rotate-180'\n );\n\n return (\n <li key={menu.label} className=\"relative\">\n {panelMenu ? (\n <button\n ref={(node) => {\n triggerRefs.current[menu.label] = node;\n }}\n type=\"button\"\n aria-expanded={isOpen}\n aria-controls={panelId}\n aria-haspopup=\"true\"\n onPointerEnter={(event) => handleTriggerEnter(menu, event.currentTarget)}\n onFocus={(event) => {\n cancelHover();\n moveBlob(event.currentTarget);\n if (skipFocusOpen.current) {\n skipFocusOpen.current = false;\n return;\n }\n openMenu(menu, event.currentTarget);\n }}\n onClick={() => {\n const trigger = triggerRefs.current[menu.label];\n if (!trigger) return;\n if (isOpen) closeMenu();\n else openMenu(menu, trigger);\n }}\n className={cn(labelClass, 'cursor-pointer', focusRing)}\n >\n <span className=\"relative flex items-center gap-1.5\">\n <span>{menu.label}</span>\n <ChevronIcon className={chevronClass} />\n <span\n aria-hidden=\"true\"\n ref={(node) => {\n inkRefs.current[index] = node;\n }}\n style={{ clipPath: 'inset(0 100% 0 0)' }}\n className=\"pointer-events-none absolute inset-0 flex items-center gap-1.5 text-[#03110f] opacity-0\"\n >\n {menu.label}\n <ChevronIcon className={chevronClass} />\n </span>\n </span>\n </button>\n ) : (\n <a\n href={menu.href ?? '#'}\n {...externalProps(menu.href ?? '')}\n aria-current={menu.href && current(menu.href) ? 'page' : undefined}\n onPointerEnter={(event) => handleTriggerEnter(menu, event.currentTarget)}\n onFocus={(event) => {\n cancelHover();\n moveBlob(event.currentTarget);\n closeMenu();\n }}\n className={cn(labelClass, focusRing)}\n >\n <span className=\"relative block\">\n {menu.label}\n <span\n aria-hidden=\"true\"\n ref={(node) => {\n inkRefs.current[index] = node;\n }}\n style={{ clipPath: 'inset(0 100% 0 0)' }}\n className=\"pointer-events-none absolute inset-0 block text-[#03110f] opacity-0\"\n >\n {menu.label}\n </span>\n </span>\n </a>\n )}\n </li>\n );\n })}\n </ul>\n </nav>\n\n <div data-intro=\"\" className=\"relative z-10 flex shrink-0 items-center gap-2\">\n <DotBloomButton\n variant=\"secondary\"\n onClick={onActionClick}\n className=\"hidden h-10 rounded-full px-4 text-[12.5px] focus-visible:ring-[#c6f56f] focus-visible:ring-offset-[#071d1b] sm:inline-flex\"\n >\n {actionLabel}\n </DotBloomButton>\n\n <button\n ref={toggleRef}\n type=\"button\"\n aria-label={mobileOpen ? 'Close menu' : 'Open menu'}\n aria-expanded={mobileOpen}\n aria-controls={mobileId}\n onClick={() => setMobileOpen((value) => !value)}\n className={cn(\n 'grid size-10 cursor-pointer place-items-center rounded-full border border-[#9fd4d6]/15 bg-white/[0.03] lg:hidden',\n focusRing\n )}\n >\n <span aria-hidden=\"true\" className=\"relative block h-2.5 w-5\">\n <span\n className={cn(\n 'absolute inset-x-0 top-0 h-px bg-[#e8f3f2] transition-transform duration-500 motion-reduce:transition-none',\n EASE_OUT_EXPO,\n mobileOpen && 'translate-y-[4.5px] rotate-45'\n )}\n />\n <span\n className={cn(\n 'absolute inset-x-0 bottom-0 h-px bg-[#e8f3f2] transition-transform duration-500 motion-reduce:transition-none',\n EASE_OUT_EXPO,\n mobileOpen && '-translate-y-[4.5px] -rotate-45'\n )}\n />\n </span>\n </button>\n </div>\n </div>\n\n <div\n ref={panelRef}\n id={panelId}\n inert={!openKey}\n onPointerEnter={cancelClose}\n onPointerLeave={scheduleClose}\n className=\"pointer-events-none absolute inset-x-0 top-0 z-40 h-0 opacity-0\"\n >\n <svg\n aria-hidden=\"true\"\n className=\"absolute top-0 left-0 h-[44rem] w-full overflow-visible\"\n style={{\n filter:\n 'drop-shadow(0 0 0.6px rgb(159 212 214 / 0.45)) drop-shadow(0 32px 40px rgb(0 0 0 / 0.55))',\n }}\n >\n <g\n filter={`url(#${panelGooId})`}\n fill=\"#071d1b\"\n className={openKey ? 'pointer-events-auto' : 'pointer-events-none'}\n >\n <path ref={shapeRef} />\n <circle ref={bumpRef} r=\"0\" />\n </g>\n </svg>\n <div\n ref={bodyRef}\n className={cn(\n 'absolute top-0 left-0',\n openKey ? 'pointer-events-auto' : 'pointer-events-none'\n )}\n >\n {menus.map((menu) => {\n if (!menu.columns?.length) return null;\n const width = panelWidth(menu);\n\n return (\n <div\n key={menu.label}\n ref={(node) => {\n sectionRefs.current[menu.label] = node;\n }}\n data-section=\"\"\n inert={openKey !== menu.label}\n onPointerLeave={hideHover}\n style={{ width }}\n className={cn(\n 'absolute top-0 left-0 p-2.5 opacity-0',\n openKey === menu.label ? 'pointer-events-auto' : 'pointer-events-none'\n )}\n >\n <span aria-hidden=\"true\" className=\"pointer-events-none absolute inset-0\">\n <span\n data-hover-row=\"\"\n className=\"absolute top-0 left-0 rounded-2xl bg-[radial-gradient(140%_120%_at_0%_50%,rgb(198_245_111/0.12),transparent_60%),linear-gradient(#0c2924,#0c2924)] opacity-0 shadow-[inset_0_1px_0_rgb(232_243_242/0.06)]\"\n />\n <span className=\"absolute inset-0\" style={{ filter: `url(#${linkGooId})` }}>\n <span\n data-hover-blob=\"\"\n className=\"absolute top-0 left-0 rounded-[11px] bg-[#c6f56f] opacity-0\"\n />\n <span\n data-hover-drop=\"\"\n style={{ width: ICON_DROP, height: ICON_DROP }}\n className=\"absolute top-0 left-0 rounded-full bg-[#c6f56f] opacity-0\"\n />\n </span>\n </span>\n <div className={cn('relative grid gap-2.5', panelGrid(menu))}>\n {menu.columns.map((column, columnIndex) => (\n <ul key={columnIndex} className=\"flex flex-col\">\n {column.map((link) => (\n <li key={`${link.label}-${link.href}`} data-item=\"\">\n <a\n href={link.href}\n {...externalProps(link.href)}\n aria-current={current(link.href) ? 'page' : undefined}\n onClick={() => closeMenu()}\n onPointerEnter={(event) => moveHover(event.currentTarget)}\n onFocus={(event) => moveHover(event.currentTarget)}\n className={cn(\n 'group/link relative flex items-start gap-3 rounded-2xl p-2.5',\n focusRing\n )}\n >\n <span\n data-icon=\"\"\n className=\"mt-0.5 grid size-8 shrink-0 place-items-center rounded-[11px] border border-[#9fd4d6]/12 bg-[#0a2420]/45 text-[#c6f56f] transition-[color,background-color,border-color] duration-300 group-hover/link:border-transparent group-hover/link:bg-transparent group-hover/link:text-[#03110f] group-hover/link:delay-75 group-focus-visible/link:border-transparent group-focus-visible/link:bg-transparent group-focus-visible/link:text-[#03110f]\"\n >\n <MenuGlyph icon={link.icon ?? 'bloom'} className=\"size-4\" />\n </span>\n <span\n className={cn(\n 'min-w-0 transition-transform duration-500 group-hover/link:translate-x-1',\n EASE_OUT_EXPO\n )}\n >\n <span className=\"flex items-center gap-1.5 text-[13.5px] leading-tight font-medium text-[#e8f3f2]\">\n {link.label}\n <ArrowIcon\n className={cn(\n 'size-3 -translate-x-1 text-[#c6f56f] opacity-0 transition-[opacity,translate] duration-500 group-hover/link:translate-x-0 group-hover/link:opacity-100',\n EASE_OUT_EXPO\n )}\n />\n </span>\n {link.description ? (\n <span className=\"mt-1 block text-[12.5px] leading-snug text-[#9fd4d6]/60 transition-colors duration-300 group-hover/link:text-[#9fd4d6]/85\">\n {link.description}\n </span>\n ) : null}\n </span>\n </a>\n </li>\n ))}\n </ul>\n ))}\n\n {menu.feature ? (\n <div\n data-item=\"\"\n onPointerEnter={hideHover}\n className=\"flex flex-col overflow-hidden rounded-2xl border border-[#9fd4d6]/12 bg-[#0a2420]\"\n >\n <div className=\"relative h-28 shrink-0 overflow-hidden\">\n {menu.feature.image ? (\n <img\n src={menu.feature.image}\n alt=\"\"\n className=\"size-full object-cover\"\n loading=\"lazy\"\n />\n ) : (\n <FeatureArt />\n )}\n </div>\n <div className=\"flex flex-1 flex-col p-3\">\n <p className=\"text-[13px] font-medium\">{menu.feature.title}</p>\n <p className=\"mt-1 text-[12px] leading-snug text-[#9fd4d6]/60\">\n {menu.feature.description}\n </p>\n <a\n href={menu.feature.href}\n {...externalProps(menu.feature.href)}\n onClick={() => closeMenu()}\n className={cn(\n 'group/feature mt-auto inline-flex items-center gap-1.5 pt-3 text-[12px] font-medium text-[#c6f56f]',\n focusRing\n )}\n >\n {menu.feature.cta ?? 'Explore'}\n <ArrowIcon className=\"size-3.5 transition-transform duration-300 group-hover/feature:translate-x-0.5\" />\n </a>\n </div>\n </div>\n ) : null}\n </div>\n </div>\n );\n })}\n </div>\n </div>\n </div>\n </header>\n\n <div\n ref={scrollerRef}\n inert={mobileOpen}\n data-lenis-prevent=\"\"\n className=\"absolute inset-0 z-10 [scrollbar-width:none] overflow-y-auto [&::-webkit-scrollbar]:hidden\"\n >\n {children ?? <SamplePage />}\n </div>\n\n <div\n ref={mobileRef}\n id={mobileId}\n inert={!mobileOpen}\n className=\"invisible fixed inset-0 z-50 flex flex-col bg-[#04140f] lg:hidden\"\n >\n <div className=\"flex items-center justify-between px-4 pt-5 pb-4 sm:px-6\">\n <a href={homeHref} className={cn('flex items-center gap-2.5 rounded-full', focusRing)}>\n {logo}\n <span className=\"text-[17px] font-medium tracking-tight\">{brand}</span>\n </a>\n <button\n ref={mobileCloseRef}\n type=\"button\"\n aria-label=\"Close menu\"\n onClick={() => {\n setMobileOpen(false);\n toggleRef.current?.focus();\n }}\n className={cn(\n 'grid size-10 cursor-pointer place-items-center rounded-full border border-[#9fd4d6]/15 bg-white/[0.04]',\n focusRing\n )}\n >\n <svg aria-hidden=\"true\" viewBox=\"0 0 24 24\" className=\"size-4\" fill=\"none\">\n <path\n d=\"M6 6l12 12M18 6L6 18\"\n stroke=\"currentColor\"\n strokeWidth=\"1.4\"\n strokeLinecap=\"round\"\n />\n </svg>\n </button>\n </div>\n\n <div className=\"min-h-0 flex-1 overflow-y-auto px-4 pb-8 sm:px-6\">\n <ul className=\"flex flex-col\">\n {menus.map((menu) => {\n const panelMenu = Boolean(menu.columns?.length);\n const isOpen = accordion === menu.label;\n const links = menu.columns?.flat() ?? [];\n const contentId = `${mobileId}-${menu.label.replace(/[^a-zA-Z0-9]/g, '')}`;\n\n return (\n <li\n key={menu.label}\n data-mobile-item=\"\"\n className=\"border-b border-[#9fd4d6]/10 last:border-b-0\"\n >\n {panelMenu ? (\n <>\n <button\n type=\"button\"\n aria-expanded={isOpen}\n aria-controls={contentId}\n onClick={() => setAccordion(isOpen ? null : menu.label)}\n className={cn(\n 'flex w-full cursor-pointer items-center justify-between py-4 text-left',\n focusRing\n )}\n >\n <span className=\"text-[26px] leading-none font-medium tracking-[-0.03em]\">\n {menu.label}\n </span>\n <ChevronIcon\n className={cn(\n 'w-3 text-[#c6f56f] transition-transform duration-300',\n isOpen && 'rotate-180'\n )}\n />\n </button>\n <div\n id={contentId}\n inert={!isOpen}\n className={cn(\n 'grid transition-[grid-template-rows] duration-400 motion-reduce:transition-none',\n EASE_OUT_EXPO,\n isOpen ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'\n )}\n >\n <div className=\"overflow-hidden\">\n <ul className=\"flex flex-col gap-1 pb-3\">\n {links.map((link) => (\n <li key={`${link.label}-${link.href}`}>\n <a\n href={link.href}\n {...externalProps(link.href)}\n onClick={() => setMobileOpen(false)}\n className={cn(\n 'flex items-center gap-3 rounded-xl px-2 py-2.5 text-[15px] text-[#e8f3f2]/75',\n focusRing\n )}\n >\n <MenuGlyph\n icon={link.icon ?? 'bloom'}\n className=\"size-4 text-[#c6f56f]\"\n />\n {link.label}\n </a>\n </li>\n ))}\n </ul>\n </div>\n </div>\n </>\n ) : (\n <a\n href={menu.href ?? '#'}\n {...externalProps(menu.href ?? '')}\n onClick={() => setMobileOpen(false)}\n className={cn(\n 'block py-4 text-[26px] leading-none font-medium tracking-[-0.03em]',\n focusRing\n )}\n >\n {menu.label}\n </a>\n )}\n </li>\n );\n })}\n </ul>\n\n <div data-mobile-item=\"\" className=\"mt-6\">\n <DotBloomButton\n variant=\"secondary\"\n onClick={(event) => {\n setMobileOpen(false);\n onActionClick?.(event);\n }}\n className=\"h-12 w-full rounded-full focus-visible:ring-[#c6f56f] focus-visible:ring-offset-[#04140f]\"\n >\n {actionLabel}\n </DotBloomButton>\n </div>\n </div>\n </div>\n </section>\n );\n};\n\nexport default NavbarGooeyBloom;\n"
+ }
}
};
diff --git a/apps/docs/registry/demos/navbar-corner-panel.tsx b/apps/docs/registry/demos/navbar-corner-panel.tsx
index 75254a4..f592b1d 100644
--- a/apps/docs/registry/demos/navbar-corner-panel.tsx
+++ b/apps/docs/registry/demos/navbar-corner-panel.tsx
@@ -4,7 +4,7 @@ import NavbarCornerPanel from '@/registry/tweenui/navbar-corner-panel';
const NavbarCornerPanelDemo = () => {
return (
- + + Metaball navigation +
++ Point at a link. A pool of lime stretches across the bar, sheds a few droplets on the way, + and settles under the label. Open a menu and the card below re-forms around it. +
+ +{menu.feature.title}
++ {menu.feature.description} +
+ closeMenu()} + className={cn( + 'group/feature mt-auto inline-flex items-center gap-1.5 pt-3 text-[12px] font-medium text-[#c6f56f]', + focusRing + )} + > + {menu.feature.cta ?? 'Explore'} +