shadimage
tetra-ui
uinative-date-selectsource: ok · hit

Native Date Select

A native date/time selection input built on Expo UI DatePicker (iOS) and DateTimePicker (Android). Always compose with Content; optionally add Trigger and Input for a form-styled field or custom trigger. Content opens a wheel bottom sheet on iOS and Material dialogs on Android.

npx shadcn add "https://tetra-ui.com/r/native-date-select.json"
free

Where it goes

calendar

Interaction

modaldropdown

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
@expo/uireact-native-reanimated

Registry dependencies

@tetra-ui/action-input@tetra-ui/bottom-sheet@tetra-ui/icons@tetra-ui/input@tetra-ui/slot@tetra-ui/slots

Files5 · source cached 10/8/2026, 7:42:41 PM

ui/native-date-select/index.ts
export * from "./native-date-select";
export type {
  NativeDateSelectMode,
  NativeDateSelectPickerProps,
  NativeDateSelectPresentation,
  NativeDateSelectVariant,
} from "./native-date-select-picker";
ui/native-date-select/native-date-select.tsx
import {
  Children,
  createContext,
  isValidElement,
  useCallback,
  useContext,
  useEffect,
  useLayoutEffect,
  useMemo,
  useState,
} from "react";
import {
  type GestureResponderEvent,
  Platform,
  Pressable,
  type PressableProps,
  View,
} from "react-native";
import Animated, {
  Easing,
  interpolate,
  useAnimatedStyle,
  useSharedValue,
  withTiming,
} from "react-native-reanimated";
import { cn } from "@/lib/utils";
import { ActionInput } from "@/components/ui/action-input";
import {
  BottomSheet,
  BottomSheetBody,
  BottomSheetContent,
  BottomSheetFooter,
  BottomSheetHeader,
  BottomSheetTitle,
} from "@/components/ui/bottom-sheet";
import { ChevronDownIcon } from "@/components/ui/icons";
import {
  InputAddon,
  type InputAddonChild,
  type InputAddonChildren,
  InputAddonIcon,
  useInputAddons,
} from "@/components/ui/input";
import { Slot } from "@/components/ui/slot";
import {
  type NativeDateSelectMode,
  NativeDateSelectPicker,
  type NativeDateSelectVariant,
} from "./native-date-select-picker";
import { nativeDateSelectSlots } from "./native-date-select-slots";

// Constants
const ANIMATION_DURATION = 280;
const ANIMATION_EASING = Easing.out(Easing.cubic);
const WHEEL_PICKER_HEIGHT = 216;
const GRAPHICAL_PICKER_HEIGHT = 360;
const DEFAULT_PLACEHOLDER = "Pick a date";

// Types
type NativeDateSelectContextProps = {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  value?: Date;
  onValueChange: (value: Date) => void;
  selectedValue?: Date;
  setSelectedValue: (value: Date) => void;
  onConfirm: (value?: Date) => void;
  onCancel: () => void;
  disabled?: boolean;
  mode: NativeDateSelectMode;
  variant: NativeDateSelectVariant;
  setVariant: (variant: NativeDateSelectVariant) => void;
  minimumDate?: Date;
  maximumDate?: Date;
  is24Hour?: boolean;
  placeholder: string;
  setPlaceholder: (placeholder: string) => void;
  className?: string;
  testID?: string;
};

type NativeDateSelectProps = {
  open?: boolean;
  onOpenChange?: (open: boolean) => void;
  value?: Date;
  onValueChange?: (value: Date) => void;
  mode?: NativeDateSelectMode;
  variant?: NativeDateSelectVariant;
  minimumDate?: Date;
  maximumDate?: Date;
  disabled?: boolean;
  is24Hour?: boolean;
  className?: string;
  testID?: string;
  children?: React.ReactNode;
};

type NativeDateSelectInputProps = Partial<
  React.ComponentProps<typeof ActionInput>
> & {
  variant?: NativeDateSelectVariant;
  placeholder?: string;
  formatValue?: (date: Date) => string;
  children?: React.ReactNode;
};

type NativeDateSelectTriggerProps = PressableProps & {
  asChild?: boolean;
};

type NativeDateSelectContentProps = {
  children?: React.ReactNode;
};

type NativeDateSelectSheetConfirmProps = PressableProps & {
  asChild?: boolean;
};

// Context
const NativeDateSelectContext =
  createContext<NativeDateSelectContextProps | null>(null);

const useNativeDateSelect = () => {
  const context = useContext(NativeDateSelectContext);
  if (!context) {
    throw new Error(
      "useNativeDateSelect must be used within a NativeDateSelect"
    );
  }
  return context;
};

// Helpers
const formatDateByMode = (date: Date, mode: NativeDateSelectMode) => {
  switch (mode) {
    case "time":
      return new Intl.DateTimeFormat(undefined, {
        hour: "numeric",
        minute: "2-digit",
      }).format(date);
    case "datetime":
      return new Intl.DateTimeFormat(undefined, {
        day: "numeric",
        hour: "numeric",
        minute: "2-digit",
        month: "short",
        year: "numeric",
      }).format(date);
    default:
      return new Intl.DateTimeFormat(undefined, {
        day: "numeric",
        month: "short",
        year: "numeric",
      }).format(date);
  }
};

// Components
/**
 * Native date/time select built on Expo UI DatePicker (iOS) and DateTimePicker (Android).
 * Always compose with NativeDateSelectContent. Optionally add Trigger and Input.
 */
export const NativeDateSelect = (props: NativeDateSelectProps) => {
  return (
    <nativeDateSelectSlots.Provider>
      <NativeDateSelectRoot {...props} />
    </nativeDateSelectSlots.Provider>
  );
};

const NativeDateSelectRoot = ({
  open: openProp,
  onOpenChange: onOpenChangeProp,
  value: valueProp,
  onValueChange: onValueChangeProp,
  mode = "date",
  variant: variantProp = "default",
  minimumDate,
  maximumDate,
  disabled,
  is24Hour,
  className,
  testID,
  children,
}: NativeDateSelectProps) => {
  const [internalOpen, setInternalOpen] = useState(openProp ?? false);
  const [internalValue, setInternalValue] = useState<Date>();
  const [selectedValue, setSelectedValue] = useState<Date>();
  const [placeholder, setPlaceholder] = useState(DEFAULT_PLACEHOLDER);
  const hasInput = nativeDateSelectSlots.useHasSlot("input");
  const [variant, setVariant] = useState<NativeDateSelectVariant>(variantProp);

  const isOpenControlled = openProp !== undefined;
  const open = isOpenControlled ? openProp : internalOpen;

  const isValueControlled = valueProp !== undefined;
  const value = isValueControlled ? valueProp : internalValue;

  useLayoutEffect(() => {
    if (hasInput) {
      return;
    }

    setVariant(variantProp);
  }, [hasInput, variantProp]);

  useEffect(() => {
    if (value !== undefined) {
      setSelectedValue(value);
    }
  }, [value]);

  const onOpenChange = useCallback(
    (nextOpen: boolean) => {
      if (!isOpenControlled) {
        setInternalOpen(nextOpen);
      }
      onOpenChangeProp?.(nextOpen);
    },
    [isOpenControlled, onOpenChangeProp]
  );

  const onValueChange = useCallback(
    (nextValue: Date) => {
      if (!isValueControlled) {
        setInternalValue(nextValue);
      }
      onValueChangeProp?.(nextValue);
    },
    [isValueControlled, onValueChangeProp]
  );

  const onConfirm = useCallback(
    (nextValue?: Date) => {
      const finalValue = nextValue ?? selectedValue;

      if (finalValue !== undefined) {
        onValueChange(finalValue);
      }

      onOpenChange(false);
    },
    [onOpenChange, onValueChange, selectedValue]
  );

  const onCancel = useCallback(() => {
    if (value !== undefined) {
      setSelectedValue(value);
    }
    onOpenChange(false);
  }, [onOpenChange, value]);

  const ctx = useMemo(
    () => ({
      className,
      disabled,
      is24Hour,
      maximumDate,
      minimumDate,
      mode,
      onCancel,
      onConfirm,
      onOpenChange,
      onValueChange,
      open,
      placeholder,
      selectedValue,
      setPlaceholder,
      setSelectedValue,
      setVariant,
      testID,
      value,
      variant,
    }),
    [
      className,
      disabled,
      is24Hour,
      maximumDate,
      minimumDate,
      mode,
      onCancel,
      onConfirm,
      onOpenChange,
      onValueChange,
      open,
      placeholder,
      selectedValue,
      testID,
      value,
      variant,
    ]
  );

  return (
    <NativeDateSelectContext.Provider value={ctx}>
      {children}
    </NativeDateSelectContext.Provider>
  );
};

export const NativeDateSelectTrigger = ({
  asChild,
  onPress: onPressProp,
  ...props
}: NativeDateSelectTriggerProps) => {
  const { disabled, onOpenChange, value, selectedValue, setSelectedValue } =
    useNativeDateSelect();

  const handlePress = useCallback(
    (event: GestureResponderEvent) => {
      onPressProp?.(event);

      if (disabled) {
        return;
      }

      const seedValue = selectedValue ?? value ?? new Date();
      setSelectedValue(seedValue);
      onOpenChange(true);
    },
    [
      disabled,
      onOpenChange,
      onPressProp,
      selectedValue,
      setSelectedValue,
      value,
    ]
  );

  const Comp = asChild ? Slot.Pressable : Pressable;

  return (
    <nativeDateSelectSlots.Fill name="trigger" passthrough>
      <Comp {...props} disabled={disabled} onPress={handlePress} />
    </nativeDateSelectSlots.Fill>
  );
};

NativeDateSelectTrigger.displayName = "NativeDateSelectTrigger";

/**
 * Form-styled native date select input.
 * - Default / wheel / graphical: display-only ActionInput (open via NativeDateSelectTrigger)
 * - iOS `compact`: non-pressable input shell; only the native compact DatePicker is interactive
 */
export const NativeDateSelectInput = (props: NativeDateSelectInputProps) => {
  return (
    <nativeDateSelectSlots.Fill name="input" passthrough>
      <NativeDateSelectInputView {...props} />
    </nativeDateSelectSlots.Fill>
  );
};

NativeDateSelectInput.displayName = "NativeDateSelectInput";

const NativeDateSelectInputView = ({
  variant: variantProp,
  placeholder = DEFAULT_PLACEHOLDER,
  formatValue,
  className,
  testID,
  children,
  ...props
}: NativeDateSelectInputProps) => {
  const {
    open,
    value,
    onValueChange,
    selectedValue,
    disabled,
    mode,
    variant: variantFromRoot,
    minimumDate,
    maximumDate,
    is24Hour,
    setPlaceholder,
    setVariant,
  } = useNativeDateSelect();

  const variant = variantProp ?? variantFromRoot;

  const addonElements = useMemo(() => {
    const addons: InputAddonChild[] = [];
    for (const child of Children.toArray(children)) {
      if (isValidElement(child) && child.type === InputAddon) {
        addons.push(child as InputAddonChild);
      }
    }
    return addons;
  }, [children]);

  const { startAddons, endAddons, groupClassName } = useInputAddons(
    addonElements as InputAddonChildren
  );

  useLayoutEffect(() => {
    setPlaceholder(placeholder);
  }, [placeholder, setPlaceholder]);

  useLayoutEffect(() => {
    if (variantProp !== undefined) {
      setVariant(variantProp);
    }
  }, [variantProp, setVariant]);

  const committedValue = value;
  const pickerValue = selectedValue ?? committedValue ?? new Date();

  const valueLabel = useMemo(() => {
    if (committedValue === undefined) {
      return;
    }
    return formatValue
      ? formatValue(committedValue)
      : formatDateByMode(committedValue, mode);
  }, [committedValue, formatValue, mode]);

  const openSharedValue = useSharedValue(open ? 1 : 0);

  useEffect(() => {
    openSharedValue.value = withTiming(open ? 1 : 0, {
      duration: ANIMATION_DURATION,
      easing: ANIMATION_EASING,
    });
  }, [open, openSharedValue]);

  const animatedStyle = useAnimatedStyle(() => {
    const rotate = interpolate(openSharedValue.value, [0, 1], [0, 180]);
    return {
      transform: [{ rotate: `${rotate}deg` }],
    };
  });

  if (Platform.OS === "ios" && variant === "compact") {
    return (
      <View
        className={cn(
          "flex min-h-12 w-full flex-row items-center gap-2 rounded-lg border border-input bg-background py-2 pr-0 pl-3",
          disabled && "opacity-50",
          groupClassName,
          className
        )}
      >
        {startAddons}

        <View className="min-w-0 grow" />

        <InputAddon align="inline-end" className="shrink-0">
          <NativeDateSelectPicker
            disabled={disabled}
            is24Hour={is24Hour}
            maximumDate={maximumDate}
            minimumDate={minimumDate}
            mode={mode}
            onValueChange={onValueChange}
            testID={testID}
            value={pickerValue}
            variant="compact"
          />
        </InputAddon>

        {endAddons}
      </View>
    );
  }

  const sheetInputAddons = [
    ...startAddons,
    <InputAddon align="inline-end" key="native-date-select-chevron">
      <Animated.View pointerEvents="box-none" style={animatedStyle}>
        <InputAddonIcon>
          <ChevronDownIcon />
        </InputAddonIcon>
      </Animated.View>
    </InputAddon>,
    ...endAddons,
  ] as InputAddonChildren;

  return (
    <ActionInput
      {...props}
      className={cn(groupClassName, className)}
      disabled={disabled}
      focused={open}
      placeholder={placeholder}
      testID={testID}
      value={valueLabel}
    >
      {sheetInputAddons}
    </ActionInput>
  );
};

/**
 * Presentation surface for the native date select. Always required.
 * - Content-only: inline native picker (variant from root)
 * - iOS wheel/default + form UI: bottom sheet with wheel DatePicker
 * - iOS graphical + form UI: bottom sheet with graphical DatePicker
 * - iOS compact + Input: no sheet (picker lives in NativeDateSelectInput)
 * - Android + form UI: Material date/time dialog when open
 */
export const NativeDateSelectContent = ({
  children,
}: NativeDateSelectContentProps) => {
  const {
    open,
    onOpenChange,
    value,
    onValueChange,
    selectedValue,
    setSelectedValue,
    onCancel,
    disabled,
    mode,
    variant,
    minimumDate,
    maximumDate,
    is24Hour,
    placeholder,
    className,
    testID,
  } = useNativeDateSelect();

  const hasTrigger = nativeDateSelectSlots.useHasSlot("trigger");
  const hasInput = nativeDateSelectSlots.useHasSlot("input");
  const hasFooter = nativeDateSelectSlots.useHasSlot("footer");
  const hasFormUi = hasTrigger || hasInput;

  const requiresConfirm = Platform.OS === "ios" && hasFormUi && hasFooter;
  const committedValue = value;
  const draftValue = selectedValue ?? committedValue ?? new Date();
  const pickerValue = requiresConfirm
    ? draftValue
    : (committedValue ?? draftValue);

  const handlePickerValueChange = useCallback(
    (nextValue: Date) => {
      if (requiresConfirm) {
        setSelectedValue(nextValue);
        return;
      }
      onValueChange(nextValue);
    },
    [onValueChange, requiresConfirm, setSelectedValue]
  );

  if (!hasFormUi) {
    return (
      <>
        {children}
        <NativeDateSelectPicker
          className={className}
          disabled={disabled}
          is24Hour={is24Hour}
          maximumDate={maximumDate}
          minimumDate={minimumDate}
          mode={mode}
          onValueChange={onValueChange}
          presentation="inline"
          testID={testID}
          value={committedValue ?? new Date()}
          variant={variant}
        />
      </>
    );
  }

  if (Platform.OS === "ios" && variant === "compact" && hasInput) {
    return children;
  }

  if (Platform.OS === "ios") {
    const sheetVariant = variant === "graphical" ? "graphical" : "wheel";

    return (
      <>
        {children}
        <BottomSheet
          onOpenChange={requiresConfirm ? onCancel : onOpenChange}
          open={open}
        >
          <BottomSheetContent>
            <BottomSheetHeader>
              <BottomSheetTitle>{placeholder}</BottomSheetTitle>
            </BottomSheetHeader>
            <BottomSheetBody className={hasFooter ? undefined : "pb-4"}>
              <NativeDateSelectPicker
                disabled={disabled}
                is24Hour={is24Hour}
                matchContents={false}
                maximumDate={maximumDate}
                minimumDate={minimumDate}
                mode={mode}
                onValueChange={(nextValue) => {
                  handlePickerValueChange(nextValue);

                  if (sheetVariant === "graphical" && !requiresConfirm) {
                    onOpenChange(false);
                  }
                }}
                style={{
                  height:
                    sheetVariant === "graphical"
                      ? GRAPHICAL_PICKER_HEIGHT
                      : WHEEL_PICKER_HEIGHT,
                  width: "100%",
                }}
                testID={testID}
                value={pickerValue}
                variant={sheetVariant}
              />
            </BottomSheetBody>
            <nativeDateSelectSlots.Outlet name="footer" />
          </BottomSheetContent>
        </BottomSheet>
      </>
    );
  }

  if (!open) {
    return children;
  }

  return (
    <>
      {children}
      <NativeDateSelectPicker
        disabled={disabled}
        is24Hour={is24Hour}
        maximumDate={maximumDate}
        minimumDate={minimumDate}
        mode={mode}
        onDismiss={onCancel}
        onValueChange={(nextValue) => {
          onValueChange(nextValue);
          onOpenChange(false);
        }}
        presentation="dialog"
        testID={testID}
        value={pickerValue}
        variant={variant}
      />
    </>
  );
};

NativeDateSelectContent.displayName = "NativeDateSelectContent";

export const NativeDateSelectSheetFooter = (
  props: React.ComponentProps<typeof BottomSheetFooter>
) => {
  return (
    <nativeDateSelectSlots.Fill name="footer">
      <BottomSheetFooter {...props} />
    </nativeDateSelectSlots.Fill>
  );
};

NativeDateSelectSheetFooter.displayName = "NativeDateSelectSheetFooter";

export const NativeDateSelectSheetConfirm = ({
  asChild,
  onPress: onPressProp,
  ...props
}: NativeDateSelectSheetConfirmProps) => {
  const { onConfirm } = useNativeDateSelect();

  const onPress = useCallback(
    (event: GestureResponderEvent) => {
      onPressProp?.(event);
      onConfirm();
    },
    [onConfirm, onPressProp]
  );

  const Comp = asChild ? Slot.Pressable : Pressable;

  return <Comp {...props} onPress={onPress} />;
};
ui/native-date-select/native-date-select-slots.ts
import { createSlots } from "@/lib/slots";

export const nativeDateSelectSlots = createSlots<
  "footer" | "input" | "trigger"
>({
  errorMessage:
    "NativeDateSelect parts must be rendered inside NativeDateSelect",
});
ui/native-date-select/native-date-select-picker.tsx
import {
  DatePickerDialog,
  DateTimePicker,
  Host as HostPrimitive,
  TimePickerDialog,
} from "@expo/ui/jetpack-compose";
import { useState } from "react";
import type { StyleProp, ViewStyle } from "react-native";
import { useCSSVariable, withUniwind } from "uniwind";
import { cn } from "@/lib/utils";

const StyledHost = withUniwind(HostPrimitive);

export type NativeDateSelectMode = "date" | "time" | "datetime";
export type NativeDateSelectVariant =
  | "default"
  | "wheel"
  | "compact"
  | "graphical";
export type NativeDateSelectPresentation = "inline" | "dialog";

export type NativeDateSelectPickerProps = {
  value: Date;
  onValueChange: (value: Date) => void;
  mode?: NativeDateSelectMode;
  variant?: NativeDateSelectVariant;
  minimumDate?: Date;
  maximumDate?: Date;
  disabled?: boolean;
  is24Hour?: boolean;
  presentation?: NativeDateSelectPresentation;
  onDismiss?: () => void;
  className?: string;
  style?: StyleProp<ViewStyle>;
  testID?: string;
  matchContents?: boolean;
};

const modeToDisplayedComponents = (
  mode: NativeDateSelectMode
): "date" | "hourAndMinute" => {
  if (mode === "time") {
    return "hourAndMinute";
  }
  return "date";
};

const variantToAndroidPickerVariant = (
  variant: NativeDateSelectVariant
): "picker" | "input" => {
  if (variant === "wheel") {
    return "input";
  }
  return "picker";
};

const mergeDateAndTime = (datePart: Date, timePart: Date) => {
  const next = new Date(datePart);
  next.setHours(
    timePart.getHours(),
    timePart.getMinutes(),
    timePart.getSeconds(),
    timePart.getMilliseconds()
  );
  return next;
};

/**
 * Android native date/time picker.
 * - `presentation="inline"`: Compose DateTimePicker (date or time; datetime falls back to date)
 * - `presentation="dialog"`: Material dialogs; datetime shows date then time sequentially
 */
export const NativeDateSelectPicker = ({
  value,
  onValueChange,
  mode = "date",
  variant = "default",
  minimumDate,
  maximumDate,
  is24Hour,
  presentation = "inline",
  onDismiss,
  className,
  style,
}: NativeDateSelectPickerProps) => {
  const primaryColor = useCSSVariable("--color-primary") as string;
  const [dialogStep, setDialogStep] = useState<"date" | "time">(
    mode === "time" ? "time" : "date"
  );
  const [pendingDate, setPendingDate] = useState(value);

  const selectableDates =
    minimumDate || maximumDate
      ? { end: maximumDate, start: minimumDate }
      : undefined;

  if (presentation === "dialog") {
    const handleDismiss = () => {
      setDialogStep(mode === "time" ? "time" : "date");
      onDismiss?.();
    };

    if (mode === "time" || dialogStep === "time") {
      return (
        <HostPrimitive style={style}>
          <TimePickerDialog
            color={primaryColor}
            initialDate={(mode === "datetime"
              ? pendingDate
              : value
            ).toISOString()}
            is24Hour={is24Hour}
            onDateSelected={(date) => {
              const next =
                mode === "datetime"
                  ? mergeDateAndTime(pendingDate, date)
                  : date;
              setDialogStep(mode === "time" ? "time" : "date");
              onValueChange(next);
            }}
            onDismissRequest={handleDismiss}
          />
        </HostPrimitive>
      );
    }

    return (
      <HostPrimitive style={style}>
        <DatePickerDialog
          color={primaryColor}
          initialDate={value.toISOString()}
          onDateSelected={(date) => {
            if (mode === "datetime") {
              setPendingDate(date);
              setDialogStep("time");
              return;
            }
            onValueChange(date);
          }}
          onDismissRequest={handleDismiss}
          selectableDates={selectableDates}
          variant={variantToAndroidPickerVariant(variant)}
        />
      </HostPrimitive>
    );
  }

  return (
    <StyledHost
      className={cn("w-full", className)}
      matchContents={{ vertical: true }}
      style={[{ width: "100%" }, style]}
    >
      <DateTimePicker
        color={primaryColor}
        displayedComponents={modeToDisplayedComponents(mode)}
        initialDate={value.toISOString()}
        is24Hour={is24Hour}
        onDateSelected={onValueChange}
        selectableDates={selectableDates}
        showVariantToggle={false}
        variant={variantToAndroidPickerVariant(variant)}
      />
    </StyledHost>
  );
};
ui/native-date-select/native-date-select-picker.ios.tsx
import { DatePicker, Host as HostPrimitive } from "@expo/ui/swift-ui";
import {
  datePickerStyle,
  disabled as disabledModifier,
  labelsHidden,
  type ModifierConfig,
  tint,
} from "@expo/ui/swift-ui/modifiers";
import type { StyleProp, ViewStyle } from "react-native";
import { useCSSVariable, withUniwind } from "uniwind";
import { cn } from "@/lib/utils";

const StyledHost = withUniwind(HostPrimitive);

export type NativeDateSelectMode = "date" | "time" | "datetime";
export type NativeDateSelectVariant =
  | "default"
  | "wheel"
  | "compact"
  | "graphical";
export type NativeDateSelectPresentation = "inline" | "dialog";

export type NativeDateSelectPickerProps = {
  value: Date;
  onValueChange: (value: Date) => void;
  mode?: NativeDateSelectMode;
  variant?: NativeDateSelectVariant;
  minimumDate?: Date;
  maximumDate?: Date;
  disabled?: boolean;
  is24Hour?: boolean;
  presentation?: NativeDateSelectPresentation;
  onDismiss?: () => void;
  className?: string;
  style?: StyleProp<ViewStyle>;
  testID?: string;
  matchContents?: boolean;
};

const modeToDisplayedComponents = (
  mode: NativeDateSelectMode
): ("date" | "hourAndMinute")[] => {
  switch (mode) {
    case "time":
      return ["hourAndMinute"];
    case "datetime":
      return ["date", "hourAndMinute"];
    default:
      return ["date"];
  }
};

const variantToDatePickerStyle = (
  variant: NativeDateSelectVariant
): "automatic" | "compact" | "graphical" | "wheel" => {
  switch (variant) {
    case "wheel":
      return "wheel";
    case "compact":
      return "compact";
    case "graphical":
      return "graphical";
    default:
      return "automatic";
  }
};

/**
 * iOS native date/time picker via SwiftUI DatePicker.
 * `presentation` is ignored (always inline).
 */
export const NativeDateSelectPicker = ({
  value,
  onValueChange,
  mode = "date",
  variant = "default",
  minimumDate,
  maximumDate,
  disabled,
  className,
  style,
  testID,
  matchContents = true,
}: NativeDateSelectPickerProps) => {
  const primaryColor = useCSSVariable("--color-primary") as string;

  const modifiers: ModifierConfig[] = [
    datePickerStyle(variantToDatePickerStyle(variant)),
    tint(primaryColor),
  ];

  if (variant === "compact") {
    modifiers.push(labelsHidden());
  }

  if (disabled) {
    modifiers.push(disabledModifier(true));
  }

  return (
    <StyledHost
      className={cn(className)}
      matchContents={matchContents}
      style={style}
    >
      <DatePicker
        displayedComponents={modeToDisplayedComponents(mode)}
        modifiers={modifiers}
        onDateChange={onValueChange}
        range={
          minimumDate || maximumDate
            ? { end: maximumDate, start: minimumDate }
            : undefined
        }
        selection={value}
        testID={testID}
      />
    </StyledHost>
  );
};

More from tetra-ui

Browse all components in this library →