header.tsx
"use client"
import { useEffect, useId, useRef, useState } from "react"
import Link from "next/link"
import { cn } from "cn"
import {
animate,
AnimatePresence,
motion,
useMotionValue,
useReducedMotion,
useTransform,
} from "motion/react"
import useMeasure from "react-use-measure"
import { Button } from "@/components/ui/button"
import { Icons } from "@/components/ui/icons"
const links = [
{ label: "Home", href: "#" },
{ label: "About", href: "#about" },
{ label: "Blog", href: "#blog" },
{ label: "Contact", href: "#contact" },
]
export function SiteHeader() {
const [isOpen, setIsOpen] = useState(false)
const [measureRef, bounds] = useMeasure({ offsetSize: true })
const navRef = useRef<HTMLElement>(null)
const toggleRef = useRef<HTMLButtonElement>(null)
const menuId = useId()
const reducedMotion = useReducedMotion()
const width = useMotionValue(192)
const viewBox = useTransform(width, (value) => `0 0 ${value} 96`)
const fill = useTransform(
width,
(value) =>
`M0 0 H${value} V40 H${value - 8} C${value - 36} 40 ${value - 36} 72 ${value - 64} 72 H64 C36 72 36 40 8 40 H0 Z`
)
const stroke = useTransform(
width,
(value) =>
`M0 39.5 H8 C36 39.5 36 71.5 64 71.5 H${value - 64} C${value - 36} 71.5 ${value - 36} 39.5 ${value - 8} 39.5 H${value}`
)
useEffect(() => {
const controls = animate(
width,
isOpen ? Math.max(192, bounds.width) : 192,
reducedMotion
? { duration: 0 }
: {
type: "spring",
stiffness: 340,
damping: 30,
delay: isOpen ? 0 : 0.2,
}
)
return () => controls.stop()
}, [bounds.width, isOpen, reducedMotion, width])
useEffect(() => {
if (!isOpen) return
const dismiss = (event: PointerEvent | KeyboardEvent) => {
if (event instanceof KeyboardEvent) {
if (event.key !== "Escape") return
event.preventDefault()
setIsOpen(false)
toggleRef.current?.focus()
} else if (
event.target instanceof Node &&
!navRef.current?.contains(event.target)
)
setIsOpen(false)
}
document.addEventListener("pointerdown", dismiss)
document.addEventListener("keydown", dismiss)
return () => {
document.removeEventListener("pointerdown", dismiss)
document.removeEventListener("keydown", dismiss)
}
}, [isOpen])
return (
<header
data-slot="header"
className="pointer-events-none fixed inset-x-0 top-0 z-50 flex h-24 justify-center"
>
<div
aria-hidden="true"
className="absolute inset-x-0 top-0 h-10 border-b border-border/50 bg-card"
/>
<div
ref={measureRef}
className="relative flex w-[calc(100%-2rem)] max-w-xl justify-center"
>
<motion.nav
ref={navRef}
style={{ width }}
aria-label="Main navigation"
className="pointer-events-auto relative h-24 overflow-hidden"
onPointerEnter={(event) => {
if (event.pointerType === "mouse") setIsOpen(true)
}}
onPointerLeave={() => {
if (!navRef.current?.contains(document.activeElement))
setIsOpen(false)
}}
onBlur={(event) => {
if (!event.currentTarget.contains(event.relatedTarget))
setIsOpen(false)
}}
>
<motion.svg
aria-hidden="true"
style={{ width }}
height="96"
viewBox={viewBox}
className="pointer-events-none absolute inset-0 overflow-visible"
>
<motion.path d={fill} className="fill-card" />
<motion.path
d={stroke}
fill="none"
className="stroke-border/50"
vectorEffect="non-scaling-stroke"
/>
</motion.svg>
<Button
ref={toggleRef}
type="button"
aria-label={isOpen ? "Close navigation" : "Open navigation"}
aria-expanded={isOpen}
aria-controls={isOpen ? menuId : undefined}
onClick={() => setIsOpen(!isOpen)}
variant="ghost"
size="icon-lg"
className="absolute top-3.5 left-1/2 z-20 -translate-x-1/2"
>
<Icons.Logo aria-hidden="true" className="size-7" />
</Button>
<AnimatePresence initial={false}>
{isOpen && (
<motion.div
data-slot="navigation-menu-popup"
id={menuId}
initial="closed"
animate="open"
exit="closed"
variants={{
open: {
transition: {
delayChildren: reducedMotion ? 0 : 0.12,
staggerChildren: reducedMotion ? 0 : 0.025,
},
},
closed: {
transition: {
staggerChildren: reducedMotion ? 0 : 0.015,
staggerDirection: -1,
},
},
}}
style={{ width: Math.max(192, bounds.width) - 32 }}
className="absolute top-3.5 left-1/2 grid h-10 -translate-x-1/2 grid-cols-[1fr_2.5rem_1fr] gap-2 sm:gap-6"
>
{[links.slice(0, 2), links.slice(2)].map((group, index) => (
<div
key={group[0].href}
className={cn(
"flex items-center gap-1 sm:gap-6",
index === 0 ? "col-start-1 justify-end" : "col-start-3"
)}
>
{group.map((link) => (
<motion.div
key={link.href}
variants={{
open: { opacity: 1, y: 0 },
closed: { opacity: 0, y: reducedMotion ? 0 : -10 },
}}
transition={{
duration: reducedMotion ? 0 : 0.16,
ease: "easeOut",
}}
>
<Button
onClick={() => setIsOpen(false)}
variant="ghost"
className="sm:text-sm"
render={<Link href={link.href} role="link" />}
nativeButton={false}
>
{link.label}
</Button>
</motion.div>
))}
</div>
))}
</motion.div>
)}
</AnimatePresence>
</motion.nav>
</div>
</header>
)
}






























