{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "expandable-menu-navbar", "type": "registry:ui", "title": "Expandable Menu Navbar", "description": "A monochrome floating navbar that springs open into a stacked menu of grouped, metadata-rich nav items with a status footer and keyboard shortcut.", "dependencies": [ "motion" ], "files": [ { "path": "registry/ruixenui/expandable-menu-navbar.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion } from \"motion/react\";\n\ninterface NavItem {\n label: string;\n meta?: string;\n href?: string;\n external?: boolean;\n}\n\ninterface NavSection {\n title?: string;\n items: NavItem[];\n}\n\ninterface ExpandableMenuNavbarProps {\n brand?: React.ReactNode;\n brandLabel?: string;\n sections?: NavSection[];\n status?: string;\n version?: string;\n shortcut?: string;\n height?: number | string;\n className?: string;\n}\n\nconst DEFAULT_SECTIONS: NavSection[] = [\n {\n items: [\n { label: \"Index\", meta: \"01\" },\n { label: \"Work\", meta: \"24\" },\n { label: \"Notes\", meta: \"12\" },\n { label: \"Lab\", meta: \"08\" },\n ],\n },\n {\n title: \"Resources\",\n items: [\n { label: \"Changelog\", meta: \"v2.4\" },\n { label: \"Library\", meta: \"126\" },\n { label: \"Tools\", meta: \"07\" },\n { label: \"Press kit\", meta: \"↗\", external: true },\n ],\n },\n {\n title: \"Connect\",\n items: [\n { label: \"Contact\" },\n { label: \"Newsletter\", meta: \"monthly\" },\n { label: \"Twitter\", meta: \"↗\", external: true },\n { label: \"GitHub\", meta: \"↗\", external: true },\n ],\n },\n];\n\nfunction StackedMark() {\n return (\n \n \n \n \n );\n}\n\nconst SPRING = { type: \"spring\", stiffness: 280, damping: 32 } as const;\nconst ITEM_EASE = [0.22, 0.61, 0.36, 1] as const;\nconst COLLAPSED_HEIGHT = 56;\nconst WRAPPER_PADDING = 32;\n\nexport default function ExpandableMenuNavbar({\n brand,\n brandLabel = \"Index\",\n sections = DEFAULT_SECTIONS,\n status = \"Available\",\n version = \"v2.4.1\",\n shortcut = \"M\",\n height = 560,\n className,\n}: ExpandableMenuNavbarProps) {\n const [open, setOpen] = React.useState(false);\n const containerRef = React.useRef(null);\n const scrollRef = React.useRef(null);\n const [measuredOpenHeight, setMeasuredOpenHeight] = React.useState(0);\n\n React.useEffect(() => {\n const el = containerRef.current;\n if (!el) return;\n const measure = () => {\n setMeasuredOpenHeight(Math.max(0, el.clientHeight - WRAPPER_PADDING));\n };\n measure();\n const ro = new ResizeObserver(measure);\n ro.observe(el);\n return () => ro.disconnect();\n }, []);\n\n React.useEffect(() => {\n if (!open) return;\n const handler = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") setOpen(false);\n };\n window.addEventListener(\"keydown\", handler);\n return () => window.removeEventListener(\"keydown\", handler);\n }, [open]);\n\n React.useEffect(() => {\n if (open && scrollRef.current) {\n scrollRef.current.scrollTop = 0;\n }\n }, [open]);\n\n const fallbackOpenHeight =\n (typeof height === \"number\" ? height : 560) - WRAPPER_PADDING;\n const openHeight =\n measuredOpenHeight > 0 ? measuredOpenHeight : fallbackOpenHeight;\n\n const flatIndex = React.useMemo(() => {\n const offsets: number[] = [];\n let acc = 0;\n for (const s of sections) {\n offsets.push(acc);\n acc += s.items.length;\n }\n return offsets;\n }, [sections]);\n\n return (\n \n \n {open && (\n setOpen(false)}\n />\n )}\n \n\n
\n \n setOpen((v) => !v)}\n aria-expanded={open}\n aria-controls=\"expandable-menu-content\"\n className=\"flex h-14 shrink-0 items-center justify-between gap-3 px-4 text-foreground outline-none focus-visible:ring-1 focus-visible:ring-foreground/20\"\n >\n \n {brand ?? }\n \n {brandLabel}\n \n \n \n \n {shortcut}\n \n \n {open ? \"Close\" : \"Menu\"}\n \n \n \n \n \n \n\n \n {open && (\n \n
\n {sections.map((section, sIdx) => (\n
\n {section.title && (\n \n {section.title}\n \n )}\n\n {section.items.map((item, iIdx) => {\n const idx = flatIndex[sIdx] + iIdx;\n return (\n \n \n {item.label}\n \n \n {item.meta && {item.meta}}\n \n \n \n \n \n );\n })}\n
\n ))}\n\n \n \n \n \n \n \n {status}\n \n {version}\n \n
\n \n )}\n
\n \n
\n \n );\n}\n", "type": "registry:ui", "target": "components/ruixen/expandable-menu-navbar.tsx" } ] }