/** * What a human types into a timecode field, turned into a position * in a piece of media — or into a stated reason it was not one. * * ### Milliseconds, and only milliseconds * * A media position is a **duration from the start of the file**, so * the value is an integer count of milliseconds and nothing else. It * is not a `Date`, it is not a clock time, and it is not a string: * the only arithmetic anybody does with it is subtraction, and a * number is the type that supports it. The props that carry one keep * the `Ms` suffix, because a duration's unit is genuinely ambiguous * and the fleet has both seconds and milliseconds in use * ([decision](../../../../docs/decisions/2026-08-25-an-outbound-http-cache-names-its-fields-fetchedat-and-expiresat.md)). * * ### The fleet writes the printing half five times and the reading * half never * * `formatTimecode` replaces five hand-rolled `h:mm:ss` printers — * queuepilot's `clock` and `toClock`, castkit's `formatTime`, and * mux-magic's two modal formatters — each with its own answer to * whether the hour is shown and whether the minute is padded. * `parseTimecodeInput` has no counterpart at all: nothing in the * fleet *reads* a timecode a person typed, which is why the grammar * gets the care here rather than being rediscovered per app. * * ### The rule that governs the grammar: never guess in silence * * The same three mechanisms `parseDateInput.ts` states, for the same * reason: * * 1. **The grammar is ordered and total.** Each accepting rule is * anchored on the *whole* input, the first match wins, and if * none matches the answer is a named failure. `1:` is not one * minute, and `1:90` is not `2:30`. * 2. **An overflow is a failure with a name, not a carry.** `1:90` * is refused, and the message offers both legal spellings — * `2:30` and `90` — without committing to either. Silently * carrying 90 seconds into the minute is the `1:60` trap * mux-magic wrote down in * `docs/audits/2026-06-29-pre-rename-domain-decisions.md`. * 3. **Nothing commits without an echo.** `TimecodeInput` renders * the resolved position in full underneath the field, in a live * region, before anything commits. * * ### The largest field present is unbounded; every smaller one is * 0–59 * * One sentence covers every bound in the grammar. `90` is ninety * **seconds**, `90:00` is ninety **minutes**, and `1:90` is refused * because the minute beside it makes 90 a seconds field. That is why * a bare number is seconds rather than minutes: seconds is the * smallest unit the field prints, so the reading that needs no * explanation is the one where the typist appended nothing. */ /** * A clip window, in milliseconds. Keys alphabetised, matching * `DateRange`. * * **Both ends are independently optional**, and the four * combinations are four real states rather than three plus an error: * no window at all, from here to the end of the media, from the * beginning up to here, and the window between two points. An open * start is not zero — it is the absence of a choice — which is what * lets a half-open window be clamped without being compared. */ export type TimecodeRange = { end: null | number; start: null | number; }; export type TimecodeInputResult = { kind: "empty"; } | { kind: "timecode"; milliseconds: number; } | { kind: "unparsed"; reason: string; }; export type FormatTimecodeOptions = { /** * `true` — the default — prints the hours field even when it is * `00`, so every value the field writes has the same shape and the * same width. `false` rolls the hours into the minutes * (`5400000` → `90:00.000`), which the grammar reads back * unchanged because the largest field present is unbounded. */ isHoursShown?: boolean; /** * `0` through `3`. `0` drops the fraction and its separator. * * `"auto"` prints the fraction only when there is one to print: a * whole number of seconds comes back as `01:01:00`, and half a * second more comes back as `01:01:00.500`. It is how a decimal * number is written everywhere else — you write `3`, not `3.000` — * and the grammar reads both spellings back to the number that * produced them, because its fraction is optional. */ millisecondDigits?: "auto" | number; }; /** * A position in a piece of media, printed. * * Zero-padded, and **truncating** rather than rounding: at * `millisecondDigits: 0` a position 1999 ms into a file is second 1, * because it has not reached second 2 yet. Rounding would print a * seek point the media has not played. * * The default is the canonical `hh:mm:ss.mmm`. `TimecodeInput` asks * for `millisecondDigits: "auto"` instead, so a whole second is * written back as `01:01:00` rather than as `01:01:00.000` — a * person who typed no fraction did not ask for one, and both * spellings re-read to the number that produced them. * * Either way it is ONE spelling per value, which is what the fleet's * five printers never were: three of them hide the hour and two do * not pad the minute, so the same position is a different picture in * each app. */ export declare const formatTimecode: (milliseconds: number, { isHoursShown, millisecondDigits, }?: FormatTimecodeOptions) => string; /** * Hold a position inside the media. * * The floor is `0` when no `minimum` is given, because a media * position before the start of the file does not exist — the same * reason the grammar refuses a negative rather than clamping one * silently. An inverted pair of bounds resolves to the floor: a * caller whose `maximum` sits below its `minimum` has a bug, and * quietly widening the window would hide it somewhere nobody looks. */ export declare const clampTimecode: (milliseconds: number, minimum?: number, maximum?: number) => number; /** * Read a typed timecode, or say why it is not one. * * Takes no options, deliberately. Every knob considered belongs * somewhere else: a maximum is the field's clamp at commit rather * than a parse failure (two sources for one fact is how a control * ends up invalid with nothing saying why), the decimal comma is * accepted unconditionally instead of being a locale switch, and a * frame rate makes this a different widget. An options bag that * means nothing is API a consumer has to read before learning there * is nothing in it. */ export declare const parseTimecodeInput: (rawText: string) => TimecodeInputResult; //# sourceMappingURL=timecode.d.ts.map