# TimeField

## Summary

`TimeField` is a [compound component](https://vega-ui.dev/docs/glossary/#compound-component) for masked time entry. It builds on `TextField` and adds format, min/max, and step constraints for time strings when the product value is time-only.

## Imports

```tsx
import {
  TimeField,
  TimeFieldInput,
  type TimeFieldInputProps,
  type TimeFieldProps,
} from '@vega-ui/react';
```

## Exported Types

- `TimeFieldProps`
- `TimeFieldInputProps`

## Minimal Composition

```tsx
import { TimeField, TimeFieldInput } from '@vega-ui/react';

export default function MinimalComposition() {
  return (
    <TimeField>
      <TimeFieldInput aria-label='Reminder time' placeholder='hh:mm' />
    </TimeField>
  );
}
```

## Required Parts

- `TimeField`: root time constraints and format provider
- `TimeFieldInput`: masked text input

## Optional Parts

- no extra public parts beyond the input

## Composition Order

1. `TimeField`
2. `TimeFieldInput`

## Variants

- Format: `HH:mm` or `HH:mm:ss`
- Bounds: optional `min` and `max`
- State: controlled or uncontrolled `value` on the root, disabled through input props

## Edge Cases

- Format, `min`, and `max` must agree with each other.
- Partial typed values should not be treated as complete times too early.
- Users need clear 24-hour format expectations in product copy.
- If the feature really stores a timestamp, `DateTimeField` is usually the clearer abstraction than splitting date and time policy ad hoc.

## Common Mistakes

- Assuming locale formatting instead of documenting the expected mask.
- Passing format-specific bounds that do not match the chosen format.
- Treating incomplete masked text as valid submission data.

## Examples

### Basic

#### Basic: daily reminder time

A labelled field for a 24-hour time. `defaultValue` on the root is an `HH:mm` string, and the helper text tells users which clock and time zone the time is in.

```tsx
import { HelperText, Label, TimeField, TimeFieldInput } from '@vega-ui/react';

export default function ReminderTime() {
  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-3)', width: '100%', maxWidth: 260 }}>
      <Label htmlFor='daily-reminder'>Daily reminder</Label>
      <TimeField defaultValue='08:30'>
        <TimeFieldInput id='daily-reminder' placeholder='hh:mm' aria-describedby='daily-reminder-hint' />
      </TimeField>
      <HelperText id='daily-reminder-hint'>24-hour time in your local time zone.</HelperText>
    </div>
  );
}
```

### Controlled/Stateful

#### Controlled/Stateful: shift start and end

`value` and `onChangeValue` on the root keep both times in parent state. The start is the `min` of the end field, so an end before the start is clamped, and the shift length is computed from the two values. While a time is incomplete its value is `''`.

```tsx
import { useState } from 'react';
import { Fieldset, FieldsetLegend, Label, Text, TimeField, TimeFieldInput } from '@vega-ui/react';

const toMinutes = (time: string) => {
  const [hours, minutes] = time.split(':').map(Number);

  return hours * 60 + minutes;
};

const describeShift = (start: string, end: string) => {
  if (!start || !end) return 'Enter both times';

  const length = toMinutes(end) - toMinutes(start);

  return `${Math.floor(length / 60)} h ${length % 60} min`;
};

export default function ShiftHours() {
  const [start, setStart] = useState('09:00');
  const [end, setEnd] = useState('17:30');

  return (
    <Fieldset style={{ display: 'grid', gap: 'var(--spacing-8)', width: '100%', maxWidth: 260 }}>
      <FieldsetLegend style={{ marginBlockEnd: 'var(--spacing-6)' }}>Shift</FieldsetLegend>
      <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
        <Label htmlFor='shift-start'>Starts</Label>
        <TimeField value={start} onChangeValue={setStart}>
          <TimeFieldInput id='shift-start' placeholder='hh:mm' />
        </TimeField>
      </div>
      <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
        <Label htmlFor='shift-end'>Ends</Label>
        <TimeField min={start} value={end} onChangeValue={setEnd}>
          <TimeFieldInput id='shift-end' placeholder='hh:mm' />
        </TimeField>
      </div>
      <Text size={2} aria-live='polite' style={{ color: 'var(--label-secondary)' }}>
        Length: {describeShift(start, end)}
      </Text>
    </Fieldset>
  );
}
```

### Form/Integration

#### Form/Integration: submitting a time

For `HH:mm` and `HH:mm:ss` the typed text already is the value, so `name` on `TimeFieldInput` submits it as is. `required` and `pattern` block an empty or incomplete time, and the reset button restores `defaultValue` of the root.

```tsx
import { FormEvent, useState } from 'react';
import { Button, HelperText, Label, Text, TimeField, TimeFieldInput } from '@vega-ui/react';

export default function QuietHoursForm() {
  const [submitted, setSubmitted] = useState('');

  const onSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    setSubmitted(JSON.stringify(Object.fromEntries(data)));
  };

  return (
    <form onSubmit={onSubmit} onReset={() => setSubmitted('')} style={{ display: 'grid', gap: 'var(--spacing-12)', width: '100%', maxWidth: 260 }}>
      <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
        <Label htmlFor='quiet-from'>Mute notifications from</Label>
        <TimeField defaultValue='22:00'>
          <TimeFieldInput
            id='quiet-from'
            name='quietFrom'
            required
            pattern='\d{2}:\d{2}'
            placeholder='hh:mm'
            aria-describedby='quiet-from-hint'
          />
        </TimeField>
        <HelperText id='quiet-from-hint'>24-hour time, for example 22:00.</HelperText>
      </div>
      <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 'var(--spacing-4)' }}>
        <Button type='reset' variant='secondary' appearance='ghost'>Reset</Button>
        <Button type='submit'>Save</Button>
      </div>
      {submitted && <Text size={2} style={{ color: 'var(--label-secondary)', overflowWrap: 'anywhere' }}>Sent: <code>{submitted}</code></Text>}
    </form>
  );
}
```

### Layout/Overlay

#### Layout/Overlay: opening hours table

Small fields inside table cells, one row per weekday. Each input has no visible label of its own, so it gets an `aria-label` that names the day and the column.

```tsx
import {
  Table,
  TableBody,
  TableData,
  TableHead,
  TableHeading,
  TableRow,
  TimeField,
  TimeFieldInput,
} from '@vega-ui/react';

const DAYS = [
  { day: 'Monday', opens: '09:00', closes: '18:00' },
  { day: 'Tuesday', opens: '09:00', closes: '18:00' },
  { day: 'Wednesday', opens: '09:00', closes: '20:00' },
  { day: 'Thursday', opens: '09:00', closes: '18:00' },
  { day: 'Friday', opens: '10:00', closes: '16:00' },
];

export default function OpeningHoursTable() {
  return (
    <Table aria-label='Opening hours'>
      <TableHead>
        <TableRow>
          <TableHeading>Day</TableHeading>
          <TableHeading>Opens</TableHeading>
          <TableHeading>Closes</TableHeading>
        </TableRow>
      </TableHead>
      <TableBody>
        {DAYS.map(({ day, opens, closes }) => (
          <TableRow key={day}>
            <TableData>{day}</TableData>
            <TableData>
              <TimeField size='sm' defaultValue={opens} style={{ width: 96 }}>
                <TimeFieldInput name={`${day.toLowerCase()}Opens`} aria-label={`${day} opens`} />
              </TimeField>
            </TableData>
            <TableData>
              <TimeField size='sm' defaultValue={closes} style={{ width: 96 }}>
                <TimeFieldInput name={`${day.toLowerCase()}Closes`} aria-label={`${day} closes`} />
              </TimeField>
            </TableData>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  );
}
```

### Error

#### Error: time outside an overnight window

`min` and `max` cannot describe a window that crosses midnight, so the field has no bounds and the submit handler checks the 22:00–06:00 rule itself. `error` and `aria-invalid` mark the field, and the next change of the value clears the error.

```tsx
import { FormEvent, useState } from 'react';
import { Button, HelperText, Label, TimeField, TimeFieldInput } from '@vega-ui/react';

const isOvernight = (time: string) => time >= '22:00' || time <= '06:00';

export default function NightDeliveryForm() {
  const [time, setTime] = useState('');
  const [error, setError] = useState('');
  const invalid = error !== '';

  const onChangeValue = (value: string) => {
    setTime(value);
    setError('');
  };

  const onSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();

    if (time && !isOvernight(time)) {
      setError('Night deliveries arrive between 22:00 and 06:00.');
    }
  };

  return (
    <form onSubmit={onSubmit} style={{ display: 'grid', gap: 'var(--spacing-12)', width: '100%', maxWidth: 260 }}>
      <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
        <Label htmlFor='night-delivery'>Night delivery at</Label>
        <TimeField error={invalid} value={time} onChangeValue={onChangeValue}>
          <TimeFieldInput
            id='night-delivery'
            required
            placeholder='hh:mm'
            aria-invalid={invalid}
            aria-describedby='night-delivery-hint'
          />
        </TimeField>
        <HelperText id='night-delivery-hint' error={invalid}>{invalid ? error : 'Between 22:00 and 06:00.'}</HelperText>
      </div>
      <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 'var(--spacing-4)' }}>
        <Button type='submit'>Book</Button>
      </div>
    </form>
  );
}
```

### Disabled

#### Disabled: read-only schedule slot

`readOnly` on `TimeFieldInput` keeps the time focusable and submitted with the form while users cannot edit it.

```tsx
import { HelperText, Label, TimeField, TimeFieldInput } from '@vega-ui/react';

export default function ReadOnlyScheduleSlot() {
  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-3)', width: '100%', maxWidth: 260 }}>
      <Label htmlFor='backup-slot'>Nightly backup</Label>
      <TimeField defaultValue='03:00'>
        <TimeFieldInput id='backup-slot' name='backupSlot' readOnly aria-describedby='backup-slot-hint' />
      </TimeField>
      <HelperText id='backup-slot-hint'>Set by your administrator.</HelperText>
    </div>
  );
}
```

#### Disabled: disabled by a fieldset

`TimeField` has no `disabled` prop of its own: disable the input, or a surrounding `<fieldset disabled>` as here, where the switch turns both times off for an all-day event.

```tsx
import { useState } from 'react';
import {
  Fieldset,
  FieldsetLegend,
  Label,
  Switch,
  SwitchIndicator,
  SwitchInput,
  TimeField,
  TimeFieldInput,
} from '@vega-ui/react';

export default function DisabledByFieldset() {
  const [allDay, setAllDay] = useState(true);

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-12)', width: '100%', maxWidth: 260 }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-4)' }}>
        <Switch>
          <SwitchInput id='all-day' checked={allDay} onChange={(event) => setAllDay(event.currentTarget.checked)} />
          <SwitchIndicator />
        </Switch>
        <Label htmlFor='all-day'>All-day event</Label>
      </div>
      <Fieldset disabled={allDay} style={{ display: 'grid', gap: 'var(--spacing-8)' }}>
        <FieldsetLegend style={{ marginBlockEnd: 'var(--spacing-6)' }}>Time</FieldsetLegend>
        <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
          <Label htmlFor='event-start'>Starts</Label>
          <TimeField defaultValue='10:00'>
            <TimeFieldInput id='event-start' name='eventStart' />
          </TimeField>
        </div>
        <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
          <Label htmlFor='event-end'>Ends</Label>
          <TimeField defaultValue='11:00'>
            <TimeFieldInput id='event-end' name='eventEnd' />
          </TimeField>
        </div>
      </Fieldset>
    </div>
  );
}
```

### Edge

#### Edge: bound time window

Like a native `<input type="time">`, a complete time outside `min` and `max` is clamped to the nearest bound, so `07:15` becomes `09:00`. The readout shows the clamped value.

```tsx
import { useState } from 'react';
import { HelperText, Label, Text, TimeField, TimeFieldInput } from '@vega-ui/react';

export default function BoundTimeWindow() {
  const [callback, setCallback] = useState('');

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-8)', width: '100%', maxWidth: 260 }}>
      <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
        <Label htmlFor='callback-time'>Call me back at</Label>
        <TimeField min='09:00' max='18:00' value={callback} onChangeValue={setCallback}>
          <TimeFieldInput id='callback-time' placeholder='hh:mm' aria-describedby='callback-time-hint' />
        </TimeField>
        <HelperText id='callback-time-hint'>Support works 09:00–18:00. Try 07:15 or 19:40.</HelperText>
      </div>
      <Text size={2} style={{ color: 'var(--label-secondary)' }}>
        Value: {callback ? <code>{callback}</code> : 'no complete time'}
      </Text>
    </div>
  );
}
```

#### Edge: quarter-hour step

`step` sets how far `ArrowUp` and `ArrowDown` move the minutes under the caret, wrapping from `45` to `00` without changing the hour. The hours always move by one, and typed minutes are kept as typed.

```tsx
import { HelperText, Label, TimeField, TimeFieldInput } from '@vega-ui/react';

export default function QuarterHourStep() {
  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-3)', width: '100%', maxWidth: 260 }}>
      <Label htmlFor='class-start'>Class starts</Label>
      <TimeField step={15} defaultValue='18:45'>
        <TimeFieldInput id='class-start' aria-describedby='class-start-hint' />
      </TimeField>
      <HelperText id='class-start-hint'>Put the caret in the minutes and press ↑ or ↓.</HelperText>
    </div>
  );
}
```

#### Edge: seconds precision

`format='HH:mm:ss'` adds a seconds segment; the value then has seconds, and `min` and `max` are written with seconds too.

```tsx
import { useState } from 'react';
import { Label, Text, TimeField, TimeFieldInput } from '@vega-ui/react';

export default function SecondsPrecisionRule() {
  const [runAt, setRunAt] = useState('00:00:30');

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-8)', width: '100%', maxWidth: 260 }}>
      <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
        <Label htmlFor='cron-time'>Run the export at</Label>
        <TimeField format='HH:mm:ss' min='00:00:30' max='23:59:30' value={runAt} onChangeValue={setRunAt}>
          <TimeFieldInput id='cron-time' placeholder='hh:mm:ss' />
        </TimeField>
      </div>
      <Text size={2} style={{ color: 'var(--label-secondary)' }}>
        Value: {runAt ? <code>{runAt}</code> : 'no complete time'}
      </Text>
    </div>
  );
}
```

## Anatomy

### Overview

`TimeField` is a small compound wrapper around `TextField`. The root provides format and bound context, and `TimeFieldInput` applies the masking behavior.

### Required Parts

#### `TimeField`

Required. Owns `format`, `min`, `max`, and `step` context.

#### `TimeFieldInput`

Required. Renders the actual masked input.

### Optional Parts

There are no extra public parts beyond the input.

### Composition Order

1. `TimeField`
2. `TimeFieldInput`

### Valid Composition Patterns

```tsx
import { TimeField, TimeFieldInput } from '@vega-ui/react';

export default function MinimalComposition() {
  return (
    <TimeField>
      <TimeFieldInput aria-label='Reminder time' placeholder='hh:mm' />
    </TimeField>
  );
}
```

### Invalid Composition Patterns

#### Bounds that do not match the format

Example: `format='HH:mm'` with a seconds-based `max`.

#### Wrapper without `TimeFieldInput`

The root renders, but there is no masked control.
