| 1 | "use client"; |
| 2 | |
| 3 | import { useRouter } from "next/navigation"; |
| 4 | import { Command } from "cmdk"; |
| 5 | import { motion, AnimatePresence } from "framer-motion"; |
| 6 | import { NAV } from "./nav"; |
| 7 | import { useUiStore } from "@/stores/ui"; |
| 8 | import { springs } from "@/lib/motion"; |
| 9 | |
| 10 | export function CommandPalette() { |
| 11 | const router = useRouter(); |
| 12 | const open = useUiStore((s) => s.commandOpen); |
| 13 | const setOpen = useUiStore((s) => s.setCommandOpen); |
| 14 | |
| 15 | const go = (href: string) => { |
| 16 | setOpen(false); |
| 17 | router.push(href); |
| 18 | }; |
| 19 | |
| 20 | return ( |
| 21 | <AnimatePresence> |
| 22 | {open ? ( |
| 23 | <div className="fixed inset-0 z-[var(--z-modal)] flex items-start justify-center p-4 pt-[14vh]"> |
| 24 | <motion.div |
| 25 | className="absolute inset-0 bg-black/55 backdrop-blur-sm" |
| 26 | initial={{ opacity: 0 }} |
| 27 | animate={{ opacity: 1 }} |
| 28 | exit={{ opacity: 0 }} |
| 29 | onClick={() => setOpen(false)} |
| 30 | aria-hidden |
| 31 | /> |
| 32 | <motion.div |
| 33 | initial={{ opacity: 0, scale: 0.97, y: -8 }} |
| 34 | animate={{ opacity: 1, scale: 1, y: 0, transition: springs.sheet }} |
| 35 | exit={{ opacity: 0, scale: 0.97, y: -8, transition: { duration: 0.15 } }} |
| 36 | className="ring-frame relative w-full max-w-lg overflow-hidden rounded-[var(--radius-lg)] border border-[var(--color-border-default)] bg-[var(--color-bg-overlay)] shadow-[var(--shadow-pop)]" |
| 37 | > |
| 38 | <Command label="Command palette" className="w-full"> |
| 39 | <div className="border-b border-[var(--color-border-subtle)] px-4"> |
| 40 | <Command.Input |
| 41 | autoFocus |
| 42 | placeholder="Jump to a page or search…" |
| 43 | className="h-12 w-full bg-transparent text-sm text-[var(--color-fg-primary)] placeholder:text-[var(--color-fg-tertiary)] focus:outline-none" |
| 44 | /> |
| 45 | </div> |
| 46 | <Command.List className="max-h-[320px] overflow-y-auto p-2"> |
| 47 | <Command.Empty className="px-3 py-6 text-center text-sm text-[var(--color-fg-tertiary)]"> |
| 48 | No results. |
| 49 | </Command.Empty> |
| 50 | <Command.Group |
| 51 | heading="Navigate" |
| 52 | className="px-1 text-[10px] font-medium uppercase tracking-wider text-[var(--color-fg-tertiary)] [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5" |
| 53 | > |
| 54 | {NAV.filter((n) => n.ready).map((item) => ( |
| 55 | <Command.Item |
| 56 | key={item.href} |
| 57 | value={item.label} |
| 58 | onSelect={() => go(item.href)} |
| 59 | className="flex cursor-pointer items-center gap-3 rounded-[var(--radius)] px-3 py-2 text-sm text-[var(--color-fg-secondary)] aria-selected:bg-[var(--color-accent-teal-soft)] aria-selected:text-[var(--color-fg-primary)]" |
| 60 | > |
| 61 | <item.icon className="h-4 w-4" aria-hidden /> |
| 62 | {item.label} |
| 63 | </Command.Item> |
| 64 | ))} |
| 65 | </Command.Group> |
| 66 | </Command.List> |
| 67 | </Command> |
| 68 | </motion.div> |
| 69 | </div> |
| 70 | ) : null} |
| 71 | </AnimatePresence> |
| 72 | ); |
| 73 | } |