"use client"; import React, { useState, useEffect, useRef } from 'react'; import './HandyAnsicht.css'; interface HandyAnsichtProps { children?: React.ReactNode; } type GooeyNavItem = { label: string; href: string; type?: 'link' | 'logo' | 'menu'; icon?: string; }; import { Menu, X } from 'lucide-react'; interface GooeyNavProps { items: GooeyNavItem[]; animationTime?: number; particleCount?: number; particleDistances?: [number, number]; particleR?: number; timeVariance?: number; colors?: number[]; initialActiveIndex?: number; } const GooeyNav: React.FC = ({ items, animationTime = 600, particleCount = 15, particleDistances = [90, 10], particleR = 100, timeVariance = 300, colors = [1, 2, 3, 1, 2, 3, 1, 4], initialActiveIndex = 0, }) => { const containerRef = useRef(null); const navRef = useRef(null); const filterRef = useRef(null); const textRef = useRef(null); const [activeIndex, setActiveIndex] = useState(initialActiveIndex); const [isMenuOpen, setIsMenuOpen] = useState(false); const noise = (n = 1) => n / 2 - Math.random() * n; const getXY = (distance: number, pointIndex: number, totalPoints: number) => { const angle = ((360 + noise(8)) / totalPoints) * pointIndex * (Math.PI / 180); return [distance * Math.cos(angle), distance * Math.sin(angle)]; }; const createParticle = (i: number, t: number, d: [number, number], r: number) => { const rotate = noise(r / 10); return { start: getXY(d[0], particleCount - i, particleCount), end: getXY(d[1] + noise(7), particleCount - i, particleCount), time: t, scale: 1 + noise(0.2), color: colors[Math.floor(Math.random() * colors.length)], rotate: rotate > 0 ? (rotate + r / 20) * 10 : (rotate - r / 20) * 10, }; }; const makeParticles = (element: HTMLElement) => { const d = particleDistances; const r = particleR; const bubbleTime = animationTime * 2 + timeVariance; element.style.setProperty('--time', `${bubbleTime}ms`); for (let i = 0; i < particleCount; i += 1) { const t = animationTime * 2 + noise(timeVariance * 2); const p = createParticle(i, t, d, r); element.classList.remove('active'); setTimeout(() => { const particle = document.createElement('span'); const point = document.createElement('span'); particle.classList.add('particle'); particle.style.setProperty('--start-x', `${p.start[0]}px`); particle.style.setProperty('--start-y', `${p.start[1]}px`); particle.style.setProperty('--end-x', `${p.end[0]}px`); particle.style.setProperty('--end-y', `${p.end[1]}px`); particle.style.setProperty('--time', `${p.time}ms`); particle.style.setProperty('--scale', `${p.scale}`); particle.style.setProperty('--color', `var(--color-${p.color}, white)`); particle.style.setProperty('--rotate', `${p.rotate}deg`); point.classList.add('point'); particle.appendChild(point); element.appendChild(particle); requestAnimationFrame(() => element.classList.add('active')); setTimeout(() => { try { element.removeChild(particle); } catch { /* ignore removal errors */ } }, t); }, 30); } }; const updateEffectPosition = (element: HTMLLIElement) => { if (!containerRef.current || !filterRef.current || !textRef.current) return; const containerRect = containerRef.current.getBoundingClientRect(); const pos = element.getBoundingClientRect(); const styles: Partial = { left: `${pos.x - containerRect.x}px`, top: `${pos.y - containerRect.y}px`, width: `${pos.width}px`, height: `${pos.height}px`, }; Object.assign(filterRef.current.style, styles); Object.assign(textRef.current.style, styles); // Nur Text kopieren wenn es kein Logo und kein Menü-Button ist if (!element.classList.contains('nav-item-logo') && !element.classList.contains('nav-item-menu')) { textRef.current.innerText = element.innerText; } else { textRef.current.innerText = ''; } }; const handleClick = (e: React.MouseEvent, index: number, item: GooeyNavItem) => { e.preventDefault(); if (item.type === 'logo') { window.scrollTo({ top: 0, behavior: 'smooth' }); setIsMenuOpen(false); return; } if (item.type === 'menu') { setIsMenuOpen(!isMenuOpen); return; } const liEl = e.currentTarget.parentElement as HTMLLIElement | null; if (!liEl || activeIndex === index) return; // Normal navigation window.location.href = item.href; setIsMenuOpen(false); // Menü schließen nach Klick setActiveIndex(index); updateEffectPosition(liEl); if (filterRef.current) { const particles = filterRef.current.querySelectorAll('.particle'); particles.forEach((p) => filterRef.current?.removeChild(p)); } if (textRef.current) { textRef.current.classList.remove('active'); void textRef.current.offsetWidth; // force reflow textRef.current.classList.add('active'); } if (filterRef.current) { makeParticles(filterRef.current); } }; const handleKeyDown = (e: React.KeyboardEvent, index: number, item: GooeyNavItem) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); const liEl = e.currentTarget.parentElement as HTMLLIElement | null; if (liEl) { handleClick({ currentTarget: e.currentTarget, preventDefault: () => {} } as React.MouseEvent, index, item); } } }; useEffect(() => { if (!navRef.current || !containerRef.current) return; const activeLi = navRef.current.querySelectorAll('li')[activeIndex] as HTMLLIElement | undefined; if (activeLi && !activeLi.classList.contains('nav-item-logo') && !activeLi.classList.contains('nav-item-menu')) { updateEffectPosition(activeLi); textRef.current?.classList.add('active'); } const resizeObserver = new ResizeObserver(() => { const currentActiveLi = navRef.current?.querySelectorAll('li')[activeIndex] as HTMLLIElement | undefined; if (currentActiveLi && !currentActiveLi.classList.contains('nav-item-logo') && !currentActiveLi.classList.contains('nav-item-menu')) { updateEffectPosition(currentActiveLi); } }); resizeObserver.observe(containerRef.current); return () => resizeObserver.disconnect(); }, [activeIndex]); // Modifizierte Items für die Anzeige (Logo + Menu Button im geschlossenen Zustand) const displayItems = isMenuOpen ? items // Zeige alle Items wenn offen (muss im CSS geregelt werden, dass sie untereinander oder anders dargestellt werden) : [items.find(i => i.type === 'logo')!, { label: 'Menu', href: '#', type: 'menu' } as GooeyNavItem]; return (
4 ? 'has-contact' : ''} ${isMenuOpen ? 'menu-open' : ''}`} ref={containerRef}> {/* Dropdown / Expanded Menu Items - Außerhalb von nav, damit position: fixed relativ zum Viewport ist */}
    {items.filter(i => i.type !== 'logo' && i.type !== 'menu').map((item, index) => (
  • { window.location.href = item.href; setIsMenuOpen(false); }}> {item.label}
  • ))}
{/* Effect nur für Inline-Items, hier deaktiviert da wir auf Overlay umstellen */} {/* */}
); }; const allGooeyItems: GooeyNavItem[] = [ { label: 'Home', href: '#', type: 'logo', icon: '/WebKlarLogo.png' }, { label: 'Über uns', href: '#about' }, { label: 'Leistungen', href: '#services' }, { label: 'Abläufe', href: '#process' }, { label: 'Kontakt', href: '#contact' }, ]; const HandyAnsicht: React.FC = ({ children }) => { const [isMobile, setIsMobile] = useState(false); const [isClient, setIsClient] = useState(false); useEffect(() => { setIsClient(true); const checkMobile = () => { setIsMobile(window.innerWidth < 768); }; checkMobile(); window.addEventListener('resize', checkMobile); const mediaQuery = window.matchMedia('(max-width: 767px)'); const handleMediaChange = (e: MediaQueryListEvent | MediaQueryList) => { setIsMobile(e.matches); }; if (mediaQuery.addEventListener) { mediaQuery.addEventListener('change', handleMediaChange); } else { mediaQuery.addListener(handleMediaChange); } handleMediaChange(mediaQuery); return () => { window.removeEventListener('resize', checkMobile); if (mediaQuery.removeEventListener) { mediaQuery.removeEventListener('change', handleMediaChange); } else { mediaQuery.removeListener(handleMediaChange); } }; }, []); // Immer alle Items anzeigen, Kontakt ist sofort da const currentItems = allGooeyItems; if (!isClient || !isMobile) { return <>{children}; } return (
{children}
); }; export default HandyAnsicht;