Variable

TimecodeInput

Text input for entering timeline positions as timecode. Use `TimecodeInput` when a toolbar, inspector, or properties panel needs a native text input for clip starts, clip ends, playhead positions, or range boundaries. It keeps flexible entry open to the user (`90`, `1:30.25`, `1:02:03.4567`, and `00:01:30:12` can all represent timeline positions) while the companion helpers parse precise input and format display text consistently. This component renders only the input. Pair it with `parseTimecodeInput` to validate the current text, pass `invalid` when parsing returns `null`, and convert valid seconds back to `RationalTime` with `fromSeconds(parsed, rate)` at your timeline boundary. The rendered input receives `data-slot="timecode-input"` and a `timecode-input` class before consumer classes, which keeps it compatible with shadcn-style slot selectors and Canvas Timeline's semantic stylesheet approach. It forwards refs to the underlying native input element.

Signature

Type Definition
TimecodeInput: ForwardRefExoticComponent<TimecodeInputProps & RefAttributes<HTMLInputElement>>

Examples

tsx Example
import { useState } from 'react';
import {
TimecodeInput,
type TimecodeInputFormatOptions,
type TimecodeInputParseOptions,
formatTimecodeInput,
parseTimecodeInput,
} from '@techsquidtv/canvas-timeline-react/timecode-input';
import { fromSeconds, type RationalTime } from '@techsquidtv/canvas-timeline-utils';
const formatOptions = [
{ value: 'seconds', label: 'Seconds', formatOptions: { format: 'seconds' } },
{
value: 'frames-24',
label: '24 fps',
formatOptions: { format: 'frames', frameRate: 24 },
parseOptions: { frameRate: 24 },
},
] satisfies Array<{
value: string;
label: string;
formatOptions: TimecodeInputFormatOptions;
parseOptions?: TimecodeInputParseOptions;
}>;
const sequenceRate = 24000;
const initialSeconds = 3723.04;
function ClipStartInput({
onApply,
}: {
onApply: (time: RationalTime) => void;
}) {
const [formatValue, setFormatValue] = useState('seconds');
const [text, setText] = useState(() =>
formatTimecodeInput(initialSeconds, { format: 'seconds' })
);
const selectedFormat =
formatOptions.find((option) => option.value === formatValue) ?? formatOptions[0];
const parsedSeconds = parseTimecodeInput(text, selectedFormat.parseOptions);
function handleFormatChange(nextFormatValue: string) {
const nextFormat = formatOptions.find((option) => option.value === nextFormatValue);
const nextSeconds = parseTimecodeInput(text, selectedFormat.parseOptions);
if (!nextFormat) {
return;
}
setFormatValue(nextFormat.value);
if (nextSeconds !== null) {
setText(formatTimecodeInput(nextSeconds, nextFormat.formatOptions));
}
}
return (
<form
onSubmit={(event) => {
event.preventDefault();
if (parsedSeconds !== null) {
onApply(fromSeconds(parsedSeconds, sequenceRate));
setText(formatTimecodeInput(parsedSeconds, selectedFormat.formatOptions));
}
}}
>
<TimecodeInput
aria-label="Clip start"
value={text}
invalid={parsedSeconds === null}
onValueChange={setText}
/>
<select
aria-label="Timecode format"
value={formatValue}
onChange={(event) => handleFormatChange(event.currentTarget.value)}
>
{formatOptions.map((option) => (
<option key={option.value} value={option.value}>
{parsedSeconds === null
? option.label
: `${option.label} (${formatTimecodeInput(parsedSeconds, {
...option.formatOptions,
})})`}
</option>
))}
</select>
<button disabled={parsedSeconds === null} type="submit">
Apply
</button>
</form>
);
}