.agents/oskvp/site/src/components/Navigation.tsx

141 lines
4.5 KiB
TypeScript

import { useState, useEffect, useRef } from 'react';
import gsap from 'gsap';
const NAV_LINKS = [
{ href: '/', label: 'All Work' },
{ href: '/music-videos', label: 'Music Videos' },
{ href: '/commercials', label: 'Commercials' },
{ href: '/about', label: 'About' },
{ href: '/contact', label: 'Contact' },
];
interface Props {
currentPath?: string;
}
export default function Navigation({ currentPath = '/' }: Props) {
const [isScrolled, setIsScrolled] = useState(false);
const [menuOpen, setMenuOpen] = useState(false);
const linksRef = useRef<HTMLAnchorElement[]>([]);
const overlayRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const onScroll = () => setIsScrolled(window.scrollY > 40);
window.addEventListener('scroll', onScroll, { passive: true });
return () => window.removeEventListener('scroll', onScroll);
}, []);
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape' && menuOpen) closeMenu();
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [menuOpen]);
function openMenu() {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
setMenuOpen(true);
document.body.style.overflow = 'hidden';
return;
}
setMenuOpen(true);
document.body.style.overflow = 'hidden';
const ctx = gsap.context(() => {
gsap.fromTo(overlayRef.current,
{ opacity: 0 },
{ opacity: 1, duration: 0.3, ease: 'power2.out' }
);
gsap.fromTo(linksRef.current,
{ y: 30, opacity: 0 },
{ y: 0, opacity: 1, duration: 0.5, ease: 'power2.out', stagger: 0.07, delay: 0.1 }
);
});
return () => ctx.revert();
}
function closeMenu() {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
setMenuOpen(false);
document.body.style.overflow = '';
return;
}
gsap.to(overlayRef.current, {
opacity: 0,
duration: 0.25,
ease: 'power2.in',
onComplete: () => {
setMenuOpen(false);
document.body.style.overflow = '';
},
});
}
return (
<>
<header
className={`fixed top-0 left-0 right-0 z-50 flex items-center justify-between px-6 md:px-10 transition-all duration-300 ${
isScrolled ? 'bg-black/80 backdrop-blur-md py-4' : 'py-6'
}`}
style={{ height: 'var(--nav-height)' }}
>
<a href="/" className="text-white font-display text-xl font-semibold tracking-wide hover:opacity-70 transition-opacity">
OSKV
</a>
<nav className="hidden md:flex items-center gap-8">
{NAV_LINKS.map((link) => (
<a
key={link.href}
href={link.href}
className={`text-sm font-light tracking-widest uppercase transition-opacity duration-200 ${
currentPath === link.href
? 'text-white opacity-100'
: 'text-white/60 hover:text-white hover:opacity-100'
}`}
>
{link.label}
</a>
))}
</nav>
<button
onClick={menuOpen ? closeMenu : openMenu}
className="md:hidden flex flex-col gap-1.5 p-2 -mr-2 group"
aria-label={menuOpen ? 'Close menu' : 'Open menu'}
>
<span className={`block w-6 h-px bg-white transition-all duration-300 ${menuOpen ? 'rotate-45 translate-y-[7px]' : ''}`} />
<span className={`block w-6 h-px bg-white transition-all duration-300 ${menuOpen ? 'opacity-0' : ''}`} />
<span className={`block w-6 h-px bg-white transition-all duration-300 ${menuOpen ? '-rotate-45 -translate-y-[7px]' : ''}`} />
</button>
</header>
{/* Mobile fullscreen menu */}
<div
ref={overlayRef}
className={`fixed inset-0 z-40 bg-black flex flex-col justify-center px-8 md:hidden ${menuOpen ? 'flex' : 'hidden'}`}
>
<nav className="flex flex-col gap-6">
{NAV_LINKS.map((link, i) => (
<a
key={link.href}
href={link.href}
ref={(el) => { if (el) linksRef.current[i] = el; }}
onClick={closeMenu}
className={`font-display text-4xl font-medium tracking-tight transition-opacity ${
currentPath === link.href ? 'text-white' : 'text-white/50 hover:text-white'
}`}
>
{link.label}
</a>
))}
</nav>
</div>
</>
);
}