"use client"; import { useEffect, useRef, useState } from 'react'; import Link from 'next/link'; import { gsap } from 'gsap'; import './PillNav.css'; interface PillNavItem { label: string; href: string; ariaLabel?: string; } interface PillNavProps { logo?: 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, logoAlt = 'Logo', items, activeHref, className = '', ease = 'power3.easeOut', baseColor = '#fff', pillColor = '#060010', hoveredPillTextColor = '#060010', pillTextColor, onMobileMenuClick, initialLoadAnimation = true }: PillNavProps) => { const resolvedPillTextColor = pillTextColor ?? baseColor; 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); const hasAnimatedRef = useRef(false); useEffect(() => { const layout = () => { circleRefs.current.forEach(circle => { 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 }); const index = circleRefs.current.indexOf(circle); if (index === -1) return; 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(); const onResize = () => layout(); window.addEventListener('resize', onResize); if (document.fonts?.ready) { document.fonts.ready.then(layout).catch(() => {}); } const menu = mobileMenuRef.current; if (menu) { gsap.set(menu, { visibility: 'hidden', opacity: 0, scaleY: 1 }); } if (initialLoadAnimation && !hasAnimatedRef.current) { 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 }); } hasAnimatedRef.current = true; } else if (navItemsRef.current && !hasAnimatedRef.current) { // Wenn initialLoadAnimation false ist, setze die Items sofort auf sichtbar const navItems = navItemsRef.current; gsap.set(navItems, { width: 'auto', overflow: 'visible' }); if (logoRef.current) { gsap.set(logoRef.current, { scale: 1 }); } hasAnimatedRef.current = true; } return () => window.removeEventListener('resize', onResize); }, [items, ease]); // initialLoadAnimation entfernt, damit es nur einmal läuft 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' }); }; 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' }); }; 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.2, 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, scaleY: 1 }, { opacity: 1, y: 0, scaleY: 1, duration: 0.3, ease, transformOrigin: 'top center' } ); } else { gsap.to(menu, { opacity: 0, y: 10, scaleY: 1, duration: 0.2, ease, transformOrigin: 'top center', onComplete: () => { gsap.set(menu, { visibility: 'hidden' }); } }); } } onMobileMenuClick?.(); }; const isExternalLink = (href: string) => href.startsWith('http://') || href.startsWith('https://') || href.startsWith('//') || href.startsWith('mailto:') || href.startsWith('tel:') || href.startsWith('#'); const cssVars = { ['--base']: baseColor, ['--pill-bg']: pillColor, ['--hover-text']: hoveredPillTextColor, ['--pill-text']: resolvedPillTextColor } as React.CSSProperties; return (
    {items.map((item, i) => (
  • {isExternalLink(item.href) ? ( setIsMobileMenuOpen(false)} > {item.label} ) : ( { setIsMobileMenuOpen(false); if (item.href.startsWith('#')) { const element = document.getElementById(item.href.substring(1)); if (element) { element.scrollIntoView({ behavior: 'smooth' }); } } }} > {item.label} )}
  • ))}
); }; export default PillNav;