datetimepicker

PreviousNext

A datetime picker component

Docs
shadix-uicomponent

Preview

Loading preview…
registry/new-york/components/datetimepicker/datetimepicker.tsx
"use client";
import { memo, useCallback, useEffect } from "react";

import { AnimatePresence } from "motion/react";
import type {
    DateRange,
    DateTimePickerProps,
} from "@/registry/new-york/components/datetimepicker/datetimepicker-types";

import { DatePickerView } from "@/registry/new-york/components/datetimepicker/datepicker-view";
import {
    DateTimePickerProvider,
    useDateTimePicker,
} from "@/registry/new-york/components/datetimepicker/datetimepicker-context";
import { sortDateRange } from "@/registry/new-york/components/datetimepicker/datetimepicker-utils";
import { TimePickerView } from "@/registry/new-york/components/datetimepicker/timepicker-view";
import { cn } from "@/shadcn/lib/utils";

const DateTimePickerInner = memo(
    ({
        mode,
        enableTime = false,
        timeFormat = "12h",
        onDateChange,
        onClose,
        minDate,
        maxDate,
        disabled,
        disableInitialAnimation = false,
        className,
        value,
    }: DateTimePickerProps) => {
        const { state, dispatch, selectDate, selectTime } = useDateTimePicker();

        const handleDateSelect = useCallback(
            (date: Date) => {
                if (mode === "single") {
                    selectDate(date);
                    if (!enableTime) {
                        onDateChange(date);
                        onClose?.();
                    } else {
                        dispatch({ type: "SET_VIEW", payload: "time" });
                        selectTime(12, 0);
                    }
                } else if (mode === "range") {
                    if (!state.selectedDate) {
                        // First click - select start date
                        selectDate(date);
                        dispatch({ type: "SET_RANGE_END", payload: true });
                    } else {
                        // Second click - complete the range
                        const range = sortDateRange(state.selectedDate, date);
                        dispatch({ type: "SET_RANGE", payload: range });

                        if (!enableTime) {
                            onDateChange(range);
                            onClose?.();
                        } else {
                            dispatch({ type: "SET_VIEW", payload: "time" });
                            selectTime(12, 0);
                        }
                    }
                }
            },
            [
                mode,
                state.selectedDate,
                enableTime,
                selectDate,
                selectTime,
                dispatch,
                onDateChange,
                onClose,
            ],
        );

        const handleTimeConfirm = useCallback(() => {
            if (state.selectedTime) {
                const finalDate = new Date(state.selectedDate || new Date());
                finalDate.setHours(
                    state.selectedTime.hours,
                    state.selectedTime.minutes,
                    0,
                );

                if (mode === "single") {
                    onDateChange?.(finalDate);
                } else if (mode === "range" && state.selectedRange) {
                    const startDate = new Date(state.selectedRange.start);
                    const endDate = new Date(state.selectedRange.end);

                    startDate.setHours(
                        state.selectedTime.hours,
                        state.selectedTime.minutes,
                        0,
                    );
                    endDate.setHours(
                        state.selectedTime.hours,
                        state.selectedTime.minutes,
                        0,
                    );

                    onDateChange?.({
                        start: startDate,
                        end: endDate,
                    });
                }

                dispatch({ type: "SET_VIEW", payload: "date" });

                onClose?.();
            }
        }, [
            state.selectedDate,
            state.selectedRange,
            state.selectedTime,
            mode,
            onDateChange,
            onClose,
            dispatch,
        ]);

        useEffect(() => {
            if (!value || value === null) {
                dispatch({ type: "RESET" });
                return;
            }

            if (mode === "single" && value instanceof Date) {
                dispatch({ type: "SET_DATE", payload: value });
                dispatch({ type: "SET_DISPLAY_MONTH", payload: value });
            } else if (
                mode === "range" &&
                value &&
                typeof value === "object" &&
                "start" in value &&
                "end" in value
            ) {
                dispatch({ type: "SET_RANGE", payload: value as DateRange });
                dispatch({
                    type: "SET_DISPLAY_MONTH",
                    payload: (value as DateRange).start,
                });
            }
        }, [value, dispatch, mode]);

        return (
            <div
                className={cn(
                    "flex items-center justify-center w-[250px] max-w-md mx-auto overflow-hidden border rounded-lg",
                    className,
                )}
            >
                <AnimatePresence mode="wait">
                    {state.view === "date" ? (
                        <DatePickerView
                            key={`date-picker`}
                            selectedDate={state.selectedDate}
                            selectedRange={state.selectedRange}
                            currentMonth={state.currentMonth}
                            mode={mode}
                            onDateSelect={handleDateSelect}
                            onMonthChange={(month) =>
                                dispatch({
                                    type: "SET_DISPLAY_MONTH",
                                    payload: month,
                                })
                            }
                            minDate={minDate}
                            maxDate={maxDate}
                            disabled={disabled}
                            disableInitialAnimation={disableInitialAnimation}
                        />
                    ) : (
                        <TimePickerView
                            key="time-picker"
                            hours={state.selectedTime?.hours ?? 12}
                            minutes={state.selectedTime?.minutes ?? 0}
                            format={timeFormat}
                            onHoursChange={(hours) =>
                                selectTime(
                                    hours,
                                    state.selectedTime?.minutes ?? 0,
                                )
                            }
                            onMinutesChange={(minutes) =>
                                selectTime(
                                    state.selectedTime?.hours ?? 12,
                                    minutes,
                                )
                            }
                            onConfirm={handleTimeConfirm}
                            onCancel={() =>
                                dispatch({ type: "SET_VIEW", payload: "date" })
                            }
                        />
                    )}
                </AnimatePresence>
            </div>
        );
    },
);

DateTimePickerInner.displayName = "DateTimePickerInner";

// Exported component with provider
export const DateTimePicker = memo((props: DateTimePickerProps) => {
    return (
        <DateTimePickerProvider mode={props.mode} enableTime={props.enableTime}>
            <DateTimePickerInner {...props} />
        </DateTimePickerProvider>
    );
});

DateTimePicker.displayName = "DateTimePicker";

Installation

npx shadcn@latest add @shadix-ui/datetimepicker

Usage

import { Datetimepicker } from "@/components/datetimepicker"
<Datetimepicker />