# Date Picker

> A customizable date and time picker component with bottom sheet UI.

**BNA UI** — a React Native / Expo component library.
These components render through `react-native`, not the DOM: there are no HTML
elements, no Tailwind classes and no Radix primitives. Source is copied into your
project and imported through `@/components/ui/*`, `@/components/charts/*`,
`@/hooks/*` and `@/theme/*`. Colours come from the `useColor` hook rather than
hardcoded hex; sizing tokens (`HEIGHT`, `FONT_SIZE`, `BORDER_RADIUS`, `CORNERS`)
come from `@/theme/globals`.

- Docs: https://ui.ahmedbna.com/docs/components/date-picker
- Markdown: https://ui.ahmedbna.com/docs/components/date-picker.md
- Structured JSON (props, usage, source, examples): https://ui.ahmedbna.com/r/ai/date-picker.json
- Install payload (source plus every file it imports): https://ui.ahmedbna.com/r/date-picker.json
- Install: `npx bna-ui add date-picker`
- npm dependencies: `expo-haptics`, `lucide-react-native`, `react-native-gesture-handler`, `react-native-reanimated`, `react-native-safe-area-context`, `react-native-svg`, `react-native-worklets`
- Registry dependencies: `mode-provider`, `useColorScheme`, `colors`, `useColor`, `useHaptics`, `globals`, `useKeyboardHeight`, `text`, `view`, `bottom-sheet`, `icon`, `spinner`, `button`, `scroll-view`
- Preview recording: https://demo.ahmedbna.com/0130-date-picker-demo.MP4

---

**Example:** A basic date picker with calendar view

```tsx
// components/demo/date-picker/date-picker-demo.tsx
import { DatePicker } from '@/components/ui/date-picker';
import React, { useState } from 'react';

export function DatePickerDemo() {
  const [selectedDate, setSelectedDate] = useState<Date | undefined>();

  return (
    <DatePicker
      label='Select Date'
      value={selectedDate}
      onChange={setSelectedDate}
      placeholder='Choose a date'
    />
  );
}
```

## Installation

### CLI

```bash
npx bna-ui add date-picker
```

### Manual

**1.** Install the following dependencies:

```bash
npx expo install lucide-react-native
```

**2.** Copy and paste the following code into your project.

```tsx
// components/ui/date-picker.tsx
import { BottomSheet, useBottomSheet } from '@/components/ui/bottom-sheet';
import { Button } from '@/components/ui/button';
import { Icon } from '@/components/ui/icon';
import { ScrollView } from '@/components/ui/scroll-view';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import { useColor } from '@/hooks/useColor';
import { useHaptics } from '@/hooks/useHaptics';
import { BORDER_RADIUS, CORNERS, FONT_SIZE, HEIGHT } from '@/theme/globals';
import {
  Calendar,
  CalendarClock,
  ChevronDown,
  ChevronLeft,
  ChevronRight,
  Clock,
  CalendarRange,
  ArrowRight,
} from 'lucide-react-native';
import { useCallback, useMemo, useState } from 'react';
import { TextStyle, TouchableOpacity, ViewStyle } from 'react-native';

export interface DateRange {
  startDate: Date | null;
  endDate: Date | null;
}

// Conditional typing based on mode
interface BaseDatePickerProps {
  label?: string;
  error?: string;
  placeholder?: string;
  disabled?: boolean;
  style?: ViewStyle;
  minimumDate?: Date;
  maximumDate?: Date;
  timeFormat?: '12' | '24';
  variant?: 'filled' | 'outline' | 'group';
  labelStyle?: TextStyle;
  errorStyle?: TextStyle;
  haptic?: boolean;
}

interface DatePickerPropsRange extends BaseDatePickerProps {
  mode: 'range';
  value?: DateRange;
  onChange?: (value: DateRange | undefined) => void;
}

interface DatePickerPropsDate extends BaseDatePickerProps {
  mode?: 'date' | 'time' | 'datetime';
  value?: Date;
  onChange?: (value: Date | undefined) => void;
}

export type DatePickerProps = DatePickerPropsRange | DatePickerPropsDate;

const MONTHS = [
  'January',
  'February',
  'March',
  'April',
  'May',
  'June',
  'July',
  'August',
  'September',
  'October',
  'November',
  'December',
];

const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];

// Generate year range (current year ± 50 years)
const currentYear = new Date().getFullYear();
const YEARS = Array.from({ length: 101 }, (_, i) => currentYear - 50 + i);

// Type guard to check if value is DateRange
const isDateRange = (
  value: Date | DateRange | undefined
): value is DateRange => {
  return (
    value !== undefined &&
    typeof value === 'object' &&
    value !== null &&
    'startDate' in value &&
    'endDate' in value
  );
};

export function DatePicker(props: DatePickerProps) {
  const {
    label,
    error,
    placeholder = 'Select date',
    disabled = false,
    style,
    minimumDate,
    maximumDate,
    timeFormat = '24',
    variant = 'filled',
    labelStyle,
    errorStyle,
    haptic = true,
  } = props;

  const mode = props.mode || 'date';
  const value = props.value;
  const onChange = props.onChange;

  const feedback = useHaptics(haptic);

  const { isVisible, open, close } = useBottomSheet();

  // Get the current date for navigation, prioritizing single date or range start date
  const getCurrentDate = useCallback(() => {
    if (mode === 'range') {
      const rangeValue = isDateRange(value)
        ? value
        : { startDate: null, endDate: null };
      return rangeValue.startDate || new Date();
    }
    return (value as Date) || new Date();
  }, [value, mode]);

  const [currentDate, setCurrentDate] = useState(() => getCurrentDate());
  const [viewMode, setViewMode] = useState<'date' | 'time' | 'month' | 'year'>(
    'date'
  );
  const [showMonthPicker, setShowMonthPicker] = useState(false);
  const [showYearPicker, setShowYearPicker] = useState(false);

  // Range selection state for temporary storage during selection
  const [tempRange, setTempRange] = useState<DateRange>(() =>
    mode === 'range' && isDateRange(value)
      ? value
      : { startDate: null, endDate: null }
  );

  // Theme colors
  const cardColor = useColor('card');
  const borderColor = useColor('border');
  const primaryColor = useColor('primary');
  const primaryForegroundColor = useColor('primaryForeground');
  const mutedColor = useColor('muted');
  const textMutedColor = useColor('textMuted');
  const mutedForegroundColor = useColor('mutedForeground');
  const textColor = useColor('text');
  const errorColor = useColor('red');

  const formatDisplayValue = useCallback(() => {
    if (mode === 'range') {
      const rangeValue = isDateRange(value)
        ? value
        : { startDate: null, endDate: null };

      if (!rangeValue.startDate && !rangeValue.endDate) {
        return placeholder;
      }

      const startStr = rangeValue.startDate
        ? rangeValue.startDate.toLocaleDateString()
        : '';
      const endStr = rangeValue.endDate
        ? rangeValue.endDate.toLocaleDateString()
        : '';

      if (startStr && endStr) {
        return `${startStr} - ${endStr}`;
      } else if (startStr) {
        return `${startStr} - Select end date`;
      } else if (endStr) {
        return `Select start date - ${endStr}`;
      }
      return placeholder;
    }

    const dateValue = value as Date;
    if (!dateValue) return placeholder;

    switch (mode) {
      case 'time':
        if (timeFormat === '12') {
          return dateValue.toLocaleTimeString([], {
            hour: '2-digit',
            minute: '2-digit',
            hour12: true,
          });
        }
        return dateValue.toLocaleTimeString([], {
          hour: '2-digit',
          minute: '2-digit',
          hour12: false,
        });
      case 'datetime':
        const timeStr =
          timeFormat === '12'
            ? dateValue.toLocaleTimeString([], {
                hour: '2-digit',
                minute: '2-digit',
                hour12: true,
              })
            : dateValue.toLocaleTimeString([], {
                hour: '2-digit',
                minute: '2-digit',
                hour12: false,
              });
        return `${dateValue.toLocaleDateString()} ${timeStr}`;
      default:
        return dateValue.toLocaleDateString();
    }
  }, [value, mode, placeholder, timeFormat]);

  // Helper function to check if a date is disabled
  const isDateDisabled = useCallback(
    (date: Date) => {
      if (minimumDate && date < minimumDate) return true;
      if (maximumDate && date > maximumDate) return true;
      return false;
    },
    [minimumDate, maximumDate]
  );

  // Helper function to check if a date is in range
  const isDateInRange = useCallback(
    (date: Date) => {
      if (mode !== 'range' || !tempRange.startDate || !tempRange.endDate) {
        return false;
      }

      // Create new date objects to avoid mutation
      const startDate = new Date(tempRange.startDate);
      const endDate = new Date(tempRange.endDate);
      const checkDate = new Date(date);

      // Normalize dates for comparison (remove time)
      startDate.setHours(0, 0, 0, 0);
      endDate.setHours(0, 0, 0, 0);
      checkDate.setHours(0, 0, 0, 0);

      return checkDate >= startDate && checkDate <= endDate;
    },
    [mode, tempRange]
  );

  // Helper function to check if a date is a range endpoint
  const isRangeEndpoint = useCallback(
    (date: Date) => {
      if (mode !== 'range') {
        return { isStart: false, isEnd: false };
      }

      const normalizedDate = new Date(date);
      normalizedDate.setHours(0, 0, 0, 0);

      const isStart =
        tempRange.startDate &&
        new Date(tempRange.startDate).setHours(0, 0, 0, 0) ===
          normalizedDate.getTime();
      const isEnd =
        tempRange.endDate &&
        new Date(tempRange.endDate).setHours(0, 0, 0, 0) ===
          normalizedDate.getTime();

      return { isStart: !!isStart, isEnd: !!isEnd };
    },
    [mode, tempRange]
  );

  // Memoized calendar calculations
  const calendarData = useMemo(() => {
    const year = currentDate.getFullYear();
    const month = currentDate.getMonth();

    // Get first day of month and number of days
    const firstDay = new Date(year, month, 1).getDay();
    const daysInMonth = new Date(year, month + 1, 0).getDate();

    // Create calendar grid with proper positioning
    const weeks: (number | null)[][] = [];
    let currentWeek: (number | null)[] = [];

    // Fill empty cells for days before month starts
    for (let i = 0; i < firstDay; i++) {
      currentWeek.push(null);
    }

    // Add days of the month
    for (let day = 1; day <= daysInMonth; day++) {
      currentWeek.push(day);

      // If week is complete (7 days) or it's the last day, start a new week
      if (currentWeek.length === 7) {
        weeks.push([...currentWeek]);
        currentWeek = [];
      }
    }

    // Add the last incomplete week if it exists
    if (currentWeek.length > 0) {
      // Fill remaining cells with null
      while (currentWeek.length < 7) {
        currentWeek.push(null);
      }
      weeks.push(currentWeek);
    }

    return { weeks, year, month, daysInMonth };
  }, [currentDate]);

  const handleRangeSelect = (day: number) => {
    const selectedDate = new Date(
      currentDate.getFullYear(),
      currentDate.getMonth(),
      day
    );

    // Check if date is disabled
    if (isDateDisabled(selectedDate)) return;

    feedback('selection');

    // If no start date or both dates are selected, start fresh
    if (!tempRange.startDate || (tempRange.startDate && tempRange.endDate)) {
      setTempRange({
        startDate: selectedDate,
        endDate: null,
      });
    } else {
      // We have a start date but no end date
      const startDate = tempRange.startDate;

      if (selectedDate < startDate) {
        // If selected date is before start date, make it the new start date
        setTempRange({
          startDate: selectedDate,
          endDate: null,
        });
      } else {
        // Selected date is after start date, make it the end date
        setTempRange({
          startDate: startDate,
          endDate: selectedDate,
        });
      }
    }
  };

  const handleDateSelect = (day: number) => {
    if (mode === 'range') {
      handleRangeSelect(day);
      return;
    }

    const newDate = new Date(
      currentDate.getFullYear(),
      currentDate.getMonth(),
      day
    );

    // Check if date is disabled
    if (isDateDisabled(newDate)) return;

    // Range mode returns above, so this only fires for the leaf case and never
    // doubles up with handleRangeSelect.
    feedback('selection');

    setCurrentDate(newDate);

    if (mode === 'date') {
      (onChange as (value: Date | undefined) => void)?.(newDate);
      close();
    } else if (mode === 'datetime') {
      setViewMode('time');
    }
  };

  const handleTimeChange = (hours: number, minutes: number) => {
    feedback('tick');
    const newDate = new Date(currentDate);
    newDate.setHours(hours, minutes, 0, 0);
    setCurrentDate(newDate);
  };

  const navigateMonth = (direction: 'prev' | 'next') => {
    feedback('tick');
    const newDate = new Date(currentDate);
    if (direction === 'prev') {
      newDate.setMonth(newDate.getMonth() - 1);
    } else {
      newDate.setMonth(newDate.getMonth() + 1);
    }
    setCurrentDate(newDate);
  };

  const handleMonthSelect = (monthIndex: number) => {
    feedback('selection');
    const newDate = new Date(currentDate);
    newDate.setMonth(monthIndex);
    setCurrentDate(newDate);
    setShowMonthPicker(false);
  };

  const handleYearSelect = (year: number) => {
    feedback('selection');
    const newDate = new Date(currentDate);
    newDate.setFullYear(year);
    setCurrentDate(newDate);
    setShowYearPicker(false);
  };

  const handleConfirm = () => {
    feedback('success');
    if (mode === 'range') {
      (onChange as (value: DateRange | undefined) => void)?.(tempRange);
    } else {
      (onChange as (value: Date | undefined) => void)?.(currentDate);
    }
    close();
  };

  const resetToToday = () => {
    const today = new Date();
    setCurrentDate(today);

    if (mode === 'range') {
      setTempRange({ startDate: today, endDate: null });
    } else if (mode === 'date') {
      (onChange as (value: Date | undefined) => void)?.(today);
      close();
    }
  };

  const clearSelection = () => {
    if (mode === 'range') {
      setTempRange({ startDate: null, endDate: null });
      (onChange as (value: DateRange | undefined) => void)?.(undefined);
    } else {
      (onChange as (value: Date | undefined) => void)?.(undefined);
    }
  };

  const renderMonthYearHeader = () => (
    <View
      style={{
        flexDirection: 'row',
        justifyContent: 'space-between',
        alignItems: 'center',
        marginBottom: 24,
        paddingHorizontal: 8,
      }}
    >
      <TouchableOpacity
        onPress={() => navigateMonth('prev')}
        style={{
          padding: 10,
          borderRadius: CORNERS,
          backgroundColor: mutedColor,
        }}
      >
        <ChevronLeft size={20} color={textColor} />
      </TouchableOpacity>

      <View
        style={{
          flex: 1,
          flexDirection: 'row',
          justifyContent: 'center',
          alignItems: 'center',
          gap: 12,
          marginHorizontal: 12,
        }}
      >
        <TouchableOpacity
          onPress={() => setShowMonthPicker(true)}
          style={{
            flex: 1,
            flexDirection: 'row',
            alignItems: 'center',
            justifyContent: 'center',
            paddingHorizontal: 12,
            paddingVertical: 10,
            borderRadius: CORNERS,
            backgroundColor: mutedColor,
          }}
        >
          <Text variant='subtitle' style={{ marginRight: 4 }}>
            {MONTHS[calendarData.month]}
          </Text>
          <ChevronDown size={16} color={textColor} />
        </TouchableOpacity>

        <TouchableOpacity
          onPress={() => setShowYearPicker(true)}
          style={{
            flex: 1,
            flexDirection: 'row',
            alignItems: 'center',
            justifyContent: 'center',
            paddingHorizontal: 16,
            paddingVertical: 10,
            borderRadius: CORNERS,
            backgroundColor: mutedColor,
          }}
        >
          <Text variant='subtitle' style={{ marginRight: 4 }}>
            {calendarData.year}
          </Text>
          <ChevronDown size={16} color={textColor} />
        </TouchableOpacity>
      </View>

      <TouchableOpacity
        onPress={() => navigateMonth('next')}
        style={{
          padding: 10,
          borderRadius: CORNERS,
          backgroundColor: mutedColor,
        }}
      >
        <ChevronRight size={20} color={textColor} />
      </TouchableOpacity>
    </View>
  );

  const renderCalendar = () => (
    <View>
      {renderMonthYearHeader()}
      {/* Day headers */}
      <View
        style={{
          flexDirection: 'row',
          marginBottom: 12,
          paddingHorizontal: 4,
        }}
      >
        {DAYS.map((day) => (
          <View
            key={day}
            style={{
              flex: 1,
              alignItems: 'center',
            }}
          >
            <Text variant='caption' style={{ fontSize: 12, fontWeight: '600' }}>
              {day}
            </Text>
          </View>
        ))}
      </View>

      {/* Calendar grid */}
      <View style={{ paddingHorizontal: 4 }}>
        {calendarData.weeks.map((week, weekIndex) => (
          <View
            key={weekIndex}
            style={{
              flexDirection: 'row',
              marginBottom: 4,
            }}
          >
            {week.map((day, dayIndex) => {
              const dayDate = day
                ? new Date(calendarData.year, calendarData.month, day)
                : null;

              const isSelected =
                day &&
                value &&
                !isDateRange(value) &&
                value.getDate() === day &&
                value.getMonth() === calendarData.month &&
                value.getFullYear() === calendarData.year;

              const isToday =
                day &&
                new Date().getDate() === day &&
                new Date().getMonth() === calendarData.month &&
                new Date().getFullYear() === calendarData.year;

              const disabled = dayDate ? isDateDisabled(dayDate) : false;

              // Range-specific styling
              const inRange = dayDate ? isDateInRange(dayDate) : false;
              const rangeEndpoints = dayDate
                ? isRangeEndpoint(dayDate)
                : { isStart: false, isEnd: false };

              return (
                <View
                  key={dayIndex}
                  style={[
                    {
                      flex: 1,
                      alignItems: 'center',
                      backgroundColor:
                        mode === 'range' && inRange
                          ? primaryColor
                          : 'transparent',
                      paddingHorizontal: mode === 'range' && inRange ? 0 : 0,
                    },
                    rangeEndpoints.isStart && {
                      borderTopLeftRadius: CORNERS,
                      borderBottomLeftRadius: CORNERS,
                    },
                    rangeEndpoints.isEnd && {
                      borderTopRightRadius: CORNERS,
                      borderBottomRightRadius: CORNERS,
                    },
                  ]}
                >
                  {day ? (
                    <TouchableOpacity
                      onPress={() => !disabled && handleDateSelect(day)}
                      disabled={disabled}
                      style={[
                        {
                          width: 40,
                          height: 40,
                          borderRadius:
                            rangeEndpoints.isStart || rangeEndpoints.isEnd
                              ? 0
                              : CORNERS,
                          backgroundColor:
                            rangeEndpoints.isStart || rangeEndpoints.isEnd
                              ? primaryColor
                              : inRange
                                ? primaryColor
                                : isSelected
                                  ? primaryColor
                                  : 'transparent',
                          borderWidth:
                            isToday && !isSelected && !inRange ? 1 : 0,
                          borderColor: primaryColor,
                          justifyContent: 'center',
                          alignItems: 'center',
                          opacity: disabled ? 0.3 : 1,
                        },
                        rangeEndpoints.isStart && {
                          borderTopLeftRadius: CORNERS,
                          borderBottomLeftRadius: CORNERS,
                        },
                        rangeEndpoints.isEnd && {
                          borderTopRightRadius: CORNERS,
                          borderBottomRightRadius: CORNERS,
                        },
                      ]}
                    >
                      <Text
                        style={{
                          color:
                            rangeEndpoints.isStart || rangeEndpoints.isEnd
                              ? primaryForegroundColor
                              : inRange
                                ? primaryForegroundColor
                                : isSelected
                                  ? primaryForegroundColor
                                  : disabled
                                    ? mutedForegroundColor
                                    : textColor,
                          fontWeight:
                            rangeEndpoints.isStart ||
                            rangeEndpoints.isEnd ||
                            isSelected ||
                            isToday
                              ? '600'
                              : '400',
                          fontSize: FONT_SIZE,
                        }}
                      >
                        {day}
                      </Text>
                    </TouchableOpacity>
                  ) : (
                    <View style={{ width: 40, height: 40 }} />
                  )}
                </View>
              );
            })}
          </View>
        ))}
      </View>

      {/* Range selection info */}
      {mode === 'range' && (
        <View
          style={{
            flexDirection: 'row',
            alignItems: 'center',
            justifyContent: 'space-between',
            marginTop: 16,
            padding: 20,
            paddingHorizontal: 36,
            backgroundColor: mutedColor,
            borderRadius: BORDER_RADIUS,
          }}
        >
          <Text variant='subtitle' style={{ flex: 1 }}>
            {tempRange.startDate
              ? `${tempRange.startDate.toLocaleDateString()}`
              : 'Start date'}
          </Text>

          <View
            style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}
          >
            <ArrowRight color={textColor} strokeWidth={3} />
          </View>

          <Text variant='subtitle' style={{ flex: 1, textAlign: 'right' }}>
            {tempRange.endDate
              ? `${tempRange.endDate.toLocaleDateString()}`
              : 'End date'}
          </Text>
        </View>
      )}
    </View>
  );

  const renderTimePicker = () => {
    const selectedHours = currentDate.getHours();
    const selectedMinutes = currentDate.getMinutes();

    const isPM = selectedHours >= 12;

    return (
      <View style={{ height: 300 }}>
        <View
          style={{
            flexDirection: 'row',
            flex: 1,
            gap: 16,
          }}
        >
          {/* Hours */}
          <View style={{ flex: 1 }}>
            <Text
              variant='caption'
              style={{ textAlign: 'center', marginBottom: 12 }}
            >
              Hours
            </Text>
            <ScrollView
              showsVerticalScrollIndicator={false}
              contentContainerStyle={{
                paddingVertical: 20,
              }}
            >
              {Array.from({ length: timeFormat === '12' ? 12 : 24 }, (_, i) =>
                timeFormat === '12' ? (i === 0 ? 12 : i) : i
              ).map((hour) => {
                const actualHour =
                  timeFormat === '12'
                    ? hour === 12
                      ? isPM
                        ? 12
                        : 0
                      : isPM
                        ? hour + 12
                        : hour
                    : hour;

                const isSelected = actualHour === selectedHours;

                return (
                  <TouchableOpacity
                    key={hour}
                    onPress={() =>
                      handleTimeChange(actualHour, selectedMinutes)
                    }
                    style={{
                      paddingVertical: 12,
                      paddingHorizontal: 16,
                      borderRadius: CORNERS,
                      backgroundColor: isSelected
                        ? primaryColor
                        : 'transparent',
                      marginVertical: 2,
                      alignItems: 'center',
                    }}
                  >
                    <Text
                      style={{
                        color: isSelected ? primaryForegroundColor : textColor,
                        fontWeight: isSelected ? '600' : '400',
                        fontSize: FONT_SIZE,
                      }}
                    >
                      {hour.toString().padStart(2, '0')}
                    </Text>
                  </TouchableOpacity>
                );
              })}
            </ScrollView>
          </View>

          {/* Minutes */}
          <View style={{ flex: 1 }}>
            <Text
              variant='caption'
              style={{ textAlign: 'center', marginBottom: 12 }}
            >
              Minutes
            </Text>
            <ScrollView
              showsVerticalScrollIndicator={false}
              contentContainerStyle={{
                paddingVertical: 20,
              }}
            >
              {Array.from({ length: 12 }, (_, i) => i * 5).map((minute) => (
                <TouchableOpacity
                  key={minute}
                  onPress={() => handleTimeChange(selectedHours, minute)}
                  style={{
                    paddingVertical: 12,
                    paddingHorizontal: 16,
                    borderRadius: CORNERS,
                    backgroundColor:
                      minute === selectedMinutes ? primaryColor : 'transparent',
                    marginVertical: 2,
                    alignItems: 'center',
                  }}
                >
                  <Text
                    style={{
                      color:
                        minute === selectedMinutes
                          ? primaryForegroundColor
                          : textColor,
                      fontWeight: minute === selectedMinutes ? '600' : '400',
                      fontSize: FONT_SIZE,
                    }}
                  >
                    {minute.toString().padStart(2, '0')}
                  </Text>
                </TouchableOpacity>
              ))}
            </ScrollView>
          </View>

          {/* AM/PM picker for 12-hour format */}
          {timeFormat === '12' && (
            <View style={{ flex: 0.5 }}>
              <Text
                variant='caption'
                style={{ textAlign: 'center', marginBottom: 12 }}
              >
                Period
              </Text>
              <View
                style={{
                  paddingVertical: 20,
                  gap: 8,
                }}
              >
                {['AM', 'PM'].map((period) => {
                  const isAM = period === 'AM';
                  const isSelected = isAM ? !isPM : isPM;

                  return (
                    <TouchableOpacity
                      key={period}
                      onPress={() => {
                        const newHours = isAM
                          ? selectedHours >= 12
                            ? selectedHours - 12
                            : selectedHours
                          : selectedHours < 12
                            ? selectedHours + 12
                            : selectedHours;
                        handleTimeChange(newHours, selectedMinutes);
                      }}
                      style={{
                        paddingVertical: 12,
                        paddingHorizontal: 16,
                        borderRadius: CORNERS,
                        backgroundColor: isSelected
                          ? primaryColor
                          : 'transparent',
                        alignItems: 'center',
                      }}
                    >
                      <Text
                        style={{
                          color: isSelected
                            ? primaryForegroundColor
                            : textColor,
                          fontWeight: isSelected ? '600' : '400',
                          fontSize: FONT_SIZE,
                        }}
                      >
                        {period}
                      </Text>
                    </TouchableOpacity>
                  );
                })}
              </View>
            </View>
          )}
        </View>
      </View>
    );
  };

  const renderMonthPicker = () => (
    <View style={{ height: 300 }}>
      <ScrollView
        showsVerticalScrollIndicator={false}
        contentContainerStyle={{
          paddingVertical: 20,
        }}
      >
        {MONTHS.map((month, index) => (
          <TouchableOpacity
            key={month}
            onPress={() => handleMonthSelect(index)}
            style={{
              paddingVertical: 16,
              paddingHorizontal: 20,
              borderRadius: CORNERS,
              backgroundColor:
                index === calendarData.month ? primaryColor : 'transparent',
              marginVertical: 2,
              alignItems: 'center',
            }}
          >
            <Text
              style={{
                color:
                  index === calendarData.month
                    ? primaryForegroundColor
                    : textColor,
                fontWeight: index === calendarData.month ? '600' : '400',
                fontSize: FONT_SIZE,
              }}
            >
              {month}
            </Text>
          </TouchableOpacity>
        ))}
      </ScrollView>
    </View>
  );

  const renderYearPicker = () => (
    <View style={{ height: 300 }}>
      <ScrollView
        showsVerticalScrollIndicator={false}
        contentContainerStyle={{
          paddingVertical: 20,
        }}
      >
        {YEARS.map((year) => (
          <TouchableOpacity
            key={year}
            onPress={() => handleYearSelect(year)}
            style={{
              paddingVertical: 16,
              paddingHorizontal: 20,
              borderRadius: CORNERS,
              backgroundColor:
                year === calendarData.year ? primaryColor : 'transparent',
              marginVertical: 2,
              alignItems: 'center',
            }}
          >
            <Text
              style={{
                color:
                  year === calendarData.year
                    ? primaryForegroundColor
                    : textColor,
                fontWeight: year === calendarData.year ? '600' : '400',
                fontSize: FONT_SIZE,
              }}
            >
              {year}
            </Text>
          </TouchableOpacity>
        ))}
      </ScrollView>
    </View>
  );

  const getBottomSheetContent = () => {
    if (showMonthPicker) return renderMonthPicker();
    if (showYearPicker) return renderYearPicker();

    if (mode === 'datetime') {
      return viewMode === 'date' ? renderCalendar() : renderTimePicker();
    }

    if (mode === 'time') return renderTimePicker();
    return renderCalendar();
  };

  const getBottomSheetTitle = () => {
    if (showMonthPicker) return 'Select Month';
    if (showYearPicker) return 'Select Year';

    if (mode === 'datetime') {
      return viewMode === 'date' ? 'Select Date' : 'Select Time';
    }

    if (mode === 'time') return 'Select Time';

    if (mode === 'range') return 'Select Range';

    return 'Select Date';
  };

  const handleOpenPicker = () => {
    feedback('impact-light');
    setCurrentDate(new Date());
    setViewMode('date');
    setShowMonthPicker(false);
    setShowYearPicker(false);
    open();
  };

  const triggerStyle: ViewStyle = {
    width: '100%',
    flexDirection: 'row',
    alignItems: 'center',
    paddingHorizontal: variant === 'group' ? 0 : 16,
    borderWidth: variant === 'group' ? 0 : 1,
    borderColor: variant === 'outline' ? borderColor : cardColor,
    borderRadius: CORNERS,
    backgroundColor: variant === 'filled' ? cardColor : 'transparent',
    minHeight: variant === 'group' ? 'auto' : HEIGHT,
  };

  return (
    <>
      <TouchableOpacity
        style={[triggerStyle, disabled && { opacity: 0.5 }, style]}
        onPress={handleOpenPicker}
        disabled={disabled}
      >
        <View
          style={{
            flex: 1,
            flexDirection: 'row',
            alignItems: 'center',
            gap: 8,
          }}
        >
          <View
            style={{
              width: label ? 120 : 'auto',
              flexDirection: 'row',
              alignItems: 'center',
              gap: 8,
            }}
          >
            {mode === 'time' ? (
              <Icon name={Clock} size={20} strokeWidth={1} />
            ) : mode === 'datetime' ? (
              <Icon name={CalendarClock} size={20} strokeWidth={1} />
            ) : mode === 'range' ? (
              <Icon name={CalendarRange} size={20} strokeWidth={1} />
            ) : (
              <Icon name={Calendar} size={20} strokeWidth={1} />
            )}

            {/* Label takes 1/3 of available width when present */}
            {label && (
              <View style={{ flex: 1 }}>
                <Text
                  variant='caption'
                  numberOfLines={1}
                  ellipsizeMode='tail'
                  style={[
                    {
                      color: error ? errorColor : textMutedColor,
                    },
                    labelStyle,
                  ]}
                >
                  {label}
                </Text>
              </View>
            )}
          </View>

          {/* Text takes 2/3 of available width when label is present, or full width when no label */}
          <View style={{ flex: 1 }}>
            <Text
              numberOfLines={1}
              ellipsizeMode='tail'
              style={{
                color: value ? textColor : textMutedColor,
                fontSize: FONT_SIZE,
              }}
            >
              {formatDisplayValue()}
            </Text>
          </View>
        </View>
      </TouchableOpacity>

      {error && (
        <Text
          variant='caption'
          style={[
            { color: errorColor, marginTop: 4, marginLeft: 14 },
            errorStyle,
          ]}
        >
          {error}
        </Text>
      )}

      <BottomSheet
        isVisible={isVisible}
        onClose={() => {
          close();
          setShowMonthPicker(false);
          setShowYearPicker(false);
        }}
        title={getBottomSheetTitle()}
        snapPoints={[0.7]}
        disablePanGesture={showMonthPicker || showYearPicker}
      >
        <View style={{ flex: 1 }}>
          {getBottomSheetContent()}

          <View
            style={{
              flexDirection: 'row',
              justifyContent: 'space-between',
              alignItems: 'center',
              paddingTop: 20,
              gap: 12,
            }}
          >
            <View
              style={{
                flexDirection: 'row',
                gap: 8,
              }}
            >
              <Button variant='outline' onPress={resetToToday}>
                Today
              </Button>

              <Button
                variant='outline'
                onPress={() => {
                  close();
                  setShowMonthPicker(false);
                  setShowYearPicker(false);
                  clearSelection();
                }}
              >
                {mode === 'range' ? 'Clear' : 'Cancel'}
              </Button>
            </View>

            {mode === 'datetime' && viewMode === 'date' ? (
              <Button onPress={() => setViewMode('time')} style={{ flex: 1 }}>
                Next
              </Button>
            ) : (
              <Button
                onPress={handleConfirm}
                // handleConfirm fires a success notification, so the button's
                // own light impact would land on top of it.
                haptic={false}
                style={{ flex: 1 }}
              >
                Done
              </Button>
            )}
          </View>
        </View>
      </BottomSheet>
    </>
  );
}
```

**3.** Update the import paths to match your project setup.

## Usage

```tsx
import { DatePicker } from '@/components/ui/date-picker';
```

```tsx
const [selectedDate, setSelectedDate] = useState<Date | undefined>();

<DatePicker
  label='Select Date'
  value={selectedDate}
  onChange={setSelectedDate}
  placeholder='Choose a date'
/>;
```

## Examples

#### Default

**Example:** A basic date picker with calendar view

```tsx
// components/demo/date-picker/date-picker-demo.tsx
import { DatePicker } from '@/components/ui/date-picker';
import React, { useState } from 'react';

export function DatePickerDemo() {
  const [selectedDate, setSelectedDate] = useState<Date | undefined>();

  return (
    <DatePicker
      label='Select Date'
      value={selectedDate}
      onChange={setSelectedDate}
      placeholder='Choose a date'
    />
  );
}
```

#### Time Picker

**Example:** A time picker with hour and minute selection

```tsx
// components/demo/date-picker/date-picker-time.tsx
import { DatePicker } from '@/components/ui/date-picker';
import { View } from '@/components/ui/view';
import React, { useState } from 'react';

export function DatePickerTime() {
  const [time24, setTime24] = useState<Date | undefined>();
  const [time12, setTime12] = useState<Date | undefined>();

  return (
    <View style={{ gap: 16 }}>
      <DatePicker
        label='24-Hour Time'
        mode='time'
        value={time24}
        onChange={setTime24}
        placeholder='Select time'
        timeFormat='24'
      />

      <DatePicker
        label='12-Hour Time'
        mode='time'
        value={time12}
        onChange={setTime12}
        placeholder='Select time'
        timeFormat='12'
      />
    </View>
  );
}
```

#### Date Time Picker

**Example:** A combined date and time picker

```tsx
// components/demo/date-picker/date-picker-datetime.tsx
import { DatePicker } from '@/components/ui/date-picker';
import React, { useState } from 'react';

export function DatePickerDateTime() {
  const [dateTime, setDateTime] = useState<Date | undefined>();

  return (
    <DatePicker
      label='Date & Time'
      mode='datetime'
      value={dateTime}
      onChange={setDateTime}
      placeholder='Select date and time'
      timeFormat='12'
    />
  );
}
```

#### Date Range

**Example:** Date picker range

```tsx
// components/demo/date-picker/date-picker-range.tsx
import { DatePicker, DateRange } from '@/components/ui/date-picker';
import React, { useState } from 'react';

export function DatePickerRange() {
  const [selectedRange, setSelectedRange] = useState<DateRange | undefined>();

  return (
    <DatePicker
      mode='range'
      label='Select Date'
      value={selectedRange}
      onChange={setSelectedRange}
      placeholder='Choose a range'
    />
  );
}
```

#### With Constraints

**Example:** Date picker with minimum and maximum date limits

```tsx
// components/demo/date-picker/date-picker-constraints.tsx
import { DatePicker } from '@/components/ui/date-picker';
import { View } from '@/components/ui/view';
import React, { useState } from 'react';

export function DatePickerConstraints() {
  const [pastDate, setPastDate] = useState<Date | undefined>();
  const [futureDate, setFutureDate] = useState<Date | undefined>();
  const [rangeDate, setRangeDate] = useState<Date | undefined>();

  const today = new Date();
  const maxPastDate = new Date();
  maxPastDate.setDate(today.getDate() - 1); // Yesterday

  const minFutureDate = new Date();
  minFutureDate.setDate(today.getDate() + 1); // Tomorrow

  const minRangeDate = new Date();
  minRangeDate.setMonth(today.getMonth() - 1); // Last month

  const maxRangeDate = new Date();
  maxRangeDate.setMonth(today.getMonth() + 1); // Next month

  return (
    <View style={{ gap: 16 }}>
      <DatePicker
        label='Past Dates Only'
        value={pastDate}
        onChange={setPastDate}
        placeholder='Select past date'
        maximumDate={maxPastDate}
      />

      <DatePicker
        label='Future Dates Only'
        value={futureDate}
        onChange={setFutureDate}
        placeholder='Select future date'
        minimumDate={minFutureDate}
      />

      <DatePicker
        label='Date Range'
        value={rangeDate}
        onChange={setRangeDate}
        placeholder='Select date in range'
        minimumDate={minRangeDate}
        maximumDate={maxRangeDate}
      />
    </View>
  );
}
```

#### Different Variants

**Example:** Date pickers with different styling variants

```tsx
// components/demo/date-picker/date-picker-variants.tsx
import { DatePicker } from '@/components/ui/date-picker';
import { View } from '@/components/ui/view';
import React, { useState } from 'react';

export function DatePickerVariants() {
  const [filledDate, setFilledDate] = useState<Date | undefined>();
  const [outlineDate, setOutlineDate] = useState<Date | undefined>();
  const [groupDate, setGroupDate] = useState<Date | undefined>();

  return (
    <View style={{ gap: 16 }}>
      <DatePicker
        label='Filled Variant'
        variant='filled'
        value={filledDate}
        onChange={setFilledDate}
        placeholder='Select date'
      />

      <DatePicker
        label='Outline Variant'
        variant='outline'
        value={outlineDate}
        onChange={setOutlineDate}
        placeholder='Select date'
      />

      <DatePicker
        label='Group Variant'
        variant='group'
        value={groupDate}
        onChange={setGroupDate}
        placeholder='Select date'
      />
    </View>
  );
}
```

#### Time Formats

**Example:** Time picker with 12-hour and 24-hour formats

```tsx
// components/demo/date-picker/date-picker-formats.tsx
import { DatePicker } from '@/components/ui/date-picker';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import React, { useState } from 'react';

export function DatePickerFormats() {
  const [datetime24, setDateTime24] = useState<Date | undefined>();
  const [datetime12, setDateTime12] = useState<Date | undefined>();

  return (
    <View style={{ gap: 16 }}>
      <View>
        <Text variant='caption' style={{ marginBottom: 8 }}>
          24-Hour Format
        </Text>
        <DatePicker
          mode='datetime'
          value={datetime24}
          onChange={setDateTime24}
          placeholder='Select date and time'
          timeFormat='24'
        />
      </View>

      <View>
        <Text variant='caption' style={{ marginBottom: 8 }}>
          12-Hour Format with AM/PM
        </Text>
        <DatePicker
          mode='datetime'
          value={datetime12}
          onChange={setDateTime12}
          placeholder='Select date and time'
          timeFormat='12'
        />
      </View>
    </View>
  );
}
```

#### Form Integration

**Example:** Date picker integrated within a form with validation

```tsx
// components/demo/date-picker/date-picker-form.tsx
import { Button } from '@/components/ui/button';
import { DatePicker } from '@/components/ui/date-picker';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import React, { useState } from 'react';
import { Alert } from 'react-native';

export function DatePickerForm() {
  const [birthDate, setBirthDate] = useState<Date | undefined>();
  const [appointmentDate, setAppointmentDate] = useState<Date | undefined>();
  const [errors, setErrors] = useState<{
    birthDate?: string;
    appointmentDate?: string;
  }>({});

  const validateForm = () => {
    const newErrors: typeof errors = {};

    if (!birthDate) {
      newErrors.birthDate = 'Birth date is required';
    } else {
      const today = new Date();
      const age = today.getFullYear() - birthDate.getFullYear();
      if (age < 18) {
        newErrors.birthDate = 'Must be 18 years or older';
      }
    }

    if (!appointmentDate) {
      newErrors.appointmentDate = 'Appointment date is required';
    } else {
      const today = new Date();
      today.setHours(0, 0, 0, 0);
      if (appointmentDate < today) {
        newErrors.appointmentDate = 'Appointment must be in the future';
      }
    }

    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };

  const handleSubmit = () => {
    if (validateForm()) {
      Alert.alert('Success', 'Form submitted successfully!');
    }
  };

  const maxBirthDate = new Date();
  maxBirthDate.setFullYear(maxBirthDate.getFullYear() - 18);

  const minAppointmentDate = new Date();
  minAppointmentDate.setDate(minAppointmentDate.getDate() + 1);

  return (
    <View style={{ gap: 16 }}>
      <Text variant='title' style={{ marginBottom: 8 }}>
        Registration Form
      </Text>

      <DatePicker
        label='Date of Birth'
        value={birthDate}
        onChange={(date) => {
          setBirthDate(date);
          if (errors.birthDate) {
            setErrors((prev) => ({ ...prev, birthDate: undefined }));
          }
        }}
        placeholder='Select your birth date'
        maximumDate={maxBirthDate}
        error={errors.birthDate}
      />

      <DatePicker
        label='Appointment'
        mode='datetime'
        value={appointmentDate}
        onChange={(date) => {
          setAppointmentDate(date);
          if (errors.appointmentDate) {
            setErrors((prev) => ({ ...prev, appointmentDate: undefined }));
          }
        }}
        placeholder='Select appointment date and time'
        minimumDate={minAppointmentDate}
        timeFormat='12'
        error={errors.appointmentDate}
      />

      <Button onPress={handleSubmit} style={{ marginTop: 8 }}>
        Submit Registration
      </Button>
    </View>
  );
}
```

## API Reference

### DatePicker

The main date picker component that handles date and time selection.

| Prop          | Type                                                                            | Default         | Description                                                                                                                                                     |
| ------------- | ------------------------------------------------------------------------------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `haptic`      | `boolean`                                                                       | `true`          | Whether to trigger haptic feedback while navigating and selecting — a tick per month or time step, a selection per date, and a success notification on confirm. |
| `label`       | `string`                                                                        | -               | Label text displayed above the picker.                                                                                                                          |
| `value`       | `Date \| DateRange`                                                             | -               | The currently selected date. `DateRange` (`{ startDate, endDate }`) when `mode` is `"range"`, otherwise a plain `Date`.                                         |
| `onChange`    | `(value: Date \| undefined) => void \| (value: DateRange \| undefined) => void` | -               | Callback fired when the selection changes. Receives a `DateRange \| undefined` when `mode` is `"range"`, otherwise a `Date \| undefined`.                       |
| `mode`        | `'date' \| 'range' \| 'time' \| 'datetime'`                                     | `'date'`        | The picker mode. `value`/`onChange` switch shape based on this: `DateRange` for `"range"`, `Date` for every other mode.                                         |
| `placeholder` | `string`                                                                        | `'Select date'` | Placeholder text when no date is selected.                                                                                                                      |
| `disabled`    | `boolean`                                                                       | `false`         | Whether the picker is disabled.                                                                                                                                 |
| `error`       | `string`                                                                        | -               | Error message to display.                                                                                                                                       |
| `minimumDate` | `Date`                                                                          | -               | The minimum selectable date.                                                                                                                                    |
| `maximumDate` | `Date`                                                                          | -               | The maximum selectable date.                                                                                                                                    |
| `timeFormat`  | `'12' \| '24'`                                                                  | `'24'`          | Time format for time and datetime modes.                                                                                                                        |
| `variant`     | `'filled' \| 'outline' \| 'group'`                                              | `'filled'`      | Visual variant of the picker trigger.                                                                                                                           |
| `style`       | `ViewStyle`                                                                     | -               | Additional styles for the trigger container.                                                                                                                    |
| `labelStyle`  | `TextStyle`                                                                     | -               | Additional styles for the label text.                                                                                                                           |
| `errorStyle`  | `TextStyle`                                                                     | -               | Additional styles for the error text.                                                                                                                           |

### DateRange

The value shape used when `mode` is `"range"`.

| Prop        | Type           | Description                      |
| ----------- | -------------- | -------------------------------- |
| `startDate` | `Date \| null` | The start of the selected range. |
| `endDate`   | `Date \| null` | The end of the selected range.   |

## Features

### Multiple Modes

- **Date Mode**: Calendar-based date selection
- **Range Mode**: Calendar-based range date selection
- **Time Mode**: Hour and minute selection with scrollable lists
- **DateTime Mode**: Combined date and time selection with step-by-step flow

### Flexible Time Formats

- **24-hour format**: Standard 24-hour time display (00:00 - 23:59)
- **12-hour format**: AM/PM time display with period selector

### Date Constraints

- Set minimum and maximum selectable dates
- Automatic disabling of invalid dates
- Visual feedback for disabled dates

### Customizable Variants

- **Filled**: Default filled background style
- **Outline**: Border-only style with transparent background
- **Group**: Minimal style for use within form groups

### Accessibility Features

- Full screen reader support
- Keyboard navigation compatibility
- High contrast mode support
- Proper focus management

### Navigation

- Month/year quick selection
- Smooth navigation between months
- Today button for quick access to current date
- Intuitive calendar grid layout

## Accessibility

The DatePicker component follows accessibility best practices:

- Uses semantic markup for screen readers
- Provides proper ARIA labels and descriptions
- Supports keyboard navigation
- Maintains focus management in the bottom sheet
- Offers high contrast support for visually impaired users
- Includes proper error announcement

## Customization

The component uses your theme colors and can be customized through:

- Theme color overrides
- Custom styling props
- Variant selection
- Label and error styling
- Container styling

## Performance

- Optimized calendar calculations with useMemo
- Efficient date validation
- Minimal re-renders with proper callback optimization
- Smooth scrolling in time pickers
