{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"event-calendar-nav","type":"registry:ui","title":"Composable navigation - Today, prev/next, period title, view switcher dropdown, and a free toolbar slot.","description":"Composable navigation - Today, prev/next, period title, view switcher dropdown, and a free toolbar slot.","dependencies":["date-fns","radix-ui"],"registryDependencies":["button","calendar","dropdown-menu","@neui/event-calendar","@neui/event-calendar-lib","@neui/event-calendar-types","popover","tooltip"],"files":[{"path":"event-calendar-nav.tsx","type":"registry:ui","content":"// Title: Event Calendar Nav\n// Description: Composable navigation - Today, prev/next, period title, view switcher dropdown, and a free toolbar slot.\n\n\"use client\"\n\nimport {\n useState,\n type ButtonHTMLAttributes,\n type HTMLAttributes,\n type ReactNode,\n} from \"react\"\nimport {\n useEventCalendarNavigation,\n useEventCalendarSettings,\n useEventCalendarView,\n useEventCalendarViewConfig,\n} from \"@/components/neui/event-calendar/event-calendar\"\nimport { toZoned } from \"@/components/neui/event-calendar/event-calendar-lib\"\nimport type { CalendarView } from \"@/components/neui/event-calendar/event-calendar-types\"\nimport { addDays, format } from \"date-fns\"\nimport { Slot } from \"radix-ui\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport { Calendar } from \"@/components/ui/calendar\"\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuGroup,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\"\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\"\nimport {\n Tooltip,\n TooltipContent,\n TooltipProvider,\n TooltipTrigger,\n} from \"@/components/ui/tooltip\"\nimport { IconPlaceholder } from \"@/app/(create)/components/icon-placeholder\"\n\n/** Configured nav button variant/size (viewConfig.navButtonVariant/Size)\n * plus the shared classNames.navButton hook, merged on every nav button. */\nfunction useNavButtonProps(): {\n variant: \"ghost\" | \"outline\" | \"secondary\" | \"default\"\n size: \"sm\" | \"default\"\n iconSize: \"icon-sm\" | \"icon\"\n className: string | undefined\n} {\n const viewConfig = useEventCalendarViewConfig()\n return {\n variant: viewConfig.navButtonVariant,\n size: viewConfig.navButtonSize,\n iconSize: viewConfig.navButtonSize === \"sm\" ? \"icon-sm\" : \"icon\",\n className: viewConfig.classNames?.navButton,\n }\n}\n\n/** Resolved nav tooltip policy (viewConfig.navTooltips + classNames.navTooltip). */\nfunction useNavTooltipConfig(): {\n disabled: boolean\n side: \"top\" | \"bottom\" | \"left\" | \"right\"\n delay: number\n closeDelay: number\n timeout: number\n className: string | undefined\n} {\n const viewConfig = useEventCalendarViewConfig()\n const config =\n viewConfig.navTooltips === false ? undefined : viewConfig.navTooltips\n return {\n disabled: viewConfig.navTooltips === false,\n // the nav sits at the top of the calendar, so tooltips open upward by\n // default (away from the grid); collision flipping still drops them below\n // when there is no room above\n side: config?.side ?? \"top\",\n delay: config?.delay ?? 600,\n closeDelay: config?.closeDelay ?? 0,\n timeout: config?.timeout ?? 300,\n className: viewConfig.classNames?.navTooltip,\n }\n}\n\ntype NavButtonProps = Omit<\n ButtonHTMLAttributes,\n \"children\"\n> & {\n children?: ReactNode\n /**\n * Tooltip policy (the part that usually goes wrong on clickable elements):\n * tooltips appear ONLY on hover or keyboard focus-visible - a pointer click\n * never re-triggers them. Buttons that open overlays (the view switcher)\n * use a hover-only tooltip that is force-closed while the overlay is up and\n * ignores focus, so nothing flashes when focus returns after selection.\n * Icon-only buttons default to their accessible label; Today defaults to\n * the actual current date (info the label doesn't carry). Pass null to\n * disable one, or any node to override; viewConfig.navTooltips=false turns\n * them all off (its object form tunes side/delay/closeDelay/timeout).\n */\n tooltip?: ReactNode | null\n asChild?: boolean\n}\n\n/** Hover/focus-visible tooltip wrapper; renders the bare button when disabled\n * (per-button content=null or viewConfig.navTooltips=false). */\nfunction NavTooltip({\n content,\n children,\n}: {\n content: ReactNode | null\n children: React.ReactElement\n}) {\n const tooltips = useNavTooltipConfig()\n if (tooltips.disabled || content === null || content === undefined)\n return children\n return (\n \n {children}\n \n {content}\n \n \n )\n}\n\nfunction EventCalendarNavToday({\n className,\n asChild = false,\n children,\n tooltip,\n ...props\n}: NavButtonProps) {\n const { today, isToday } = useEventCalendarNavigation()\n const settings = useEventCalendarSettings()\n const nav = useNavButtonProps()\n // display-zone \"today\", like every other today derivation in the calendar\n // (a system-zone new Date() can name a different day than Today opens)\n const defaultTooltip = format(\n toZoned(new Date(), settings.timeZone),\n settings.i18n.formats.dayTitle,\n { locale: settings.locale }\n )\n return (\n \n \n {children ?? settings.i18n.labels.today}\n \n \n )\n}\n\nfunction EventCalendarNavPrev({\n className,\n asChild = false,\n children,\n tooltip,\n ...props\n}: NavButtonProps) {\n const { prev } = useEventCalendarNavigation()\n const settings = useEventCalendarSettings()\n const nav = useNavButtonProps()\n return (\n \n \n {children ?? (\n \n )}\n \n \n )\n}\n\nfunction EventCalendarNavNext({\n className,\n asChild = false,\n children,\n tooltip,\n ...props\n}: NavButtonProps) {\n const { next } = useEventCalendarNavigation()\n const settings = useEventCalendarSettings()\n const nav = useNavButtonProps()\n return (\n \n \n {children ?? (\n \n )}\n \n \n )\n}\n\ninterface EventCalendarTitleProps extends HTMLAttributes {\n format?: (ctx: { title: string }) => ReactNode\n asChild?: boolean\n}\n\nfunction EventCalendarTitle({\n className,\n asChild = false,\n format: formatTitle,\n children,\n ...props\n}: EventCalendarTitleProps) {\n const { title } = useEventCalendarNavigation()\n const viewConfig = useEventCalendarViewConfig()\n const Comp = asChild ? Slot.Root : \"div\"\n return (\n \n {children ?? formatTitle?.({ title }) ?? title}\n \n )\n}\n\ninterface EventCalendarViewSwitcherProps extends Omit<\n ButtonHTMLAttributes,\n \"children\"\n> {\n children?: ReactNode\n /** Hover/focus-visible hint; defaults to the \"Select view\" label. Pass\n * null to disable (overlay-opener policy). */\n tooltip?: ReactNode | null\n asChild?: boolean\n}\n\nfunction EventCalendarViewSwitcher({\n className,\n asChild = false,\n children,\n tooltip,\n ...props\n}: EventCalendarViewSwitcherProps) {\n const { view, dayCount, availableViews, setView } = useEventCalendarView()\n const settings = useEventCalendarSettings()\n const viewConfig = useEventCalendarViewConfig()\n const nav = useNavButtonProps()\n const tooltips = useNavTooltipConfig()\n const labels = settings.i18n.labels\n // Controlled open: selecting a view swaps the whole content subtree in the\n // same click, so closing must not depend on the menu's internal handler.\n const [open, setOpen] = useState(false)\n // Hover-only tooltip: when the menu closes, Radix focuses the trigger\n // again and a focus-opened tooltip would flash - ignore focus opens.\n const [tipOpen, setTipOpen] = useState(false)\n\n const selectView = (v: CalendarView, opts?: { dayCount?: number }) => {\n setOpen(false)\n setView(v, opts)\n }\n\n const viewLabel = (v: CalendarView) =>\n v === \"days\"\n ? settings.i18n.viewNames.days(dayCount)\n : settings.i18n.viewNames[v]\n\n return (\n {\n setOpen(next)\n if (next) setTipOpen(false)\n }}\n >\n {/* Tooltip on an overlay-opener: hover-only (focus opens ignored) and\n force-closed while the menu is up, so it never lingers or flashes\n when focus returns on close. Inherits the nav TooltipProvider's\n delayDuration/skipDelayDuration. */}\n \n \n {/* opens are hover-only; the trigger-focus open that follows a\n menu close is ignored (prevented before Radix opens), closes\n always land */}\n event.preventDefault()}>\n \n {children ?? (\n <>\n {viewLabel(view)}\n \n \n )}\n \n \n \n {tipOpen && !open && tooltip !== null && !tooltips.disabled && (\n \n {tooltip ?? labels.selectView}\n \n )}\n \n \n {/* Keep the label inside the group so it stays associated with its items */}\n \n \n {settings.i18n.labels.selectView}\n \n {availableViews.map((v) =>\n v === \"days\" ? (\n viewConfig.dayCountPresets.map((count) => (\n selectView(\"days\", { dayCount: count })}\n >\n {settings.i18n.viewNames.days(count)}\n {/* hint derived from the preset itself, not i18n's default */}\n {viewConfig.enableShortcuts && (\n \n {count}\n \n )}\n \n ))\n ) : (\n selectView(v)}\n >\n {viewLabel(v)}\n {viewConfig.enableShortcuts && (\n \n {labels.viewShortcuts[v]}\n \n )}\n \n )\n )}\n \n \n \n )\n}\n\n/** Outline key badge; theme-token only, so it adapts to every style. */\nfunction EventCalendarViewShortcut({ children }: { children: ReactNode }) {\n const viewConfig = useEventCalendarViewConfig()\n return (\n \n {children}\n \n )\n}\n\ninterface EventCalendarDatePickerProps extends Omit<\n ButtonHTMLAttributes,\n \"children\"\n> {\n children?: ReactNode\n /** \"auto\" (default) resolves per view: range for week/N-days/agenda. */\n mode?: \"auto\" | \"single\" | \"range\"\n /** Hover/focus-visible hint; defaults to null - no tooltip, because the\n * button opens an overlay (see the NavButtonProps tooltip policy). */\n tooltip?: ReactNode | null\n asChild?: boolean\n}\n\n/** Views whose period reads better as a highlighted range. */\nconst RANGE_VIEWS: CalendarView[] = [\"week\", \"days\", \"agenda\"]\n\n/**\n * Optional go-to-date picker (shadcn Calendar in a popover), view-aware:\n * week/N-days/agenda highlight the whole active range (any click re-anchors\n * the period), other views select a single date. Not part of the default\n * nav - compose it yourself (or any external picker driving\n * useEventCalendarNavigation().goTo). No tooltip by default: it opens an\n * overlay (see the NavButtonProps tooltip policy); pass `tooltip` to opt in.\n */\nfunction EventCalendarDatePicker({\n className,\n asChild = false,\n children,\n tooltip = null,\n mode,\n ...props\n}: EventCalendarDatePickerProps) {\n const { date, goTo, activeRange } = useEventCalendarNavigation()\n const { view } = useEventCalendarView()\n const settings = useEventCalendarSettings()\n const viewConfig = useEventCalendarViewConfig()\n const nav = useNavButtonProps()\n const [open, setOpen] = useState(false)\n const zoned = toZoned(date, settings.timeZone)\n\n const configured = mode ?? \"auto\"\n const resolved =\n configured === \"auto\"\n ? RANGE_VIEWS.includes(view)\n ? \"range\"\n : \"single\"\n : configured\n\n const pick = (next: Date | undefined) => {\n if (!next) return\n goTo(next)\n setOpen(false)\n }\n\n return (\n \n \n \n \n {children ?? (\n \n )}\n \n \n \n \n {resolved === \"range\" ? (\n \n ) : (\n \n )}\n \n \n )\n}\n\ninterface EventCalendarToolbarProps extends HTMLAttributes {\n asChild?: boolean\n}\n\n/** Free slot for consumer toolbar buttons; pure layout shell. */\nfunction EventCalendarToolbar({\n className,\n asChild = false,\n children,\n ...props\n}: EventCalendarToolbarProps) {\n const viewConfig = useEventCalendarViewConfig()\n const Comp = asChild ? Slot.Root : \"div\"\n return (\n \n {children}\n \n )\n}\n\ninterface EventCalendarNavProps extends HTMLAttributes {\n /**\n * Render the view switcher in the composed layout. Turn off when the\n * calendar ships with a fixed view (e.g. a month-only embed) and users\n * should not be able to change it.\n * @default true\n */\n showViewSwitcher?: boolean\n asChild?: boolean\n}\n\n/**\n * Default composed nav: Today, prev/next, title, spacer, view switcher.\n * Pass children to use it as a pure layout shell instead.\n */\nfunction EventCalendarNav({\n className,\n asChild = false,\n children,\n showViewSwitcher = true,\n ...props\n}: EventCalendarNavProps) {\n const viewConfig = useEventCalendarViewConfig()\n const tooltips = useNavTooltipConfig()\n const Comp = asChild ? Slot.Root : \"div\"\n return (\n \n {children ?? (\n // Shared provider: first tooltip waits, moving between buttons is instant\n \n \n {showViewSwitcher && }\n
\n \n \n
\n {/* ms-3 sets the title apart from the tight control cluster so the\n period reads as its own group, not another button */}\n \n
\n \n )}\n \n )\n}\n\nexport {\n EventCalendarDatePicker,\n EventCalendarNav,\n EventCalendarNavNext,\n EventCalendarNavPrev,\n EventCalendarNavToday,\n EventCalendarTitle,\n EventCalendarToolbar,\n EventCalendarViewSwitcher,\n}\nexport type {\n EventCalendarNavProps,\n EventCalendarTitleProps,\n EventCalendarToolbarProps,\n EventCalendarViewSwitcherProps,\n}","target":"components/neui/event-calendar/event-calendar-nav.tsx"}]}