"use client"; import { ArrowRight, CalendarDays, ChevronDown, ChevronLeft, ChevronRight, } from "lucide-react"; import { AnimatePresence, type HTMLMotionProps, motion, useIsPresent, } from "motion/react"; import { type ComponentPropsWithRef, type ComponentPropsWithoutRef, type KeyboardEvent, type ReactNode, useCallback, useLayoutEffect, useRef, useState, } from "react"; import { EASE_OUT, SPRING_LAYOUT, SPRING_PRESS } from "@/lib/ease"; import { cn } from "@/lib/utils"; import { MorphPopover, MorphPopoverContent, type MorphPopoverContentProps, MorphPopoverTrigger, } from "@/components/motion/popover-morph"; import { DateRangePickerContext, useDateRangePickerContext, } from "./date-range-picker/context"; import { parse } from "./date-range-picker/date-utils"; import { useDateRangePickerController } from "./date-range-picker/use-date-range-picker"; import type { DateRange, DateRangePickerProps, } from "./date-range-picker/types"; export type { DateRange, DateRangePreset, DateRangePickerProps, } from "./date-range-picker/types"; const monthVariants = { enter: ({ direction, reduce }: { direction: number; reduce: boolean }) => ({ opacity: 0, x: reduce ? 0 : direction * 12, }), exit: ({ direction, reduce }: { direction: number; reduce: boolean }) => ({ opacity: 0, x: reduce ? 0 : direction * -8, transition: { duration: 0.12, ease: EASE_OUT }, }), }; const iconClass = "inline-flex size-8 shrink-0 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-30"; /** State shared by built-in parts and custom date range controls. */ export function useDateRangePicker() { const ctx = useDateRangePickerContext("useDateRangePicker"); return { value: ctx.value, setValue: ctx.change, open: ctx.open, setOpen: ctx.setOpen, month: ctx.month, view: ctx.view, label: ctx.label, locale: ctx.locale, disabled: ctx.disabled, selectDate: ctx.select, clear: ctx.clear, goToMonth: ctx.goToMonth, previous: () => !ctx.previousDisabled && ctx.page(-1), next: () => !ctx.nextDisabled && ctx.page(1), previousDisabled: ctx.previousDisabled, nextDisabled: ctx.nextDisabled, chooseMonth: () => ctx.openChoices("months"), chooseYear: () => ctx.openChoices("years"), }; } /** Range state and popover root. Without children, renders the inline calendar. */ export function DateRangePicker({ children, className, ...props }: DateRangePickerProps) { const ctx = useDateRangePickerController(props); return ( {children === undefined ? ( ) : ( children )} ); } export interface DateRangePickerCalendarProps extends ComponentPropsWithoutRef<"section"> { showSummary?: boolean; } /** Calendar surface; arrange or omit its summary, header, grid and footer. */ export function DateRangePickerCalendar({ children, className, showSummary, ...props }: DateRangePickerCalendarProps) { const ctx = useDateRangePickerContext("DateRangePickerCalendar"); return (

{ctx.monthFormat.format(parse(ctx.month))}

{children === undefined ? ( <> {(showSummary ?? ctx.showSummary) && } ) : ( children )}

Use arrow keys to navigate dates, Home and End for the week, Page Up and Page Down for months, Shift with Page Up or Page Down for years. Enter or Space selects a date. Use the month and year buttons to jump directly. In the month or year choices, use arrow keys to move and Escape to return.

{ctx.announcement}
); } export type DateRangePickerSummaryProps = Omit< ComponentPropsWithoutRef<"div">, "children" >; export function DateRangePickerSummary({ className, ...props }: DateRangePickerSummaryProps) { const { value, shortFormat } = useDateRangePickerContext( "DateRangePickerSummary", ); return (

Start date

{value ? shortFormat.format(parse(value.from)) : "Select date"}

); } export type DateRangePickerHeaderProps = Omit< ComponentPropsWithoutRef<"div">, "children" >; export function DateRangePickerHeader({ className, ...props }: DateRangePickerHeaderProps) { const { monthTrigger, yearTrigger, view, choicesId, disabled, openChoices, monthNameFormat, month, yearPage, yearPageEnd, year, previousDisabled, nextDisabled, page, } = useDateRangePickerContext("DateRangePickerHeader"); return (
); } export type DateRangePickerGridProps = Omit< ComponentPropsWithoutRef<"div">, "children" >; export function DateRangePickerGrid({ className, onPointerLeave, ...props }: DateRangePickerGridProps) { const { setPreview, view, month, yearPage, yearPageEnd, year, direction, reduce, choicesId, choices, focusChoices, closeChoices, goToMonth, grid, heading, help, active, locale, weeks, shownRange, value, canHover, unavailable, dateFormat, today, select, setCursor, onKey, } = useDateRangePickerContext("DateRangePickerGrid"); return (
{ onPointerLeave?.(event); setPreview(null); }} > {view !== "days" ? ( goToMonth( view === "months" ? id : `${id.padStart(4, "0")}-${month.slice(5, 7)}-01`, ) } /> ) : ( { if (node) { grid.current = node; return () => { if (grid.current === node) grid.current = null; }; } }} // biome-ignore lint/a11y/noNoninteractiveElementToInteractiveRole: WAI calendar grids retain native table structure with an interactive grid role. role="grid" aria-labelledby={heading} aria-describedby={help} tabIndex={active === null ? 0 : undefined} className="w-full table-fixed select-none border-separate border-spacing-x-0 border-spacing-y-1" > {Array.from({ length: 7 }, (_, index) => { const day = new Intl.DateTimeFormat(locale, { weekday: "long", timeZone: "UTC", }).format(Date.UTC(2026, 0, 4 + index)); return ( ); })} {weeks.map(({ start, dates: week }) => ( {week.map((date, dayIndex) => { const inRange = Boolean( date && shownRange && date >= shownRange.from && date <= (shownRange.to ?? shownRange.from), ); const endpoint = Boolean( date && value && (date === value.from || date === value.to), ); return ( // biome-ignore lint/a11y/useFocusableInteractive: Calendar cells delegate focus to the contained day button. ); })} ))}
{new Intl.DateTimeFormat(locale, { weekday: "short", timeZone: "UTC", }).format(Date.UTC(2026, 0, 4 + index))}
= value.from && date <= (value.to ?? value.from), ) : undefined } onPointerEnter={(event) => { if ( canHover && event.pointerType !== "touch" && date && !unavailable(date) && value && !value.to ) setPreview(date); }} className={cn( "relative h-8 p-0 text-center", inRange && (dayIndex === 0 || date === shownRange?.from) && "rounded-l-full", inRange && (dayIndex === 6 || date === shownRange?.to || !shownRange?.to) && "rounded-r-full", )} > {date && (
)}
); } export type DateRangePickerSelectionProps = ComponentPropsWithoutRef<"p">; export function DateRangePickerSelection({ children, className, ...props }: DateRangePickerSelectionProps) { const { value, dayCount } = useDateRangePickerContext( "DateRangePickerSelection", ); return (

{children ?? (dayCount ? `${dayCount} ${dayCount === 1 ? "day" : "days"} selected` : value ? "Choose an end date" : "Choose a start date")}

); } export type DateRangePickerClearProps = ComponentPropsWithRef<"button">; export function DateRangePickerClear({ children = "Clear", className, onClick, disabled, ...props }: DateRangePickerClearProps) { const ctx = useDateRangePickerContext("DateRangePickerClear"); return ( ); } export type DateRangePickerFooterProps = ComponentPropsWithoutRef<"div">; export function DateRangePickerFooter({ children, className, ...props }: DateRangePickerFooterProps) { return (
{children === undefined ? ( <>
) : ( children )}
); } export type DateRangePickerPresetsProps = ComponentPropsWithoutRef<"div">; export function DateRangePickerPresets({ children, className, ...props }: DateRangePickerPresetsProps) { const ctx = useDateRangePickerContext("DateRangePickerPresets"); if (children === undefined && ctx.presets.length === 0) return null; return (
{children === undefined ? ctx.presets.map((preset) => ( {preset.label} )) : children}
); } export interface DateRangePickerPresetProps extends Omit, "value"> { value: DateRange; /** Accessible preset name and selection announcement. */ label: string; } export function DateRangePickerPreset({ value, label, children, className, disabled, onClick, ...props }: DateRangePickerPresetProps) { const ctx = useDateRangePickerContext("DateRangePickerPreset"); const selected = ctx.value?.from === value.from && ctx.value?.to === value.to; return ( ); } export interface DateRangePickerTriggerProps extends HTMLMotionProps<"button"> { placeholder?: string; } export function DateRangePickerTrigger({ children, placeholder = "Select dates", className, disabled, ref, ...props }: DateRangePickerTriggerProps) { const ctx = useDateRangePickerContext("DateRangePickerTrigger"); const mergedRef = useCallback( (node: HTMLButtonElement | null) => { ctx.trigger.current = node; if (typeof ref === "function") ref(node); else if (ref) ref.current = node; }, [ctx.trigger, ref], ); const format = new Intl.DateTimeFormat(ctx.locale, { month: "short", day: "numeric", timeZone: "UTC", }); const sameYear = ctx.value?.to && ctx.value.from.slice(0, 4) === ctx.value.to.slice(0, 4); const summary = ctx.value ? `${(sameYear ? format : ctx.shortFormat).format(parse(ctx.value.from))} – ${ctx.value.to ? ctx.shortFormat.format(parse(ctx.value.to)) : "Select end"}` : placeholder; return ( {children != null ? ( {children} ) : ( <> {summary} )} ); } export interface DateRangePickerContentProps extends MorphPopoverContentProps { /** Move focus to the active calendar date when opened. Default true. */ autoFocus?: boolean; } function focusCalendarDate(content: HTMLDivElement | null) { const grid = Array.from( content?.querySelectorAll('[role="grid"]') ?? [], ).find((element) => !element.closest("[inert]")); const target = grid?.querySelector('button[tabindex="0"]') ?? grid; target?.focus({ preventScroll: true }); } export function DateRangePickerContent({ children, className, autoFocus = true, onOpenAutoFocus, ...props }: DateRangePickerContentProps) { const ctx = useDateRangePickerContext("DateRangePickerContent"); useLayoutEffect(() => { // A rapid reopen can revive the exiting surface before it unmounts. // That surface already ran its positioned callback, so focus it here too. if (ctx.open && autoFocus) focusCalendarDate(ctx.content.current); }, [ctx.open, ctx.content, autoFocus]); return ( { ctx.content.current = content; if (autoFocus) focusCalendarDate(content); onOpenAutoFocus?.(content); }} > {children} ); } export interface DateRangePickerDropdownProps extends Omit { placeholder?: string; triggerClassName?: string; calendarClassName?: string; } /** Convenience composition of the root, trigger, content and calendar. */ export function DateRangePickerDropdown({ placeholder, triggerClassName, calendarClassName, autoFocus = true, showSummary = false, ...props }: DateRangePickerDropdownProps) { return ( ); } // The departing month's grid must leave the tab order as soon as exit begins. function CalendarPresence({ children }: { children: ReactNode }) { const present = useIsPresent(); return
{children}
; } function CalendarChoices({ id, label, choices, selected, focusOnMount, reduce, onClose, onSelect, }: { id: string; label: string; choices: { id: string; label: string; disabled: boolean }[]; selected: string; focusOnMount: boolean; reduce: boolean; onClose: () => void; onSelect: (id: string) => void; }) { const enabled = choices.filter((choice) => !choice.disabled); const [cursor, setCursor] = useState(selected); const active = enabled.find((choice) => choice.id === cursor)?.id ?? enabled.find((choice) => choice.id === selected)?.id ?? enabled[0]?.id; if (cursor !== null && cursor !== active) setCursor(active ?? null); const root = useRef(null); useLayoutEffect(() => { if (focusOnMount) root.current ?.querySelector('button[tabindex="0"]') ?.focus({ preventScroll: true }); }, [focusOnMount]); const onKey = (event: KeyboardEvent) => { if (event.altKey || event.ctrlKey || event.metaKey) return; const buttons = Array.from( root.current?.querySelectorAll("button") ?? [], ); const index = buttons.indexOf(event.currentTarget); const offset = event.key === "ArrowLeft" ? -1 : event.key === "ArrowRight" ? 1 : event.key === "ArrowUp" ? -3 : event.key === "ArrowDown" ? 3 : null; let target = event.key === "Home" ? 0 : event.key === "End" ? buttons.length - 1 : offset === null ? null : index + offset; if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); onClose(); return; } if (target === null) return; event.preventDefault(); const step = offset === null ? (event.key === "Home" ? 1 : -1) : Math.sign(offset); while (target >= 0 && target < buttons.length && buttons[target].disabled) target += step; buttons[target]?.focus({ preventScroll: true }); }; return (
{choices.map((choice) => ( setCursor(choice.id)} onKeyDown={onKey} onClick={() => onSelect(choice.id)} whileTap={reduce ? undefined : { scale: 0.96 }} transition={SPRING_PRESS} className={cn( "flex h-13 items-center justify-center rounded-xl px-1 text-[13px] tabular-nums transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-25", choice.id === selected ? "bg-foreground font-medium text-background" : "bg-muted/40 hover:bg-muted", )} > {choice.label} ))}
); }