header.tsx
"use client"
import { useEffect, useId, useRef, useState } from "react"
import Link from "next/link"
import {
AnimatePresence,
motion,
MotionConfig,
useReducedMotion,
} from "motion/react"
import useMeasure from "react-use-measure"
import { Button } from "@/components/ui/button"
import { Icons } from "@/components/ui/icons"
import { XIcon } from 'lucide-react'
export function SiteHeader() {
const [isOpen, setIsOpen] = useState(false)
const [measureRef, { height }] = useMeasure()
const menuId = useId()
const reducedMotion = useReducedMotion()
const menuRef = useRef<HTMLElement>(null)
useEffect(() => {
if (!isOpen) return
const dismiss = (event: PointerEvent | KeyboardEvent) => {
if (event instanceof KeyboardEvent) {
if (event.key !== "Escape") return
event.preventDefault()
setIsOpen(false)
menuRef.current
?.querySelector<HTMLButtonElement>('button[aria-expanded="true"]')
?.focus()
} else if (
event.target instanceof Node &&
!menuRef.current?.contains(event.target)
) {
setIsOpen(false)
}
}
const viewportWidth = window.innerWidth
const resize = () => {
if (window.innerWidth === viewportWidth) return
setIsOpen(false)
}
document.addEventListener("pointerdown", dismiss)
document.addEventListener("keydown", dismiss)
window.addEventListener("resize", resize)
return () => {
document.removeEventListener("pointerdown", dismiss)
document.removeEventListener("keydown", dismiss)
window.removeEventListener("resize", resize)
}
}, [isOpen])
return (
<MotionConfig
reducedMotion="user"
transition={{ duration: reducedMotion ? 0 : 0.2, ease: "easeOut" }}
>
<header
ref={menuRef}
data-slot="header"
className="pointer-events-none fixed inset-x-0 top-0 z-50 flex items-start justify-center overflow-visible"
>
<motion.div
initial={false}
animate={{ height: isOpen ? 40 + height : 40 }}
className="pointer-events-auto w-full flex-1 border-b border-border/50 bg-card"
/>
<motion.div
initial={false}
animate={{ height: isOpen ? 64 + height : 64 }}
className="pointer-events-auto relative -ml-px w-12 shrink-0"
>
<svg
aria-hidden="true"
className="absolute inset-0 size-full"
width="48"
height="100%"
>
<motion.path
initial={false}
animate={{
d: `M0 0 H48 V${isOpen ? 64 + height : 64} C24 ${isOpen ? 64 + height : 64} 24 ${isOpen ? 40 + height : 40} 0 ${isOpen ? 40 + height : 40} Z`,
}}
className="fill-current text-card"
/>
<motion.path
initial={false}
animate={{
d: `M0 ${isOpen ? 39.5 + height : 39.5} C24 ${isOpen ? 39.5 + height : 39.5} 24 ${isOpen ? 63.5 + height : 63.5} 48 ${isOpen ? 63.5 + height : 63.5}`,
}}
className="stroke-current text-border/50"
fill="none"
strokeWidth="1"
/>
</svg>
</motion.div>
<motion.div
initial={false}
animate={{ height: isOpen ? 64 + height : 64 }}
className="pointer-events-auto relative -ml-px flex w-full max-w-[calc(100%-6rem)] flex-none flex-col overflow-hidden border-b border-border/50 bg-card md:max-w-xl lg:max-w-2xl"
>
<div className="flex h-16 shrink-0 items-center justify-between px-4 sm:px-8">
<div className="flex items-center gap-8">
<Button
aria-label="Home"
variant="ghost"
size="icon-lg"
render={<Link href="#" role="link" />}
nativeButton={false}
>
<Icons.Logo aria-hidden="true" className="size-7" />
</Button>
<nav
aria-label="Main navigation"
className="flex items-center gap-6 max-md:hidden"
>
{["Products", "Solutions", "Pricing"].map((item) => (
<Button
key={item}
variant="ghost"
render={
<Link href={`/${item.toLowerCase()}`} role="link" />
}
nativeButton={false}
>
{item}
</Button>
))}
</nav>
</div>
<div className="flex items-center gap-4">
<Button
variant="ghost"
className="max-sm:hidden"
render={<Link href="#sign-in" role="link" />}
nativeButton={false}
>
Log in
</Button>
<Button
onClick={() => setIsOpen(false)}
className="max-md:hidden"
render={<Link href="#get-started" role="link" />}
nativeButton={false}
>
Get started
</Button>
<Button
type="button"
aria-label={isOpen ? "Close menu" : "Open menu"}
aria-expanded={isOpen}
aria-controls={isOpen ? menuId : undefined}
onClick={() => setIsOpen(!isOpen)}
variant="secondary"
size="icon-lg"
className="md:hidden"
>
{isOpen ? (
<XIcon aria-hidden="true" />
) : (
<svg
aria-hidden="true"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
className="size-4"
>
<path d="M5 9h14M5 15h14" />
</svg>
)}
</Button>
</div>
</div>
<div className="absolute inset-x-0 top-16 w-full md:hidden">
<div ref={measureRef}>
<AnimatePresence>
{isOpen && (
<motion.div
data-slot="navigation-menu-popup"
initial={{ opacity: 0, y: reducedMotion ? 0 : -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: reducedMotion ? 0 : -10 }}
id={menuId}
className="no-scrollbar flex max-h-[calc(100dvh-8rem)] flex-col overflow-y-auto px-4 pt-2 pb-6"
>
<nav className="flex flex-col gap-5">
<div className="flex flex-col gap-4">
{["Products", "Solutions", "Pricing"].map((item) => (
<Button
key={item}
onClick={() => setIsOpen(false)}
variant="ghost"
render={
<Link
href={`/${item.toLowerCase()}`}
role="link"
/>
}
nativeButton={false}
>
{item}
</Button>
))}
</div>
<div className="h-px w-full bg-border/50" />
<div className="flex flex-col gap-2.5">
<Button
onClick={() => setIsOpen(false)}
variant="secondary"
className="h-10 sm:hidden"
render={<Link href="#sign-in" role="link" />}
nativeButton={false}
>
Log in
</Button>
<Button
onClick={() => setIsOpen(false)}
className="h-10 w-full"
render={<Link href="#get-started" role="link" />}
nativeButton={false}
>
Get started
</Button>
</div>
</nav>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
</motion.div>
<motion.div
initial={false}
animate={{ height: isOpen ? 64 + height : 64 }}
className="pointer-events-auto relative -ml-px w-12 shrink-0"
>
<svg
aria-hidden="true"
className="absolute inset-0 size-full"
width="48"
height="100%"
>
<motion.path
initial={false}
animate={{
d: `M0 0 H48 V${isOpen ? 40 + height : 40} C24 ${isOpen ? 40 + height : 40} 24 ${isOpen ? 64 + height : 64} 0 ${isOpen ? 64 + height : 64} Z`,
}}
className="fill-current text-card"
/>
<motion.path
initial={false}
animate={{
d: `M0 ${isOpen ? 63.5 + height : 63.5} C24 ${isOpen ? 63.5 + height : 63.5} 24 ${isOpen ? 39.5 + height : 39.5} 48 ${isOpen ? 39.5 + height : 39.5}`,
}}
className="stroke-current text-border/50"
fill="none"
strokeWidth="1"
/>
</svg>
</motion.div>
<motion.div
initial={false}
animate={{ height: isOpen ? 40 + height : 40 }}
className="pointer-events-auto w-full flex-1 border-b border-border/50 bg-card"
/>
</header>
</MotionConfig>
)
}

























