Moltenship Time
Concepts

Time and time zones

Timed, all-day, and floating events, exclusive end times, and daylight saving time.

Time uses the Temporal API for every date and time. There are no Date objects or millisecond timestamps in the API, so a date never shifts by a day because of a hidden time zone conversion.

Three kinds of events

// A call at 15:00 UTC. Shown at 17:00 in Berlin and 11:00 in New York.
const call = {
  kind: "timed",
  start: Temporal.Instant.from("2026-10-08T15:00:00Z"),
  end: Temporal.Instant.from("2026-10-08T16:00:00Z"),
};

// A public holiday. October 3 in every time zone.
const holiday = {
  kind: "all-day",
  start: Temporal.PlainDate.from("2026-10-03"),
  end: Temporal.PlainDate.from("2026-10-04"),
};

// Gym at 07:00, in whatever time zone you're in that day.
const gym = {
  kind: "floating",
  start: Temporal.PlainDateTime.from("2026-10-08T07:00"),
  end: Temporal.PlainDateTime.from("2026-10-08T08:00"),
};
  • Timed events happen at an exact moment. Time converts them to the calendar's time zone for display.
  • All-day events cover whole dates and ignore time zones.
  • Floating events have a wall-clock time but no time zone. They show at the same clock time whichever zone the calendar uses.

A timed range can also carry sourceTimeZone, the zone the event was created in. Time keeps it on the event for your views to use; display still uses the calendar's zone.

End times are exclusive

Every range includes its start and excludes its end. This is often written [start, end).

  • A 9:00–10:00 meeting and a 10:00–11:00 meeting don't overlap.
  • A one-day all-day event on October 3 ends on October 4.
  • An all-day event covering October 3 and 4 ends on October 5.

The start must be strictly before the end. Zero-length and reversed ranges are reported as invalid.

The calendar's time zone

Root takes an IANA time zone name, either as defaultTimeZone or as a controlled timeZone. Time doesn't read the browser's or the server's zone. You choose it, typically from user settings.

const actions = Calendar.useCalendarActions();

actions.setTimeZone("America/New_York");

Changing the zone moves timed events to their new local times. All-day and floating events stay where they are.

To use the visitor's own zone, read Temporal.Now.timeZoneId() in the browser. If you render on the server, store the zone with the user or pass it from the client after hydration, so the server and the browser render the same thing. See Next.js and server rendering.

Daylight saving time

Not every day has 24 hours. In Berlin, March 29, 2026 has 23 hours and October 25, 2026 has 25:

Temporal.PlainDate.from("2026-03-29").toZonedDateTime("Europe/Berlin").hoursInDay; // 23
Temporal.PlainDate.from("2026-10-25").toZonedDateTime("Europe/Berlin").hoursInDay; // 25

So some wall-clock times never happen (02:30 on March 29) and some happen twice (02:30 on October 25). The disambiguation prop on Root decides which moment such a time means:

ValueSkipped time (gap)Repeated time (fold)
"compatible" (default)Moves later by the gapThe earlier one
"earlier"Moves earlier by the gapThe earlier one
"later"Moves later by the gapThe later one
"reject"Throws a RangeErrorThrows a RangeError
Temporal.PlainDateTime.from("2026-03-29T02:30")
  .toZonedDateTime("Europe/Berlin", { disambiguation: "compatible" })
  .toString(); // "2026-03-29T03:30:00+02:00[Europe/Berlin]"

Views get the setting as environment.disambiguation. When you build a view, these helpers from @moltenship/calendar already account for these days:

  • zonedDayRange(date, timeZone) returns the real start and end of a day, 23 or 25 hours long when needed.
  • slotsInZonedDay(date, options) splits a day into time slots and marks skipped and repeated hours.
  • resolveZonedWallTime(wallTime, { timeZone, disambiguation }) tells you how a wall-clock time resolved, including how far it moved.

See the Temporal helpers reference.

The current time

Time reads the current time from one of three places, in this order:

  • now on Root: a fixed Temporal.Instant. Use it when you render on the server, so the server and the browser agree on "today".
  • clock on Root: an object with an instant() method, for a value you control, such as a fake clock in tests. Define it once at module scope.
  • The system clock, when you pass neither.

Without now, nothing that depends on the current time renders on the server: a view's today is null and useNow() returns null until the page has hydrated. The Today button always works; it reads the clock when it's clicked.

useNow() gives a component the current instant, for example to draw a line at the current time. It re-renders every intervalMs (one minute by default). This one uses the Calendar from the Quick start:

import { useNow } from "@moltenship/calendar";

function CurrentTime() {
  const now = useNow({ intervalMs: 30_000 });
  const { timeZone } = Calendar.useCalendarState();
  if (now === null) return null;

  const time = now.toZonedDateTimeISO(timeZone).toPlainTime();
  return <time dateTime={now.toString()}>{time.toString({ smallestUnit: "minute" })}</time>;
}
Edit on GitHub

Last updated on

On this page