(
+ new TZDate(1900, 11, 1, timeZone),
+ );
+ const [noonSafeEnabled, setNoonSafeEnabled] = useState(true);
+ const formatter = new Intl.DateTimeFormat("en-US", {
+ dateStyle: "full",
+ timeStyle: "short",
+ timeZone,
+ });
+
+ return (
+
+
+
+
+ );
+}
diff --git a/examples/index.ts b/examples/index.ts
index 97923afe8..efa70b3ef 100644
--- a/examples/index.ts
+++ b/examples/index.ts
@@ -101,6 +101,8 @@ export * from "./TestCase2843";
export * from "./TestCase2864";
export * from "./Testcase1567";
export * from "./TimeZone";
+export * from "./TimeZoneNoonSafe";
+export * from "./TimeZoneNoonSafeSimple";
export * from "./timezone/TestCase2833";
export * from "./Utc";
export * from "./WeekIso";
diff --git a/src/DayPicker.tsx b/src/DayPicker.tsx
index 58eaa954b..725dbf254 100644
--- a/src/DayPicker.tsx
+++ b/src/DayPicker.tsx
@@ -13,6 +13,7 @@ import { getMonthOptions } from "./helpers/getMonthOptions.js";
import { getStyleForModifiers } from "./helpers/getStyleForModifiers.js";
import { getWeekdays } from "./helpers/getWeekdays.js";
import { getYearOptions } from "./helpers/getYearOptions.js";
+import { createNoonOverrides } from "./noonDateLib.js";
import type {
DayPickerProps,
Modifiers,
@@ -100,18 +101,31 @@ export function DayPicker(initialProps: DayPickerProps) {
const { components, formatters, labels, dateLib, locale, classNames } =
useMemo(() => {
const locale = { ...defaultLocale, ...props.locale };
+ const weekStartsOn = props.broadcastCalendar ? 1 : props.weekStartsOn;
+
+ const noonOverrides =
+ props.noonSafe && props.timeZone
+ ? createNoonOverrides(props.timeZone, {
+ weekStartsOn,
+ locale,
+ })
+ : undefined;
+ const overrides =
+ props.dateLib && noonOverrides
+ ? { ...noonOverrides, ...props.dateLib }
+ : (props.dateLib ?? noonOverrides);
const dateLib = new DateLib(
{
locale,
- weekStartsOn: props.broadcastCalendar ? 1 : props.weekStartsOn,
+ weekStartsOn,
firstWeekContainsDate: props.firstWeekContainsDate,
useAdditionalWeekYearTokens: props.useAdditionalWeekYearTokens,
useAdditionalDayOfYearTokens: props.useAdditionalDayOfYearTokens,
timeZone: props.timeZone,
numerals: props.numerals,
},
- props.dateLib,
+ overrides,
);
return {
@@ -132,6 +146,7 @@ export function DayPicker(initialProps: DayPickerProps) {
props.timeZone,
props.numerals,
props.dateLib,
+ props.noonSafe,
props.components,
props.formatters,
props.labels,
diff --git a/src/noonDateLib.ts b/src/noonDateLib.ts
new file mode 100644
index 000000000..f3e351913
--- /dev/null
+++ b/src/noonDateLib.ts
@@ -0,0 +1,224 @@
+import { TZDate } from "@date-fns/tz";
+import type { EndOfWeekOptions, Locale, StartOfWeekOptions } from "date-fns";
+import {
+ differenceInCalendarDays as differenceInCalendarDaysFn,
+ differenceInCalendarMonths as differenceInCalendarMonthsFn,
+ getISOWeek as getISOWeekFn,
+ getWeek as getWeekFn,
+} from "date-fns";
+import type { DateLib } from "./classes/DateLib.js";
+
+export interface CreateNoonOverridesOptions {
+ weekStartsOn?: number;
+ locale?: Locale;
+}
+
+type SupportedDate = Date | number | string | TZDate;
+type WeekStartsOn = NonNullable;
+
+/**
+ * Creates `dateLib` overrides that keep all calendar math at noon in the target
+ * time zone. This avoids second-level offset changes (e.g., historical zones
+ * with +03:41:12) from pushing dates backward across midnight.
+ */
+export function createNoonOverrides(
+ timeZone: string,
+ options: CreateNoonOverridesOptions = {},
+): Partial {
+ const { weekStartsOn, locale } = options;
+ const fallbackWeekStartsOn: WeekStartsOn = (weekStartsOn ??
+ locale?.options?.weekStartsOn ??
+ 0) as WeekStartsOn;
+
+ // Keep all internal math anchored at noon in the target zone to avoid
+ // historical second-level offsets from crossing midnight.
+ const toNoonTZDate = (date: SupportedDate): TZDate => {
+ const normalizedDate =
+ typeof date === "number" || typeof date === "string"
+ ? new Date(date)
+ : date;
+ return new TZDate(
+ normalizedDate.getFullYear(),
+ normalizedDate.getMonth(),
+ normalizedDate.getDate(),
+ 12,
+ 0,
+ 0,
+ timeZone,
+ );
+ };
+
+ // Convert a value into a host `Date` that represents the same calendar day
+ // as the target-zone noon. This is useful for helpers (e.g., date-fns week
+ // utilities) that expect local `Date` instances rather than `TZDate`s.
+ const toCalendarDate = (date: SupportedDate): Date => {
+ const zoned = toNoonTZDate(date);
+ return new Date(
+ zoned.getFullYear(),
+ zoned.getMonth(),
+ zoned.getDate(),
+ 0,
+ 0,
+ 0,
+ 0,
+ );
+ };
+
+ return {
+ today: () => {
+ return toNoonTZDate(TZDate.tz(timeZone));
+ },
+
+ newDate: (year: number, monthIndex: number, date: number) => {
+ return new TZDate(year, monthIndex, date, 12, 0, 0, timeZone);
+ },
+
+ startOfDay: (date) => {
+ return toNoonTZDate(date);
+ },
+
+ startOfWeek: (date, options?: StartOfWeekOptions) => {
+ const base = toNoonTZDate(date);
+ const weekStartsOnValue = (options?.weekStartsOn ??
+ fallbackWeekStartsOn) as WeekStartsOn;
+ const diff = (base.getDay() - weekStartsOnValue + 7) % 7;
+ base.setDate(base.getDate() - diff);
+ return base;
+ },
+
+ startOfISOWeek: (date) => {
+ const base = toNoonTZDate(date);
+ const diff = (base.getDay() - 1 + 7) % 7;
+ base.setDate(base.getDate() - diff);
+ return base;
+ },
+
+ startOfMonth: (date) => {
+ const base = toNoonTZDate(date);
+ base.setDate(1);
+ return base;
+ },
+
+ startOfYear: (date) => {
+ const base = toNoonTZDate(date);
+ base.setMonth(0, 1);
+ return base;
+ },
+
+ endOfWeek: (date, options?: EndOfWeekOptions) => {
+ const base = toNoonTZDate(date);
+ const weekStartsOnValue = (options?.weekStartsOn ??
+ fallbackWeekStartsOn) as WeekStartsOn;
+ const endDow = (weekStartsOnValue + 6) % 7;
+ const diff = (endDow - base.getDay() + 7) % 7;
+ base.setDate(base.getDate() + diff);
+ return base;
+ },
+
+ endOfISOWeek: (date) => {
+ const base = toNoonTZDate(date);
+ const diff = (7 - base.getDay()) % 7;
+ base.setDate(base.getDate() + diff);
+ return base;
+ },
+
+ endOfMonth: (date) => {
+ const base = toNoonTZDate(date);
+ base.setMonth(base.getMonth() + 1, 0);
+ return base;
+ },
+
+ endOfYear: (date) => {
+ const base = toNoonTZDate(date);
+ base.setMonth(11, 31);
+ return base;
+ },
+
+ eachMonthOfInterval: (interval) => {
+ const start = toNoonTZDate(interval.start);
+ const end = toNoonTZDate(interval.end);
+ const result: Date[] = [];
+ const cursor = new TZDate(
+ start.getFullYear(),
+ start.getMonth(),
+ 1,
+ 12,
+ 0,
+ 0,
+ timeZone,
+ );
+ const endKey = end.getFullYear() * 12 + end.getMonth();
+ while (cursor.getFullYear() * 12 + cursor.getMonth() <= endKey) {
+ result.push(new TZDate(cursor, timeZone));
+ cursor.setMonth(cursor.getMonth() + 1, 1);
+ }
+ return result;
+ },
+
+ // Normalize to noon once before arithmetic (avoid DST/midnight edge cases),
+ // mutate the same TZDate, and return it.
+ addDays: (date, amount) => {
+ const base = toNoonTZDate(date);
+ base.setDate(base.getDate() + amount);
+ return base;
+ },
+
+ addWeeks: (date, amount) => {
+ const base = toNoonTZDate(date);
+ base.setDate(base.getDate() + amount * 7);
+ return base;
+ },
+
+ addMonths: (date, amount) => {
+ const base = toNoonTZDate(date);
+ base.setMonth(base.getMonth() + amount);
+ return base;
+ },
+
+ addYears: (date, amount) => {
+ const base = toNoonTZDate(date);
+ base.setFullYear(base.getFullYear() + amount);
+ return base;
+ },
+
+ eachYearOfInterval: (interval) => {
+ const start = toNoonTZDate(interval.start);
+ const end = toNoonTZDate(interval.end);
+ const years: Date[] = [];
+ const cursor = new TZDate(start.getFullYear(), 0, 1, 12, 0, 0, timeZone);
+ while (cursor.getFullYear() <= end.getFullYear()) {
+ years.push(new TZDate(cursor, timeZone));
+ cursor.setFullYear(cursor.getFullYear() + 1, 0, 1);
+ }
+ return years;
+ },
+
+ getWeek: (date, options) => {
+ const base = toCalendarDate(date);
+ return getWeekFn(base, {
+ weekStartsOn: options?.weekStartsOn ?? fallbackWeekStartsOn,
+ firstWeekContainsDate:
+ options?.firstWeekContainsDate ??
+ locale?.options?.firstWeekContainsDate ??
+ 1,
+ });
+ },
+
+ getISOWeek: (date) => {
+ const base = toCalendarDate(date);
+ return getISOWeekFn(base);
+ },
+
+ differenceInCalendarDays: (dateLeft, dateRight) => {
+ const left = toCalendarDate(dateLeft);
+ const right = toCalendarDate(dateRight);
+ return differenceInCalendarDaysFn(left, right);
+ },
+
+ differenceInCalendarMonths: (dateLeft, dateRight) => {
+ const left = toCalendarDate(dateLeft);
+ const right = toCalendarDate(dateRight);
+ return differenceInCalendarMonthsFn(left, right);
+ },
+ };
+}
diff --git a/src/noonJalaliDateLib.ts b/src/noonJalaliDateLib.ts
new file mode 100644
index 000000000..eb334c6a3
--- /dev/null
+++ b/src/noonJalaliDateLib.ts
@@ -0,0 +1,173 @@
+import { TZDate } from "@date-fns/tz";
+import type { EndOfWeekOptions, StartOfWeekOptions } from "date-fns";
+import {
+ addDays as addDaysJalali,
+ addMonths as addMonthsJalali,
+ addWeeks as addWeeksJalali,
+ addYears as addYearsJalali,
+ differenceInCalendarDays as differenceInCalendarDaysJalali,
+ differenceInCalendarMonths as differenceInCalendarMonthsJalali,
+ eachMonthOfInterval as eachMonthOfIntervalJalali,
+ eachYearOfInterval as eachYearOfIntervalJalali,
+ endOfISOWeek as endOfISOWeekJalali,
+ endOfMonth as endOfMonthJalali,
+ endOfWeek as endOfWeekJalali,
+ endOfYear as endOfYearJalali,
+ getWeek as getWeekJalali,
+ startOfISOWeek as startOfISOWeekJalali,
+ startOfMonth as startOfMonthJalali,
+ startOfWeek as startOfWeekJalali,
+ startOfYear as startOfYearJalali,
+} from "date-fns-jalali";
+
+import type { DateLib } from "./classes/DateLib.js";
+import type { CreateNoonOverridesOptions } from "./noonDateLib.js";
+
+type SupportedDate = Date | number | string | TZDate;
+type WeekStartsOn = NonNullable;
+
+/**
+ * Jalali-aware version of {@link createNoonDateLibOverrides}.
+ *
+ * Keeps all calendar math at noon in the target time zone while deferring to
+ * `date-fns-jalali` for calendar logic.
+ */
+export function createJalaliNoonOverrides(
+ timeZone: string,
+ options: CreateNoonOverridesOptions = {},
+): Partial {
+ const { weekStartsOn, locale } = options;
+ const fallbackWeekStartsOn: WeekStartsOn = (weekStartsOn ??
+ locale?.options?.weekStartsOn ??
+ 6) as WeekStartsOn;
+
+ // Keep all internal math anchored at noon in the target zone to avoid
+ // historical second-level offsets from crossing midnight.
+ const toNoonTZDate = (date: SupportedDate): TZDate => {
+ const normalizedDate =
+ typeof date === "number" || typeof date === "string"
+ ? new Date(date)
+ : date;
+ return new TZDate(
+ normalizedDate.getFullYear(),
+ normalizedDate.getMonth(),
+ normalizedDate.getDate(),
+ 12,
+ 0,
+ 0,
+ timeZone,
+ );
+ };
+
+ // Represent the target-zone calendar date in the host zone so date-fns-jalali
+ // (which is not time-zone aware) can operate on stable wall times.
+ const toCalendarDate = (date: SupportedDate): Date => {
+ const zoned = toNoonTZDate(date);
+ return new Date(
+ zoned.getFullYear(),
+ zoned.getMonth(),
+ zoned.getDate(),
+ 12,
+ 0,
+ 0,
+ 0,
+ );
+ };
+
+ return {
+ today: () => toNoonTZDate(TZDate.tz(timeZone)),
+ newDate: (year: number, monthIndex: number, date: number) =>
+ new TZDate(year, monthIndex, date, 12, 0, 0, timeZone),
+
+ startOfDay: (date) => {
+ return toNoonTZDate(date);
+ },
+ startOfWeek: (date, options?: StartOfWeekOptions) => {
+ const weekStartsOnValue = (options?.weekStartsOn ??
+ fallbackWeekStartsOn) as WeekStartsOn;
+ const start = startOfWeekJalali(toCalendarDate(date), {
+ weekStartsOn: weekStartsOnValue,
+ });
+ return toNoonTZDate(start);
+ },
+ startOfISOWeek: (date) => {
+ const start = startOfISOWeekJalali(toCalendarDate(date));
+ return toNoonTZDate(start);
+ },
+ startOfMonth: (date) => {
+ const start = startOfMonthJalali(toCalendarDate(date));
+ return toNoonTZDate(start);
+ },
+ startOfYear: (date) => {
+ const start = startOfYearJalali(toCalendarDate(date));
+ return toNoonTZDate(start);
+ },
+
+ endOfWeek: (date, options?: EndOfWeekOptions) => {
+ const weekStartsOnValue = (options?.weekStartsOn ??
+ fallbackWeekStartsOn) as WeekStartsOn;
+ const end = endOfWeekJalali(toCalendarDate(date), {
+ weekStartsOn: weekStartsOnValue,
+ });
+ return toNoonTZDate(end);
+ },
+ endOfISOWeek: (date) => {
+ const end = endOfISOWeekJalali(toCalendarDate(date));
+ return toNoonTZDate(end);
+ },
+ endOfMonth: (date) => {
+ const end = endOfMonthJalali(toCalendarDate(date));
+ return toNoonTZDate(end);
+ },
+ endOfYear: (date) => {
+ const end = endOfYearJalali(toCalendarDate(date));
+ return toNoonTZDate(end);
+ },
+
+ eachMonthOfInterval: (interval) => {
+ return eachMonthOfIntervalJalali({
+ start: toCalendarDate(interval.start),
+ end: toCalendarDate(interval.end),
+ }).map((date) => toNoonTZDate(date));
+ },
+
+ addDays: (date, amount) =>
+ toNoonTZDate(addDaysJalali(toCalendarDate(date), amount)),
+ addWeeks: (date, amount) =>
+ toNoonTZDate(addWeeksJalali(toCalendarDate(date), amount)),
+ addMonths: (date, amount) =>
+ toNoonTZDate(addMonthsJalali(toCalendarDate(date), amount)),
+ addYears: (date, amount) =>
+ toNoonTZDate(addYearsJalali(toCalendarDate(date), amount)),
+
+ eachYearOfInterval: (interval) => {
+ return eachYearOfIntervalJalali({
+ start: toCalendarDate(interval.start),
+ end: toCalendarDate(interval.end),
+ }).map((date) => toNoonTZDate(date));
+ },
+
+ getWeek: (date, options) => {
+ const base = toCalendarDate(date);
+ return getWeekJalali(base, {
+ weekStartsOn: options?.weekStartsOn ?? fallbackWeekStartsOn,
+ firstWeekContainsDate:
+ options?.firstWeekContainsDate ??
+ locale?.options?.firstWeekContainsDate ??
+ 1,
+ });
+ },
+
+ differenceInCalendarDays: (dateLeft, dateRight) => {
+ const left = toCalendarDate(dateLeft);
+ const right = toCalendarDate(dateRight);
+ return differenceInCalendarDaysJalali(left, right);
+ },
+
+ differenceInCalendarMonths: (dateLeft, dateRight) => {
+ const left = toCalendarDate(dateLeft);
+ const right = toCalendarDate(dateRight);
+ return differenceInCalendarMonthsJalali(left, right);
+ },
+ };
+}
diff --git a/src/persian.tsx b/src/persian.tsx
index 7f182b2b4..604c806ef 100644
--- a/src/persian.tsx
+++ b/src/persian.tsx
@@ -8,6 +8,7 @@ import {
} from "./index.js";
import { enUSJalali } from "./locale/en-US-jalali.js";
import { faIRJalali } from "./locale/fa-IR-jalali.js";
+import { createJalaliNoonOverrides } from "./noonJalaliDateLib.js";
import type { DayPickerProps } from "./types/props.js";
/** Persian (Iran) Jalali locale with DayPicker labels. */
@@ -70,20 +71,32 @@ export function DayPicker(
numerals?: DayPickerProps["numerals"];
},
) {
+ const {
+ locale: localeProp,
+ dir,
+ dateLib: dateLibProp,
+ numerals,
+ noonSafe,
+ ...restProps
+ } = props;
+
const dateLib = getDateLib({
- locale: props.locale,
+ locale: localeProp,
weekStartsOn: props.broadcastCalendar ? 1 : props.weekStartsOn,
firstWeekContainsDate: props.firstWeekContainsDate,
useAdditionalWeekYearTokens: props.useAdditionalWeekYearTokens,
useAdditionalDayOfYearTokens: props.useAdditionalDayOfYearTokens,
timeZone: props.timeZone,
+ numerals: numerals ?? "arabext",
+ noonSafe,
+ overrides: dateLibProp,
});
return (
);
@@ -95,6 +108,23 @@ export function DayPicker(
* @param options - Optional configuration for the date library.
* @returns The date library instance.
*/
-export const getDateLib = (options?: DateLibOptions) => {
- return new DateLib(options, dateFnsJalali);
+export const getDateLib = (
+ options?: DateLibOptions & {
+ noonSafe?: boolean;
+ overrides?: DayPickerProps["dateLib"];
+ },
+) => {
+ const { noonSafe, overrides, ...dateLibOptions } = options ?? {};
+ const baseOverrides =
+ noonSafe && dateLibOptions.timeZone
+ ? {
+ ...dateFnsJalali,
+ ...createJalaliNoonOverrides(dateLibOptions.timeZone, {
+ weekStartsOn: dateLibOptions.weekStartsOn,
+ locale: dateLibOptions.locale,
+ }),
+ }
+ : dateFnsJalali;
+
+ return new DateLib(dateLibOptions, { ...baseOverrides, ...overrides });
};
diff --git a/src/types/props.ts b/src/types/props.ts
index 5787138f5..099d2b291 100644
--- a/src/types/props.ts
+++ b/src/types/props.ts
@@ -304,6 +304,14 @@ export interface PropsBase {
* @see https://daypicker.dev/docs/time-zone
*/
timeZone?: string | undefined;
+ /**
+ * Keep calendar math at noon in the configured {@link timeZone} to avoid
+ * historical second-level offsets drifting days across midnight.
+ *
+ * @since 9.13.0
+ * @experimental
+ */
+ noonSafe?: boolean | undefined;
/**
* Change the components used for rendering the calendar elements.
*
diff --git a/src/utils/convertMatchersToTimeZone.ts b/src/utils/convertMatchersToTimeZone.ts
index ce7007531..164429d4d 100644
--- a/src/utils/convertMatchersToTimeZone.ts
+++ b/src/utils/convertMatchersToTimeZone.ts
@@ -1,3 +1,4 @@
+import { TZDate } from "@date-fns/tz";
import type { Matcher } from "../types/index.js";
import { toTimeZone } from "./toTimeZone.js";
import {
@@ -7,18 +8,37 @@ import {
isDateRange,
} from "./typeguards.js";
-function convertMatcher(matcher: Matcher, timeZone: string): Matcher {
+function toZoneNoon(date: Date, timeZone: string, noonSafe?: boolean) {
+ if (!noonSafe) return toTimeZone(date, timeZone);
+ const zoned = toTimeZone(date, timeZone);
+ const noonZoned = new TZDate(
+ zoned.getFullYear(),
+ zoned.getMonth(),
+ zoned.getDate(),
+ 12,
+ 0,
+ 0,
+ timeZone,
+ );
+ return new Date(noonZoned.getTime());
+}
+
+function convertMatcher(
+ matcher: Matcher,
+ timeZone: string,
+ noonSafe?: boolean,
+): Matcher {
if (typeof matcher === "boolean" || typeof matcher === "function") {
return matcher;
}
if (matcher instanceof Date) {
- return toTimeZone(matcher, timeZone);
+ return toZoneNoon(matcher, timeZone, noonSafe);
}
if (Array.isArray(matcher)) {
return matcher.map((value) =>
- value instanceof Date ? toTimeZone(value, timeZone) : value,
+ value instanceof Date ? toZoneNoon(value, timeZone, noonSafe) : value,
);
}
@@ -32,20 +52,20 @@ function convertMatcher(matcher: Matcher, timeZone: string): Matcher {
if (isDateInterval(matcher)) {
return {
- before: toTimeZone(matcher.before, timeZone),
- after: toTimeZone(matcher.after, timeZone),
+ before: toZoneNoon(matcher.before, timeZone, noonSafe),
+ after: toZoneNoon(matcher.after, timeZone, noonSafe),
};
}
if (isDateAfterType(matcher)) {
return {
- after: toTimeZone(matcher.after, timeZone),
+ after: toZoneNoon(matcher.after, timeZone, noonSafe),
};
}
if (isDateBeforeType(matcher)) {
return {
- before: toTimeZone(matcher.before, timeZone),
+ before: toZoneNoon(matcher.before, timeZone, noonSafe),
};
}
@@ -63,14 +83,17 @@ function convertMatcher(matcher: Matcher, timeZone: string): Matcher {
export function convertMatchersToTimeZone(
matchers: Matcher | Matcher[] | undefined,
timeZone: string,
+ noonSafe?: boolean,
): Matcher | Matcher[] | undefined {
if (!matchers) {
return matchers;
}
if (Array.isArray(matchers)) {
- return matchers.map((matcher) => convertMatcher(matcher, timeZone));
+ return matchers.map((matcher) =>
+ convertMatcher(matcher, timeZone, noonSafe),
+ );
}
- return convertMatcher(matchers, timeZone);
+ return convertMatcher(matchers, timeZone, noonSafe);
}
diff --git a/website/docs/localization/setting-time-zone.mdx b/website/docs/localization/setting-time-zone.mdx
index 772721f05..3e3929cd0 100644
--- a/website/docs/localization/setting-time-zone.mdx
+++ b/website/docs/localization/setting-time-zone.mdx
@@ -9,9 +9,10 @@ By default, DayPicker uses the browser’s local time zone. You can override thi
The time zone can be specified as either an [IANA time zone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) identifier or a UTC offset.
-| Prop Name | Type | Description |
-| ---------- | -------- | ------------------------------------------------------ |
-| `timeZone` | `string` | The IANA time zone to use when rendering the calendar. |
+| Prop Name | Type | Description |
+| ---------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `timeZone` | `string` | The IANA time zone to use when rendering the calendar. |
+| `noonSafe` | `boolean` | Experimental (see below). Keep calendar math at noon in the time zone to avoid historical drift and use when your time zone shows duplicated/missing days. |
```tsx
// Coordinated Universal Time
@@ -53,3 +54,25 @@ export function TimeZone() {
+
+## Historical time zones with second offsets
+
+Some historical time zones use offsets with seconds (for example `Asia/Dubai` in
+1900 is `+03:41:12`). JavaScript `Date` and `date-fns` round offsets to minutes,
+which can push midnight calculations into the previous day. Set `noonSafe` to
+keep calendar math at 12:00 in the chosen time zone so the date never drifts.
+
+```ts
+const timeZone = "Asia/Dubai";
+
+;
+```
+
+This override is optional and only needed when you render months that include
+offsets with seconds and want to avoid duplicate or missing days in the grid. Use
+it when you notice time-zone related glitches such as missing days, duplicate
+days, or unexpected week padding.
+
+
+
+
diff --git a/website/src/components/Playground/LocalizationFieldset.tsx b/website/src/components/Playground/LocalizationFieldset.tsx
index 1e6faf87a..e712f3844 100644
--- a/website/src/components/Playground/LocalizationFieldset.tsx
+++ b/website/src/components/Playground/LocalizationFieldset.tsx
@@ -271,6 +271,7 @@ export function LocalizationFieldset({
calendar: undefined,
locale: undefined,
timeZone: undefined,
+ noonSafe: undefined,
numerals: undefined,
weekStartsOn: undefined,
firstWeekContainsDate: undefined,
@@ -320,6 +321,22 @@ export function LocalizationFieldset({
))}
+ {props.timeZone ? (
+
+ ) : null}