"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"}
End date
{value?.to ? shortFormat.format(parse(value.to)) : "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 (
openChoices("months")}
className={cn(
"inline-flex items-center gap-1.5 rounded-lg px-2 py-1.5 text-[13px] font-semibold transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-40",
view === "years" && "hidden",
)}
>
{monthNameFormat.format(parse(month))}
openChoices("years")}
className="inline-flex items-center gap-1.5 rounded-lg px-2 py-1.5 text-[13px] font-medium tabular-nums text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-40"
>
{view === "years" ? `${yearPage}–${yearPageEnd}` : year}
page(-1)}
>
page(1)}
>
);
}
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 (
{new Intl.DateTimeFormat(locale, {
weekday: "short",
timeZone: "UTC",
}).format(Date.UTC(2026, 0, 4 + index))}
);
})}
{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.
= 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 && (
)}
{date && (
select(date)}
onFocus={() => {
setCursor(date);
if (value && !value.to) setPreview(date);
}}
onKeyDown={(event) => onKey(event, date)}
className={cn(
"relative mx-auto flex h-8 w-full max-w-8 items-center justify-center rounded-full text-xs tabular-nums focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-30",
endpoint
? "bg-foreground font-medium text-background"
: canHover &&
!unavailable(date) &&
!inRange &&
"hover:bg-muted",
date === today &&
!endpoint &&
"font-semibold after:absolute after:bottom-1 after:size-0.75 after:rounded-full after:bg-current",
)}
>
{new Date(parse(date)).getUTCDate()}
)}
);
})}
))}
)}
);
}
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 (
{
onClick?.(event);
if (!event.defaultPrevented) ctx.clear();
}}
>
{children}
);
}
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 (
{
onClick?.(event);
if (!event.defaultPrevented) ctx.selectPreset(value, label);
}}
>
{children ?? label}
);
}
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}
))}
);
}