1import { useRef } from 'react'
2import Link from 'next/link'
3import { useRouter } from 'next/router'
4import clsx from 'clsx'
5import { AnimatePresence, motion, useIsPresent } from 'framer-motion'
6
7import { Button } from '@/components/Button'
8import { useIsInsideMobileNavigation } from '@/components/MobileNavigation'
9import { useSectionStore } from '@/components/SectionProvider'
10import { Tag } from '@/components/Tag'
11import { remToPx } from '@/lib/remToPx'
12
13function useInitialValue(value, condition = true) {
14 let initialValue = useRef(value).current
15 return condition ? initialValue : value
16}
17
18function TopLevelNavItem({ href, children }) {
19 return (
20 <li className="md:hidden">
21 <Link
22 href={href}
23 className="block py-1 text-sm text-zinc-600 transition hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white"
24 >
25 {children}
26 </Link>
27 </li>
28 )
29}
30
31function NavLink({ href, tag, active, isAnchorLink = false, children }) {
32 return (
33 <Link
34 href={href}
35 aria-current={active ? 'page' : undefined}
36 className={clsx(
37 'flex justify-between gap-2 py-1 pr-3 text-sm transition',
38 isAnchorLink ? 'pl-7' : 'pl-4',
39 active
40 ? 'text-zinc-900 dark:text-white'
41 : 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white'
42 )}
43 >
44 <span className="truncate">{children}</span>
45 {tag && (
46 <Tag variant="small" color="zinc">
47 {tag}
48 </Tag>
49 )}
50 </Link>
51 )
52}
53
54function VisibleSectionHighlight({ group, pathname }) {
55 let [sections, visibleSections] = useInitialValue(
56 [
57 useSectionStore((s) => s.sections),
58 useSectionStore((s) => s.visibleSections),
59 ],
60 useIsInsideMobileNavigation()
61 )
62
63 let isPresent = useIsPresent()
64 let firstVisibleSectionIndex = Math.max(
65 0,
66 [{ id: '_top' }, ...sections].findIndex(
67 (section) => section.id === visibleSections[0]
68 )
69 )
70 let itemHeight = remToPx(2)
71 let height = isPresent
72 ? Math.max(1, visibleSections.length) * itemHeight
73 : itemHeight
74 let top =
75 group.links.findIndex((link) => link.href === pathname) * itemHeight +
76 firstVisibleSectionIndex * itemHeight
77
78 return (
79 <motion.div
80 layout
81 initial={{ opacity: 0 }}
82 animate={{ opacity: 1, transition: { delay: 0.2 } }}
83 exit={{ opacity: 0 }}
84 className="absolute inset-x-0 top-0 bg-zinc-800/2.5 will-change-transform dark:bg-white/2.5"
85 style={{ borderRadius: 8, height, top }}
86 />
87 )
88}
89
90function ActivePageMarker({ group, pathname }) {
91 let itemHeight = remToPx(2)
92 let offset = remToPx(0.25)
93 let activePageIndex = group.links.findIndex((link) => link.href === pathname)
94 let top = offset + activePageIndex * itemHeight
95
96 return (
97 <motion.div
98 layout
99 className="absolute left-2 h-6 w-px bg-emerald-500"
100 initial={{ opacity: 0 }}
101 animate={{ opacity: 1, transition: { delay: 0.2 } }}
102 exit={{ opacity: 0 }}
103 style={{ top }}
104 />
105 )
106}
107
108function NavigationGroup({ group, className }) {
109 // If this is the mobile navigation then we always render the initial
110 // state, so that the state does not change during the close animation.
111 // The state will still update when we re-open (re-render) the navigation.
112 let isInsideMobileNavigation = useIsInsideMobileNavigation()
113 let [router, sections] = useInitialValue(
114 [useRouter(), useSectionStore((s) => s.sections)],
115 isInsideMobileNavigation
116 )
117
118 let isActiveGroup =
119 group.links.findIndex((link) => link.href === router.pathname) !== -1
120
121 return (
122 <li className={clsx('relative mt-6', className)}>
123 <motion.h2
124 layout="position"
125 className="text-xs font-semibold text-zinc-900 dark:text-white"
126 >
127 {group.title}
128 </motion.h2>
129 <div className="relative mt-3 pl-2">
130 <AnimatePresence initial={!isInsideMobileNavigation}>
131 {isActiveGroup && (
132 <VisibleSectionHighlight group={group} pathname={router.pathname} />
133 )}
134 </AnimatePresence>
135 <motion.div
136 layout
137 className="absolute inset-y-0 left-2 w-px bg-zinc-900/10 dark:bg-white/5"
138 />
139 <AnimatePresence initial={false}>
140 {isActiveGroup && (
141 <ActivePageMarker group={group} pathname={router.pathname} />
142 )}
143 </AnimatePresence>
144 <ul role="list" className="border-l border-transparent">
145 {group.links.map((link) => (
146 <motion.li key={link.href} layout="position" className="relative">
147 <NavLink href={link.href} active={link.href === router.pathname}>
148 {link.title}
149 </NavLink>
150 <AnimatePresence mode="popLayout" initial={false}>
151 {link.href === router.pathname && sections.length > 0 && (
152 <motion.ul
153 role="list"
154 initial={{ opacity: 0 }}
155 animate={{
156 opacity: 1,
157 transition: { delay: 0.1 },
158 }}
159 exit={{
160 opacity: 0,
161 transition: { duration: 0.15 },
162 }}
163 >
164 {sections.map((section) => (
165 <li key={section.id}>
166 <NavLink
167 href={`${link.href}#${section.id}`}
168 tag={section.tag}
169 isAnchorLink
170 >
171 {section.title}
172 </NavLink>
173 </li>
174 ))}
175 </motion.ul>
176 )}
177 </AnimatePresence>
178 </motion.li>
179 ))}
180 </ul>
181 </div>
182 </li>
183 )
184}
185
186export const navigation = [
187 {
188 title: 'Onboarding',
189 links: [
190 { title: 'Tools', href: '/tools' },
191 { title: 'Environment Setup', href: '/setup' },
192 { title: 'Backend', href: '/backend' },
193 ],
194 },
195 {
196 title: 'Zed',
197 links: [{ title: 'Release Process', href: '/release-process' }],
198 },
199]
200
201export function Navigation(props) {
202 return (
203 <nav {...props}>
204 <ul role="list">
205 <TopLevelNavItem href="/">API</TopLevelNavItem>
206 <TopLevelNavItem href="#">Documentation</TopLevelNavItem>
207 <TopLevelNavItem href="#">Support</TopLevelNavItem>
208 {navigation.map((group, groupIndex) => (
209 <NavigationGroup
210 key={group.title}
211 group={group}
212 className={groupIndex === 0 && 'md:mt-0'}
213 />
214 ))}
215 <li className="sticky bottom-0 z-10 mt-6 min-[416px]:hidden">
216 <Button href="#" variant="filled" className="w-full">
217 Sign in
218 </Button>
219 </li>
220 </ul>
221 </nav>
222 )
223}