import { useEffect, useRef, useState } from 'react'; import { Link, useLocation } from 'react-router-dom'; import { gsap } from 'gsap'; import './PillNav.css'; export interface PillNavItem { label: string; href?: string; ariaLabel?: string; onClick?: () => void; } interface PillNavProps { logo?: string; logoText?: string; logoAlt?: string; items: PillNavItem[]; activeHref?: string; className?: string; ease?: string; baseColor?: string; pillColor?: string; hoveredPillTextColor?: string; pillTextColor?: string; onMobileMenuClick?: () => void; initialLoadAnimation?: boolean; } const PillNav = ({ logo, logoText, logoAlt = 'Logo', items, activeHref, className = '', ease = 'power3.easeOut', baseColor = '#0d0720', pillColor = '#1e0f3a', hoveredPillTextColor = '#e9d5ff', pillTextColor, onMobileMenuClick, initialLoadAnimation = true, }: PillNavProps) => { const resolvedPillTextColor = pillTextColor ?? '#c084fc'; const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const circleRefs = useRef<(HTMLSpanElement | null)[]>([]); const tlRefs = useRef([]); const activeTweenRefs = useRef([]); const logoImgRef = useRef(null); const logoTweenRef = useRef(null); const hamburgerRef = useRef(null); const mobileMenuRef = useRef(null); const navItemsRef = useRef(null); const logoRef = useRef(null); useEffect(() => { const layout = () => { circleRefs.current.forEach((circle, index) => { if (!circle?.parentElement) return; const pill = circle.parentElement; const rect = pill.getBoundingClientRect(); const { width: w, height: h } = rect; const R = ((w * w) / 4 + h * h) / (2 * h); const D = Math.ceil(2 * R) + 2; const delta = Math.ceil(R - Math.sqrt(Math.max(0, R * R - (w * w) / 4))) + 1; const originY = D - delta; circle.style.width = `${D}px`; circle.style.height = `${D}px`; circle.style.bottom = `-${delta}px`; gsap.set(circle, { xPercent: -50, scale: 0, transformOrigin: `50% ${originY}px` }); const label = pill.querySelector('.pill-label'); const white = pill.querySelector('.pill-label-hover'); if (label) gsap.set(label, { y: 0 }); if (white) gsap.set(white, { y: h + 12, opacity: 0 }); tlRefs.current[index]?.kill(); const tl = gsap.timeline({ paused: true }); tl.to(circle, { scale: 1.2, xPercent: -50, duration: 2, ease, overwrite: 'auto' }, 0); if (label) tl.to(label, { y: -(h + 8), duration: 2, ease, overwrite: 'auto' }, 0); if (white) { gsap.set(white, { y: Math.ceil(h + 100), opacity: 0 }); tl.to(white, { y: 0, opacity: 1, duration: 2, ease, overwrite: 'auto' }, 0); } tlRefs.current[index] = tl; }); }; layout(); window.addEventListener('resize', layout); document.fonts?.ready?.then(layout).catch(() => {}); const menu = mobileMenuRef.current; if (menu) gsap.set(menu, { visibility: 'hidden', opacity: 0, scaleY: 1 }); if (initialLoadAnimation) { const logoEl = logoRef.current; const navItems = navItemsRef.current; if (logoEl) { gsap.set(logoEl, { scale: 0 }); gsap.to(logoEl, { scale: 1, duration: 0.6, ease }); } if (navItems) { gsap.set(navItems, { width: 0, overflow: 'hidden' }); gsap.to(navItems, { width: 'auto', duration: 0.6, ease }); } } return () => window.removeEventListener('resize', layout); }, [items, ease, initialLoadAnimation]); const handleEnter = (i: number) => { const tl = tlRefs.current[i]; if (!tl) return; activeTweenRefs.current[i]?.kill(); activeTweenRefs.current[i] = tl.tweenTo(tl.duration(), { duration: 0.3, ease, overwrite: 'auto' }) as gsap.core.Tween; }; const handleLeave = (i: number) => { const tl = tlRefs.current[i]; if (!tl) return; activeTweenRefs.current[i]?.kill(); activeTweenRefs.current[i] = tl.tweenTo(0, { duration: 0.2, ease, overwrite: 'auto' }) as gsap.core.Tween; }; const handleLogoEnter = () => { const img = logoImgRef.current; if (!img) return; logoTweenRef.current?.kill(); gsap.set(img, { rotate: 0 }); logoTweenRef.current = gsap.to(img, { rotate: 360, duration: 0.4, ease, overwrite: 'auto' }); }; const toggleMobileMenu = () => { const newState = !isMobileMenuOpen; setIsMobileMenuOpen(newState); const hamburger = hamburgerRef.current; const menu = mobileMenuRef.current; if (hamburger) { const lines = hamburger.querySelectorAll('.hamburger-line'); if (newState) { gsap.to(lines[0], { rotation: 45, y: 3, duration: 0.3, ease }); gsap.to(lines[1], { rotation: -45, y: -3, duration: 0.3, ease }); } else { gsap.to(lines[0], { rotation: 0, y: 0, duration: 0.3, ease }); gsap.to(lines[1], { rotation: 0, y: 0, duration: 0.3, ease }); } } if (menu) { if (newState) { gsap.set(menu, { visibility: 'visible' }); gsap.fromTo(menu, { opacity: 0, y: 10 }, { opacity: 1, y: 0, duration: 0.3, ease }); } else { gsap.to(menu, { opacity: 0, y: 10, duration: 0.2, ease, onComplete: () => gsap.set(menu, { visibility: 'hidden' }) }); } } onMobileMenuClick?.(); }; const cssVars = { ['--base']: baseColor, ['--pill-bg']: pillColor, ['--hover-text']: hoveredPillTextColor, ['--pill-text']: resolvedPillTextColor, } as React.CSSProperties; const location = useLocation(); const currentHref = activeHref ?? location.pathname; const renderPill = (item: PillNavItem, i: number) => { const isActive = item.href ? currentHref === item.href : false; const pillClass = `pill${isActive ? ' is-active' : ''}`; const inner = ( <>