{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "navigator-dialog", "title": "Navigator Dialog", "description": "An N-level drill-in dialog — navigate into sub-views to edit things, then go back, with directional slide animations.", "dependencies": [ "lucide-react" ], "registryDependencies": [ "dialog", "button" ], "files": [ { "path": "registry/custom-ui/navigator-dialog.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { ChevronLeftIcon, XIcon } from \"lucide-react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport {\n Dialog,\n DialogClose,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from \"@/components/ui/dialog\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface NavigatorRenderProps {\n navigate: (viewId: string) => void;\n back: () => void;\n canGoBack: boolean;\n activeViewId: string;\n stack: readonly string[];\n}\n\nconst NavigatorContext = React.createContext(null);\n\nexport function useNavigator() {\n const context = React.useContext(NavigatorContext);\n\n if (!context) {\n throw new Error(\"useNavigator must be used within a NavigatorDialog\");\n }\n\n return context;\n}\n\nexport interface NavigatorView {\n /** Unique id referenced by `initialView` and `navigate()`. */\n id: string;\n /** Short label centered in the fixed navigation bar. */\n name: string;\n /** Standard shadcn dialog title rendered below the navigation bar. */\n title: React.ReactNode;\n /** Optional standard shadcn dialog description rendered below the title. */\n description?: React.ReactNode;\n /** Body content rendered below the shadcn dialog header. */\n render: (navigator: NavigatorRenderProps) => React.ReactNode;\n /** Optional actions rendered in a shadcn DialogFooter. */\n footer?: (navigator: NavigatorRenderProps) => React.ReactNode;\n}\n\nexport interface NavigatorDialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n views: NavigatorView[];\n /** Id of the view shown first and restored when the dialog closes. */\n initialView: string;\n /** Merged onto `DialogContent` for project-specific sizing. */\n className?: string;\n}\n\nexport function NavigatorDialog({\n open,\n onOpenChange,\n views,\n initialView,\n className,\n}: NavigatorDialogProps) {\n const [stack, setStack] = React.useState([initialView]);\n const [direction, setDirection] = React.useState<\"forward\" | \"back\">(\n \"forward\",\n );\n\n const activeViewId = stack[stack.length - 1];\n const activeView = views.find((view) => view.id === activeViewId);\n const canGoBack = stack.length > 1;\n\n React.useEffect(() => {\n if (!open) {\n setStack([initialView]);\n setDirection(\"forward\");\n }\n }, [open, initialView]);\n\n const navigate = React.useCallback(\n (viewId: string) => {\n if (!views.some((view) => view.id === viewId)) {\n throw new Error(\n `NavigatorDialog: no view registered with id \"${viewId}\"`,\n );\n }\n\n setDirection(\"forward\");\n setStack((prev) => [...prev, viewId]);\n },\n [views],\n );\n\n const back = React.useCallback(() => {\n if (stack.length <= 1) return;\n\n setDirection(\"back\");\n setStack((prev) => prev.slice(0, -1));\n }, [stack.length]);\n\n const contextValue = React.useMemo(\n () => ({ navigate, back, canGoBack, activeViewId, stack }),\n [navigate, back, canGoBack, activeViewId, stack],\n );\n\n if (!activeView) {\n throw new Error(\n `NavigatorDialog: no view registered with id \"${activeViewId}\"`,\n );\n }\n\n const footer = activeView.footer?.(contextValue);\n\n return (\n \n \n \n \n {canGoBack && (\n \n \n \n )}\n
\n {activeView.name}\n
\n \n \n \n \n \n \n\n
\n \n {activeView.title}\n {activeView.description && (\n {activeView.description}\n )}\n \n \n {activeView.render(contextValue)}\n
\n \n\n {footer && (\n {footer}\n )}\n
\n \n
\n );\n}\n", "type": "registry:ui", "target": "components/custom-ui/navigator-dialog.tsx" } ], "type": "registry:ui" }