diff --git a/src/components/TimePicker/TimePicker.tsx b/src/components/TimePicker/TimePicker.tsx
new file mode 100644
index 0000000000..ec03872c30
--- /dev/null
+++ b/src/components/TimePicker/TimePicker.tsx
@@ -0,0 +1,209 @@
+import * as React from 'react';
+import { Pressable, StyleSheet, Text, View, ViewStyle } from 'react-native';
+
+import type { TimePickerProps } from './types';
+import { useInternalTheme } from '../../core/theming';
+
+const pad = (n: number) => String(n).padStart(2, '0');
+
+const styles = StyleSheet.create({
+ container: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ padding: 16,
+ } as ViewStyle,
+ field: {
+ alignItems: 'center',
+ marginHorizontal: 4,
+ } as ViewStyle,
+ stepperButton: {
+ width: 40,
+ height: 28,
+ alignItems: 'center',
+ justifyContent: 'center',
+ } as ViewStyle,
+ stepperText: {
+ fontSize: 18,
+ } as ViewStyle,
+ valueText: {
+ fontSize: 36,
+ fontWeight: '500',
+ minWidth: 72,
+ textAlign: 'center',
+ } as ViewStyle,
+ label: {
+ fontSize: 12,
+ marginTop: 4,
+ } as ViewStyle,
+ colon: {
+ fontSize: 36,
+ fontWeight: '500',
+ } as ViewStyle,
+ toggle: {
+ marginLeft: 12,
+ flexDirection: 'row',
+ } as ViewStyle,
+ toggleButton: {
+ paddingHorizontal: 12,
+ paddingVertical: 8,
+ borderWidth: 1,
+ } as ViewStyle,
+});
+
+/**
+ * Material 3 time picker (input/stepper style).
+ *
+ * Lets the user pick an hour and minute with up/down steppers, and toggle
+ * AM/PM in 12-hour mode. The value is controlled as a Date; only the hour and
+ * minute components are read and written.
+ *
+ * @param props
+ */
+function TimePicker({
+ value,
+ onChange,
+ hours24 = false,
+ minuteStep = 1,
+ style,
+ theme: themeOverrides,
+ testID = 'time-picker',
+}: TimePickerProps) {
+ const theme = useInternalTheme(themeOverrides);
+ const primary = theme.isV3 ? theme.colors.primary : theme.colors.accent;
+ const onSurface = theme.isV3 ? theme.colors.onSurface : '#000';
+ const surface = theme.isV3 ? theme.colors.surface : '#fff';
+ const outline = theme.isV3 ? theme.colors.outline : '#999';
+
+ const hour24 = value.getHours();
+ const minute = value.getMinutes();
+ const isPM = hour24 >= 12;
+
+ const displayHour = hours24 ? hour24 : hour24 % 12 === 0 ? 12 : hour24 % 12;
+
+ const emit = (nextHour: number, nextMinute: number) => {
+ const next = new Date(value);
+ // Let Date carry over/underflow across hour and minute boundaries.
+ next.setHours(nextHour, nextMinute, 0, 0);
+ onChange(next);
+ };
+
+ const stepHour = (delta: number) => emit(hour24 + delta, minute);
+ const stepMinute = (delta: number) => emit(hour24, minute + delta);
+
+ const togglePeriod = () => {
+ emit(hour24 + (isPM ? -12 : 12), minute);
+ };
+
+ const PeriodToggle = () => (
+
+ {
+ if (isPM) togglePeriod();
+ }}
+ accessibilityRole="button"
+ accessibilityLabel="AM"
+ accessibilityState={{ selected: !isPM }}
+ style={[
+ styles.toggleButton,
+ {
+ borderColor: outline,
+ backgroundColor: !isPM ? primary : 'transparent',
+ borderTopRightRadius: 0,
+ borderBottomRightRadius: 0,
+ } as ViewStyle,
+ ]}
+ >
+
+ AM
+
+
+ {
+ if (!isPM) togglePeriod();
+ }}
+ accessibilityRole="button"
+ accessibilityLabel="PM"
+ accessibilityState={{ selected: isPM }}
+ style={[
+ styles.toggleButton,
+ {
+ borderColor: outline,
+ backgroundColor: isPM ? primary : 'transparent',
+ borderTopLeftRadius: 0,
+ borderBottomLeftRadius: 0,
+ marginLeft: -1,
+ } as ViewStyle,
+ ]}
+ >
+
+ PM
+
+
+
+ );
+
+ const Stepper = ({
+ testIDStem,
+ onUp,
+ onDown,
+ }: {
+ testIDStem: string;
+ onUp: () => void;
+ onDown: () => void;
+ }) => (
+
+
+ ▲
+
+
+ {testIDStem === `${testID}-hour` ? pad(displayHour) : pad(minute)}
+
+
+ ▼
+
+
+ {testIDStem === `${testID}-hour` ? 'Hour' : 'Minute'}
+
+
+ );
+
+ return (
+
+ stepHour(1)}
+ onDown={() => stepHour(-1)}
+ />
+ :
+ stepMinute(minuteStep)}
+ onDown={() => stepMinute(-minuteStep)}
+ />
+ {!hours24 ? : null}
+
+ );
+}
+
+export default TimePicker;
diff --git a/src/components/TimePicker/types.ts b/src/components/TimePicker/types.ts
new file mode 100644
index 0000000000..dfe058de51
--- /dev/null
+++ b/src/components/TimePicker/types.ts
@@ -0,0 +1,20 @@
+import type { StyleProp, ViewStyle } from 'react-native';
+
+import type { ThemeProp } from '../../types';
+
+export type TimePickerProps = {
+ /** Currently selected time as a Date (only hour/minute are used). Controlled. */
+ value: Date;
+ /** Called when the user changes the time. */
+ onChange: (time: Date) => void;
+ /** Use a 24-hour clock instead of AM/PM. @default false */
+ hours24?: boolean;
+ /** Step in minutes for the minute field. @default 1 */
+ minuteStep?: number;
+ /** Style applied to the container. */
+ style?: StyleProp;
+ /** @optional */
+ theme?: ThemeProp;
+ /** TestID for testing. */
+ testID?: string;
+};
diff --git a/src/index.tsx b/src/index.tsx
index 1b20528787..ea2dc1e9d4 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -32,6 +32,7 @@ export { default as Banner } from './components/Banner';
export { default as BottomNavigation } from './components/BottomNavigation/BottomNavigation';
export { default as Button } from './components/Button/Button';
export { default as Card } from './components/Card/Card';
+export { default as TimePicker } from './components/TimePicker/TimePicker';
export { default as Checkbox } from './components/Checkbox';
export { default as Chip } from './components/Chip/Chip';
export { default as DataTable } from './components/DataTable/DataTable';
@@ -86,6 +87,7 @@ export type {
} from './components/BottomNavigation/BottomNavigation';
export type { Props as ButtonProps } from './components/Button/Button';
export type { Props as CardProps } from './components/Card/Card';
+export type { TimePickerProps } from './components/TimePicker/types';
export type { Props as CardActionsProps } from './components/Card/CardActions';
export type { Props as CardContentProps } from './components/Card/CardContent';
export type { Props as CardCoverProps } from './components/Card/CardCover';