diff --git a/src/components/DatePicker/DatePicker.tsx b/src/components/DatePicker/DatePicker.tsx
new file mode 100644
index 0000000000..ac713e29c1
--- /dev/null
+++ b/src/components/DatePicker/DatePicker.tsx
@@ -0,0 +1,281 @@
+import * as React from 'react';
+import { Pressable, StyleSheet, Text, View, ViewStyle } from 'react-native';
+
+import type { DatePickerProps } from './types';
+import { useInternalTheme } from '../../core/theming';
+
+const MONTHS = [
+ 'January',
+ 'February',
+ 'March',
+ 'April',
+ 'May',
+ 'June',
+ 'July',
+ 'August',
+ 'September',
+ 'October',
+ 'November',
+ 'December',
+];
+
+const WEEKDAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
+
+const daysInMonth = (year: number, month: number) =>
+ new Date(year, month + 1, 0).getDate();
+
+const firstWeekday = (year: number, month: number) =>
+ new Date(year, month, 1).getDay();
+
+const sameDay = (a: Date, b: Date) =>
+ a.getFullYear() === b.getFullYear() &&
+ a.getMonth() === b.getMonth() &&
+ a.getDate() === b.getDate();
+
+const styles = StyleSheet.create({
+ container: {
+ padding: 8,
+ width: 320,
+ } as ViewStyle,
+ header: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ marginBottom: 8,
+ } as ViewStyle,
+ navLabel: {
+ fontSize: 16,
+ fontWeight: '600',
+ } as ViewStyle,
+ navButton: {
+ width: 40,
+ height: 40,
+ alignItems: 'center',
+ justifyContent: 'center',
+ } as ViewStyle,
+ navButtonText: {
+ fontSize: 20,
+ } as ViewStyle,
+ grid: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ } as ViewStyle,
+ cell: {
+ width: '14.285714285714286%',
+ height: 40,
+ alignItems: 'center',
+ justifyContent: 'center',
+ } as ViewStyle,
+ weekday: {
+ fontSize: 12,
+ fontWeight: '500',
+ } as ViewStyle,
+ day: {
+ width: 32,
+ height: 32,
+ borderRadius: 16,
+ alignItems: 'center',
+ justifyContent: 'center',
+ } as ViewStyle,
+ dayText: {
+ fontSize: 14,
+ } as ViewStyle,
+ outOfMonth: {
+ opacity: 0.35,
+ } as ViewStyle,
+});
+
+/**
+ * Material 3 calendar date picker (month view).
+ *
+ * Shows a single-month grid with prev/next navigation and a selectable day
+ * cell for each day. The selected day is highlighted; days outside the
+ * `min`/`max` range are disabled.
+ *
+ * @param props
+ */
+function DatePicker({
+ value,
+ onChange,
+ min,
+ max,
+ weekStartsOn = 0,
+ style,
+ theme: themeOverrides,
+ testID = 'date-picker',
+}: DatePickerProps) {
+ const theme = useInternalTheme(themeOverrides);
+ const [view, setView] = React.useState({
+ year: value.getFullYear(),
+ month: value.getMonth(),
+ });
+
+ 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 orderedWeekdays = [
+ ...WEEKDAYS.slice(weekStartsOn),
+ ...WEEKDAYS.slice(0, weekStartsOn),
+ ];
+
+ const total = daysInMonth(view.year, view.month);
+ const firstDow = (firstWeekday(view.year, view.month) - weekStartsOn + 7) % 7;
+
+ // Leading days from the previous month to fill the first row.
+ const prevMonthDays = daysInMonth(
+ view.month === 0 ? view.year - 1 : view.year,
+ view.month === 0 ? 11 : view.month - 1
+ );
+ const cells: Array<{
+ day: number;
+ monthOffset: number; // -1 prev, 0 current, +1 next
+ date: Date;
+ }> = [];
+
+ for (let i = 0; i < firstDow; i++) {
+ const day = prevMonthDays - firstDow + 1 + i;
+ const date = new Date(
+ view.month === 0 ? view.year - 1 : view.year,
+ view.month === 0 ? 11 : view.month - 1,
+ day
+ );
+ cells.push({ day, monthOffset: -1, date });
+ }
+ for (let d = 1; d <= total; d++) {
+ cells.push({
+ day: d,
+ monthOffset: 0,
+ date: new Date(view.year, view.month, d),
+ });
+ }
+ // Trailing days to fill a full 6-week (42-cell) grid.
+ let next = 1;
+ while (cells.length < 42) {
+ const date = new Date(
+ view.month === 11 ? view.year + 1 : view.year,
+ view.month === 11 ? 0 : view.month + 1,
+ next
+ );
+ cells.push({ day: next, monthOffset: 1, date });
+ next += 1;
+ }
+
+ const isDisabled = (date: Date) => {
+ if (
+ min &&
+ date < new Date(min.getFullYear(), min.getMonth(), min.getDate())
+ ) {
+ return true;
+ }
+ if (
+ max &&
+ date > new Date(max.getFullYear(), max.getMonth(), max.getDate())
+ ) {
+ return true;
+ }
+ return false;
+ };
+
+ const prevMonth = () => {
+ setView((v) =>
+ v.month === 0
+ ? { year: v.year - 1, month: 11 }
+ : { year: v.year, month: v.month - 1 }
+ );
+ };
+ const nextMonth = () => {
+ setView((v) =>
+ v.month === 11
+ ? { year: v.year + 1, month: 0 }
+ : { year: v.year, month: v.month + 1 }
+ );
+ };
+
+ const label = `${MONTHS[view.month]} ${view.year}`;
+
+ return (
+
+
+
+ ‹
+
+
+ {label}
+
+
+ ›
+
+
+
+
+ {orderedWeekdays.map((wd, i) => (
+
+ {wd}
+
+ ))}
+
+
+
+ {cells.map((cell, i) => {
+ const selected = sameDay(cell.date, value);
+ const disabled = isDisabled(cell.date);
+ const outOfMonth = cell.monthOffset !== 0;
+ return (
+
+ {
+ if (!disabled) onChange(cell.date);
+ }}
+ disabled={disabled}
+ accessibilityRole="button"
+ accessibilityLabel={cell.date.toDateString()}
+ style={[
+ styles.day,
+ selected
+ ? { backgroundColor: primary }
+ : { backgroundColor: 'transparent' },
+ ]}
+ >
+
+ {cell.day}
+
+
+
+ );
+ })}
+
+
+ );
+}
+
+export default DatePicker;
diff --git a/src/components/DatePicker/types.ts b/src/components/DatePicker/types.ts
new file mode 100644
index 0000000000..88d9a2f3b6
--- /dev/null
+++ b/src/components/DatePicker/types.ts
@@ -0,0 +1,22 @@
+import type { StyleProp, ViewStyle } from 'react-native';
+
+import type { ThemeProp } from '../../types';
+
+export type DatePickerProps = {
+ /** Currently selected date (controlled). */
+ value: Date;
+ /** Called when the user selects a day. */
+ onChange: (date: Date) => void;
+ /** Earliest selectable date. Days before this are disabled. */
+ min?: Date;
+ /** Latest selectable date. Days after this are disabled. */
+ max?: Date;
+ /** 0 = Sunday .. 6 = Saturday. @default 0 */
+ weekStartsOn?: 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..aa84854e20 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 DatePicker } from './components/DatePicker/DatePicker';
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 { DatePickerProps } from './components/DatePicker/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';