# Date picker

> Date and range pickers on a sunken trigger: a popover holds the calendar, presets like Last 7 days and Apply/Cancel, plus a segmented date field whose digits roll as you step them.

- Collection: Components
- Tier: free
- Docs: https://geturui.com/docs/date-picker
- Install: `npx shadcn@latest add @geturui/date-picker`
- Dependencies: motion, lucide-react

## Usage

```tsx
import { DateField, DatePicker, DateRangePicker, type DateRange } from "@/components/ui/date-picker";

export function ReportControls() {
  const [launch, setLaunch] = React.useState<Date | null>(null);
  const [period, setPeriod] = React.useState<DateRange>({ start: null, end: null });
  return (
    <>
      <DatePicker label="Launch date" value={launch} onValueChange={setLaunch} />
      <DateRangePicker label="Report period" value={period} onValueChange={setPeriod} />
      <DateField label="Renewal date" />
    </>
  );
}
```

## Props

| Prop | Type | Default | Description |
|---|---|---|---|
| value / defaultValue / onValueChange | `Date | null (DatePicker) · DateRange (DateRangePicker)` | — | Committed value; changes only on Apply. |
| label / hint / error / required | `ReactNode / boolean` | — | Wired through Field, including aria-describedby and the error shake. |
| size | `"sm" | "md" | "lg"` | "md" | Trigger height 32 / 40 / 48. |
| today | `Date` | — | Reference day for the marker, presets and Today button. Omit to read the clock after hydration. |
| min / max / disabledDates / isDateDisabled / highlightedDates / weekStartsOn | `see Calendar` | — | Passed to the calendar. |
| confirm | `boolean` | true | DatePicker: require Apply. When false, picking a day commits and closes. |
| presets | `DatePreset[] | false` | defaultRangePresets | DateRangePicker: { id, label, range(today) } quick ranges in a side column (a scroll row on phones). |
| numberOfMonths | `1 | 2` | — | DateRangePicker: defaults to 2 from 640px up, 1 below. |
| open / defaultOpen / onOpenChange / align | `boolean / start | center | end` | — | Popover state and alignment. |
| DateField | `component` | — | Segmented mm/dd/yyyy input: value, onValueChange, order (mdy | dmy | ymd), min, max, name, size, label, hint, error, bare. |

## Accessibility

- The trigger is a button labelled by the field label plus the current value; the popover is a labelled dialog that returns focus on close and cancels on Escape.
- DateField segments are spinbuttons with aria-valuetext ("October"); arrows step, digits type and auto-advance, Backspace clears.
- Presets are toggle buttons with aria-pressed; the selected length is announced politely.
- The calendar inside follows the APG date grid keyboard pattern.

## Source

### components/ui/date-picker.tsx

```tsx
"use client";

import * as React from "react";
import { motion, useReducedMotion } from "motion/react";
import { CalendarDays, ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Popover } from "@/components/ui/popover";
import { Field, fieldWellClasses, type FieldSize } from "@/components/ui/field";
import { DateField } from "@/components/ui/date-field";
import {
  Calendar,
  addDays,
  addMonths,
  compareDays,
  daysBetween,
  formatDate,
  formatRange,
  isSameDay,
  startOfMonth,
  useToday,
  type DateRange,
  type WeekStart,
} from "@/components/ui/calendar";

export { DateField, type DateFieldProps } from "@/components/ui/date-field";
export type { DateRange } from "@/components/ui/calendar";

/** A named range, computed from today so it stays deterministic. */
export interface DatePreset {
  id: string;
  label: string;
  range: (today: Date) => DateRange;
}

/** Today, Yesterday, Last 7 / 30 / 90 days, This month, Last month, Year to date. */
export const defaultRangePresets: DatePreset[] = [
  { id: "today", label: "Today", range: (t) => ({ start: t, end: t }) },
  { id: "yesterday", label: "Yesterday", range: (t) => ({ start: addDays(t, -1), end: addDays(t, -1) }) },
  { id: "7d", label: "Last 7 days", range: (t) => ({ start: addDays(t, -6), end: t }) },
  { id: "30d", label: "Last 30 days", range: (t) => ({ start: addDays(t, -29), end: t }) },
  { id: "90d", label: "Last 90 days", range: (t) => ({ start: addDays(t, -89), end: t }) },
  { id: "month", label: "This month", range: (t) => ({ start: startOfMonth(t), end: t }) },
  {
    id: "last-month",
    label: "Last month",
    range: (t) => ({ start: addMonths(startOfMonth(t), -1), end: addDays(startOfMonth(t), -1) }),
  },
  { id: "ytd", label: "Year to date", range: (t) => ({ start: new Date(t.getFullYear(), 0, 1), end: t }) },
];

interface PickerBaseProps {
  label?: React.ReactNode;
  hint?: React.ReactNode;
  error?: React.ReactNode;
  required?: boolean;
  disabled?: boolean;
  /** Trigger height: 32 / 40 / 48. @default "md" */
  size?: FieldSize;
  placeholder?: string;
  /** The day treated as today (marker, presets, Today button). Omit to read the clock after hydration. */
  today?: Date;
  min?: Date;
  max?: Date;
  disabledDates?: Date[];
  isDateDisabled?: (date: Date) => boolean;
  highlightedDates?: Date[];
  /** @default 0 (Sunday) */
  weekStartsOn?: WeekStart;
  open?: boolean;
  defaultOpen?: boolean;
  onOpenChange?: (open: boolean) => void;
  /** Panel alignment under the trigger. @default "start" */
  align?: "start" | "center" | "end";
  className?: string;
}

export interface DatePickerProps extends PickerBaseProps {
  value?: Date | null;
  defaultValue?: Date | null;
  onValueChange?: (date: Date | null) => void;
  /** Require Apply to commit. When false, picking a day commits and closes. @default true */
  confirm?: boolean;
}

export interface DateRangePickerProps extends PickerBaseProps {
  value?: DateRange;
  defaultValue?: DateRange;
  onValueChange?: (range: DateRange) => void;
  /** Quick ranges in a side column. Pass false to hide. @default defaultRangePresets */
  presets?: DatePreset[] | false;
  /** Months shown. Defaults to 2 from 640px up, 1 below. */
  numberOfMonths?: 1 | 2;
}

const EMPTY: DateRange = { start: null, end: null };
const subscribeWide = (cb: () => void) => {
  const mq = window.matchMedia("(min-width: 640px)");
  mq.addEventListener("change", cb);
  return () => mq.removeEventListener("change", cb);
};
const useWide = () =>
  React.useSyncExternalStore(
    subscribeWide,
    () => window.matchMedia("(min-width: 640px)").matches,
    () => false,
  );

function useOpen(open: boolean | undefined, defaultOpen: boolean | undefined, onOpenChange?: (v: boolean) => void) {
  const [own, setOwn] = React.useState(Boolean(defaultOpen));
  const isOpen = open ?? own;
  const set = (v: boolean) => {
    if (open === undefined) setOwn(v);
    onOpenChange?.(v);
  };
  return [isOpen, set] as const;
}

interface TriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  size: FieldSize;
  invalid: boolean;
  open: boolean;
  text: string | null;
  placeholder: string;
  valueId: string;
}

/** Sunken-well trigger button. Popover clones it to add onClick and aria-expanded. */
const Trigger = React.forwardRef<HTMLButtonElement, TriggerProps>(function Trigger(
  { size, invalid, open, text, placeholder, valueId, disabled, className, ...props },
  ref,
) {
  const reduce = useReducedMotion();
  return (
    <button
      ref={ref}
      type="button"
      disabled={disabled}
      className={fieldWellClasses({
        size,
        invalid,
        disabled,
        className: cn(
          "w-full cursor-pointer text-left outline-none focus-visible:border-volt-ink focus-visible:shadow-[0_0_0_3px_var(--volt-soft)]",
          open && "border-volt-ink",
          className,
        ),
      })}
      {...props}
    >
      <CalendarDays aria-hidden className="size-4 shrink-0" />
      <span id={valueId} className={cn("min-w-0 flex-1 truncate tabular-nums", text ? "text-ink" : "text-ink-subtle")}>
        {text ?? placeholder}
      </span>
      <motion.span
        aria-hidden
        animate={{ rotate: open && !reduce ? 180 : 0 }}
        transition={{ type: "spring", stiffness: 420, damping: 28 }}
        className="grid"
      >
        <ChevronDown className="size-4" />
      </motion.span>
    </button>
  );
});

/** Single-date picker: a sunken trigger opens a calendar with a typed date field, Today shortcut and Apply/Cancel. */
export function DatePicker({
  value,
  defaultValue = null,
  onValueChange,
  label,
  hint,
  error,
  required,
  disabled,
  size = "md",
  placeholder = "Select a date",
  today: todayProp,
  min,
  max,
  disabledDates,
  isDateDisabled,
  highlightedDates,
  weekStartsOn,
  open,
  defaultOpen,
  onOpenChange,
  confirm = true,
  align = "start",
  className,
}: DatePickerProps) {
  const today = useToday(todayProp);
  const valueId = React.useId();
  const [own, setOwn] = React.useState<Date | null>(defaultValue);
  const current = value !== undefined ? value : own;
  const [draft, setDraft] = React.useState<Date | null>(current);
  const [month, setMonth] = React.useState<Date | undefined>(current ?? todayProp ?? undefined);
  const [isOpen, setOpen] = useOpen(open, defaultOpen, onOpenChange);

  const commit = (d: Date | null) => {
    if (value === undefined) setOwn(d);
    onValueChange?.(d);
  };
  const handleOpen = (v: boolean) => {
    if (v) {
      setDraft(current);
      setMonth(current ?? today ?? undefined);
    }
    setOpen(v);
  };

  return (
    <Field label={label} hint={hint} error={error} required={required} disabled={disabled} className={className}>
      {(ids) => (
        <div className="grid">
          <Popover
            open={isOpen}
            onOpenChange={handleOpen}
            align={align}
            label="Choose a date"
            className="w-max max-w-[calc(100vw-16px)] p-0"
            trigger={
              <Trigger
                id={ids.id}
                size={size}
                invalid={ids.invalid}
                open={isOpen}
                disabled={disabled}
                text={current ? formatDate(current) : null}
                placeholder={placeholder}
                valueId={valueId}
                aria-labelledby={label ? `${ids.labelId} ${valueId}` : undefined}
                aria-describedby={ids.describedBy}
                aria-invalid={ids.invalid || undefined}
              />
            }
          >
            {({ close }) => (
              <div className="grid w-min gap-3 p-4">
                <div className="flex items-center gap-2">
                  <DateField
                    bare
                    size="sm"
                    aria-label="Date"
                    value={draft}
                    min={min}
                    max={max}
                    onValueChange={(d) => {
                      setDraft(d);
                      if (d) setMonth(d);
                    }}
                    className="flex-1"
                  />
                  <Button
                    size="sm"
                    variant="secondary"
                    disabled={!today}
                    onClick={() => {
                      if (!today) return;
                      setDraft(today);
                      setMonth(today);
                    }}
                  >
                    Today
                  </Button>
                </div>
                <Calendar
                  value={draft}
                  onValueChange={(d) => {
                    setDraft(d);
                    if (!confirm) {
                      commit(d);
                      close();
                    }
                  }}
                  month={month}
                  onMonthChange={setMonth}
                  today={todayProp}
                  min={min}
                  max={max}
                  disabledDates={disabledDates}
                  isDateDisabled={isDateDisabled}
                  highlightedDates={highlightedDates}
                  weekStartsOn={weekStartsOn}
                />
                {confirm && (
                  <div className="flex items-center justify-end gap-2 border-t border-border pt-3">
                    <Button size="sm" variant="ghost" onClick={close}>
                      Cancel
                    </Button>
                    <Button
                      size="sm"
                      disabled={isSameDay(draft, current) || (!draft && !current)}
                      onClick={() => {
                        commit(draft);
                        close();
                      }}
                    >
                      Apply
                    </Button>
                  </div>
                )}
              </div>
            )}
          </Popover>
        </div>
      )}
    </Field>
  );
}

/** Range picker: presets column, one or two months with a sweeping range band, typed start/end fields and Apply/Cancel. */
export function DateRangePicker({
  value,
  defaultValue = EMPTY,
  onValueChange,
  label,
  hint,
  error,
  required,
  disabled,
  size = "md",
  placeholder = "Select dates",
  today: todayProp,
  min,
  max,
  disabledDates,
  isDateDisabled,
  highlightedDates,
  weekStartsOn,
  open,
  defaultOpen,
  onOpenChange,
  align = "start",
  presets = defaultRangePresets,
  numberOfMonths,
  className,
}: DateRangePickerProps) {
  const reduce = useReducedMotion();
  const uid = React.useId();
  const valueId = React.useId();
  const today = useToday(todayProp);
  const wide = useWide();
  const months = numberOfMonths ?? (wide ? 2 : 1);
  const [own, setOwn] = React.useState<DateRange>(defaultValue);
  const current = value ?? own;
  const [draft, setDraft] = React.useState<DateRange>(current);
  const [month, setMonth] = React.useState<Date | undefined>(current.start ?? todayProp ?? undefined);
  const [isOpen, setOpen] = useOpen(open, defaultOpen, onOpenChange);

  const monthFor = (r: DateRange) => {
    const anchor = r.end ?? r.start ?? today;
    return anchor ? addMonths(startOfMonth(anchor), -(months - 1)) : undefined;
  };
  const handleOpen = (v: boolean) => {
    if (v) {
      setDraft(current);
      setMonth(monthFor(current));
    }
    setOpen(v);
  };
  const commit = (r: DateRange) => {
    if (value === undefined) setOwn(r);
    onValueChange?.(r);
  };

  const sameRange = (a: DateRange, b: DateRange) =>
    (a.start ? !!b.start && isSameDay(a.start, b.start) : !b.start) && (a.end ? !!b.end && isSameDay(a.end, b.end) : !b.end);
  const activePreset = presets && today ? presets.find((p) => sameRange(p.range(today), draft))?.id : undefined;
  const length = draft.start && draft.end ? daysBetween(draft.start, draft.end) + 1 : 0;
  const complete = !!draft.start && !!draft.end;

  return (
    <Field label={label} hint={hint} error={error} required={required} disabled={disabled} className={className}>
      {(ids) => (
        <div className="grid">
          <Popover
            open={isOpen}
            onOpenChange={handleOpen}
            align={align}
            label="Choose a date range"
            className="w-max max-w-[calc(100vw-16px)] overflow-hidden p-0"
            trigger={
              <Trigger
                id={ids.id}
                size={size}
                invalid={ids.invalid}
                open={isOpen}
                disabled={disabled}
                text={current.start ? formatRange(current) : null}
                placeholder={placeholder}
                valueId={valueId}
                aria-labelledby={label ? `${ids.labelId} ${valueId}` : undefined}
                aria-describedby={ids.describedBy}
                aria-invalid={ids.invalid || undefined}
              />
            }
          >
            {({ close }) => (
              <div className="flex flex-col sm:flex-row">
                {presets && presets.length > 0 && (
                  <div
                    role="group"
                    aria-label="Quick ranges"
                    className="flex gap-1 overflow-x-auto border-b border-border p-2 sm:w-40 sm:flex-col sm:overflow-visible sm:border-b-0 sm:border-r sm:p-3"
                  >
                    {presets.map((p) => {
                      const active = activePreset === p.id;
                      return (
                        <button
                          key={p.id}
                          type="button"
                          aria-pressed={active}
                          disabled={!today}
                          onClick={() => {
                            if (!today) return;
                            const r = p.range(today);
                            setDraft(r);
                            setMonth(monthFor(r));
                          }}
                          className={cn(
                            "relative shrink-0 whitespace-nowrap rounded-full px-3 py-1.5 text-left text-[13px] font-medium outline-none transition-colors duration-150 focus-visible:shadow-[var(--focus-ring)] sm:rounded-[10px]",
                            active ? "text-volt-ink" : "text-ink-muted hover:bg-surface-hover hover:text-ink",
                          )}
                        >
                          {active && (
                            <motion.span
                              aria-hidden
                              layoutId={reduce ? undefined : `${uid}-preset`}
                              transition={{ type: "spring", stiffness: 420, damping: 34 }}
                              className="absolute inset-0 rounded-[inherit] bg-volt-soft"
                            />
                          )}
                          <span className="relative">{p.label}</span>
                        </button>
                      );
                    })}
                  </div>
                )}
                <div className="grid w-min gap-3 p-4">
                  <Calendar
                    mode="range"
                    value={draft}
                    onValueChange={setDraft}
                    month={month}
                    onMonthChange={setMonth}
                    numberOfMonths={months}
                    today={todayProp}
                    min={min}
                    max={max}
                    disabledDates={disabledDates}
                    isDateDisabled={isDateDisabled}
                    highlightedDates={highlightedDates}
                    weekStartsOn={weekStartsOn}
                  />
                  <div className="flex flex-wrap items-center gap-x-3 gap-y-3 border-t border-border pt-3">
                    <div className="flex items-center gap-2">
                      <DateField
                        bare
                        size="sm"
                        icon={null}
                        aria-label="Start date"
                        value={draft.start}
                        min={min}
                        max={max}
                        onValueChange={(d) => {
                          const next = { start: d, end: d && draft.end && compareDays(d, draft.end) <= 0 ? draft.end : null };
                          setDraft(next);
                          if (d) setMonth(monthFor({ start: d, end: null }));
                        }}
                        className="w-[120px]"
                      />
                      <span aria-hidden className="text-ink-subtle">
                        –
                      </span>
                      <DateField
                        bare
                        size="sm"
                        icon={null}
                        aria-label="End date"
                        value={draft.end}
                        min={draft.start ?? min}
                        max={max}
                        onValueChange={(d) => {
                          setDraft({ start: draft.start ?? d, end: d });
                          if (d) setMonth(monthFor({ start: null, end: d }));
                        }}
                        className="w-[120px]"
                      />
                    </div>
                    <p className="text-xs text-ink-subtle" aria-live="polite">
                      {complete ? `${length} ${length === 1 ? "day" : "days"}` : draft.start ? "Pick an end date" : "Pick a start date"}
                    </p>
                    <div className="ml-auto flex items-center gap-2">
                      <Button size="sm" variant="ghost" onClick={close}>
                        Cancel
                      </Button>
                      <Button
                        size="sm"
                        disabled={!complete || sameRange(draft, current)}
                        onClick={() => {
                          commit(draft);
                          close();
                        }}
                      >
                        Apply
                      </Button>
                    </div>
                  </div>
                </div>
              </div>
            )}
          </Popover>
        </div>
      )}
    </Field>
  );
}

```

### components/ui/date-field.tsx

```tsx
"use client";

import * as React from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { CalendarDays } from "lucide-react";
import { cn } from "@/lib/utils";
import { Field, fieldWellClasses, type FieldSize } from "@/components/ui/field";
import { MONTH_NAMES, compareDays, daysInMonth, startOfDay, toISODate } from "@/components/ui/calendar";

type SegKey = "month" | "day" | "year";
type Segs = Record<SegKey, number | null>;

export interface DateFieldProps {
  value?: Date | null;
  defaultValue?: Date | null;
  /** Called with a full date once every segment is filled, and with null when one is cleared. */
  onValueChange?: (date: Date | null) => void;
  label?: React.ReactNode;
  hint?: React.ReactNode;
  error?: React.ReactNode;
  required?: boolean;
  disabled?: boolean;
  size?: FieldSize;
  /** Dates before this are clamped up when committed. */
  min?: Date;
  /** Dates after this are clamped down when committed. */
  max?: Date;
  /** Segment order. @default "mdy" */
  order?: "mdy" | "dmy" | "ymd";
  /** Leading glyph. Pass null to hide. */
  icon?: React.ReactNode;
  /** Right side of the well, e.g. a calendar button. */
  trailing?: React.ReactNode;
  /** Submits the value as YYYY-MM-DD under this name. */
  name?: string;
  /** Accessible name when there is no visible label. */
  "aria-label"?: string;
  /** Render only the well, without label and hint (for use inside other controls). */
  bare?: boolean;
  className?: string;
}

const EMPTY: Segs = { month: null, day: null, year: null };
const toSegs = (d: Date | null | undefined): Segs => (d ? { month: d.getMonth() + 1, day: d.getDate(), year: d.getFullYear() } : EMPTY);
const META: Record<SegKey, { label: string; placeholder: string; min: number; width: string }> = {
  month: { label: "Month", placeholder: "mm", min: 1, width: "min-w-[2.1ch]" },
  day: { label: "Day", placeholder: "dd", min: 1, width: "min-w-[2.1ch]" },
  year: { label: "Year", placeholder: "yyyy", min: 1, width: "min-w-[4.2ch]" },
};

function maxOf(seg: SegKey, s: Segs) {
  if (seg === "month") return 12;
  if (seg === "year") return 9999;
  return s.month ? daysInMonth(s.year ?? 2000, s.month - 1) : 31;
}

/** Typed date input in month, day and year segments: arrows step a segment with a rolling digit, a volt pill slides to the focused one. */
export function DateField({
  value,
  defaultValue,
  onValueChange,
  label,
  hint,
  error,
  required,
  disabled,
  size = "md",
  min,
  max,
  order = "mdy",
  icon,
  trailing,
  name,
  bare,
  className,
  ...rest
}: DateFieldProps) {
  const reduce = useReducedMotion();
  const uid = React.useId();
  const [segs, setSegs] = React.useState<Segs>(() => toSegs(value !== undefined ? value : defaultValue));
  const [focused, setFocused] = React.useState<SegKey | null>(null);
  const [roll, setRoll] = React.useState(1);
  const typed = React.useRef("");
  const wellRef = React.useRef<HTMLDivElement>(null);

  const order3: SegKey[] =
    order === "dmy" ? ["day", "month", "year"] : order === "ymd" ? ["year", "month", "day"] : ["month", "day", "year"];
  const sep = order === "ymd" ? "-" : "/";

  const build = (s: Segs): Date | null => {
    if (!s.month || !s.day || !s.year || s.year < 1000) return null;
    let d = new Date(s.year, s.month - 1, Math.min(s.day, daysInMonth(s.year, s.month - 1)));
    if (min && compareDays(d, min) < 0) d = startOfDay(min);
    if (max && compareDays(d, max) > 0) d = startOfDay(max);
    return d;
  };

  // Follow a controlled value without clobbering a half-typed entry.
  const valueKey = value === undefined ? undefined : value ? toISODate(value) : "";
  const [prevKey, setPrevKey] = React.useState(valueKey);
  if (valueKey !== prevKey) {
    setPrevKey(valueKey);
    const shown = build(segs);
    // Skip echoes of our own change (including null while a segment is half-typed).
    if (valueKey !== undefined && valueKey !== (shown ? toISODate(shown) : "")) setSegs(toSegs(value));
  }

  const update = (next: Segs) => {
    const before = build(segs);
    const after = build(next);
    setSegs(after ? toSegs(after) : next);
    const k = (d: Date | null) => (d ? toISODate(d) : "");
    if (k(before) !== k(after)) onValueChange?.(after);
  };

  const focusSeg = (seg: SegKey | undefined) => {
    if (!seg) return;
    wellRef.current?.querySelector<HTMLElement>(`[data-seg="${seg}"]`)?.focus();
  };
  const neighbour = (seg: SegKey, step: 1 | -1) => order3[order3.indexOf(seg) + step];

  const onKeyDown = (seg: SegKey) => (e: React.KeyboardEvent) => {
    const cur = segs[seg];
    const hi = maxOf(seg, segs);
    const lo = META[seg].min;
    if (e.key === "ArrowUp" || e.key === "ArrowDown") {
      e.preventDefault();
      const step = e.key === "ArrowUp" ? 1 : -1;
      typed.current = "";
      setRoll(step);
      // Empty segments start from the clock; this only runs in an event, never during render.
      const now = new Date();
      const start = seg === "year" ? now.getFullYear() : seg === "month" ? now.getMonth() + 1 : now.getDate();
      const nextVal =
        cur === null
          ? Math.min(start, hi)
          : seg === "year"
            ? Math.max(1000, cur + step)
            : ((cur - lo + step + (hi - lo + 1)) % (hi - lo + 1)) + lo;
      update({ ...segs, [seg]: nextVal });
      return;
    }
    if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
      e.preventDefault();
      focusSeg(neighbour(seg, e.key === "ArrowRight" ? 1 : -1));
      return;
    }
    if (e.key === "Home" || e.key === "End") {
      e.preventDefault();
      setRoll(e.key === "Home" ? -1 : 1);
      update({ ...segs, [seg]: e.key === "Home" ? (seg === "year" ? 1000 : lo) : seg === "year" ? 9999 : hi });
      return;
    }
    if (e.key === "Backspace" || e.key === "Delete") {
      e.preventDefault();
      if (typed.current.length > 1) {
        typed.current = typed.current.slice(0, -1);
        update({ ...segs, [seg]: Number(typed.current) });
      } else if (cur !== null) {
        typed.current = "";
        update({ ...segs, [seg]: null });
      } else focusSeg(neighbour(seg, -1));
      return;
    }
    if (e.key === "/" || e.key === "-" || e.key === ".") {
      e.preventDefault();
      focusSeg(neighbour(seg, 1));
      return;
    }
    if (/^\d$/.test(e.key)) {
      e.preventDefault();
      const digit = Number(e.key);
      setRoll(1);
      if (seg === "year") {
        const buf = (typed.current + e.key).slice(-4);
        typed.current = buf.length === 4 ? "" : buf;
        update({ ...segs, year: Number(buf) });
        if (buf.length === 4) focusSeg(neighbour(seg, 1));
        return;
      }
      const limit = seg === "month" ? 12 : 31;
      const combined = Number(typed.current + e.key);
      if (typed.current && combined <= limit && combined >= 1) {
        typed.current = "";
        update({ ...segs, [seg]: combined });
        focusSeg(neighbour(seg, 1));
      } else if (digit * 10 > limit) {
        typed.current = "";
        update({ ...segs, [seg]: Math.max(digit, 1) });
        if (digit > 0) focusSeg(neighbour(seg, 1));
      } else {
        typed.current = e.key;
        update({ ...segs, [seg]: digit === 0 ? segs[seg] : digit });
      }
    }
  };

  const describe = (seg: SegKey, v: number | null) => {
    if (v === null) return "Empty";
    return seg === "month" ? MONTH_NAMES[v - 1] : String(v);
  };
  const show = (seg: SegKey, v: number | null) =>
    v === null ? META[seg].placeholder : seg === "year" ? String(v) : String(v).padStart(2, "0");
  const current = build(segs);

  const well = (ids?: { id: string; labelId: string; describedBy?: string; invalid: boolean }) => (
    <div
      ref={wellRef}
      role="group"
      id={ids?.id}
      aria-labelledby={ids && label ? ids.labelId : undefined}
      aria-label={ids && label ? undefined : rest["aria-label"]}
      aria-describedby={ids?.describedBy}
      aria-disabled={disabled || undefined}
      onMouseDown={(e) => {
        if (disabled || (e.target as HTMLElement).closest("[data-seg],button")) return;
        e.preventDefault();
        focusSeg(order3.find((s) => segs[s] === null) ?? order3[0]);
      }}
      className={fieldWellClasses({
        size,
        invalid: ids?.invalid ?? Boolean(error),
        disabled,
        className: cn("cursor-text", bare && className),
      })}
    >
      {icon !== null && (icon ?? <CalendarDays aria-hidden className="size-4 shrink-0" />)}
      <span className="flex flex-1 items-center font-mono text-ink">
        {order3.map((seg, i) => (
          <React.Fragment key={seg}>
            {i > 0 && (
              <span aria-hidden className="px-0.5 text-ink-subtle">
                {sep}
              </span>
            )}
            <span
              role="spinbutton"
              data-seg={seg}
              tabIndex={disabled ? -1 : 0}
              aria-label={META[seg].label}
              aria-valuemin={seg === "year" ? 1000 : META[seg].min}
              aria-valuemax={maxOf(seg, segs)}
              aria-valuenow={segs[seg] ?? undefined}
              aria-valuetext={describe(seg, segs[seg])}
              aria-required={required || undefined}
              aria-invalid={(ids?.invalid ?? Boolean(error)) || undefined}
              onKeyDown={onKeyDown(seg)}
              onFocus={() => {
                typed.current = "";
                setFocused(seg);
              }}
              onBlur={() => setFocused((f) => (f === seg ? null : f))}
              className={cn(
                "relative inline-flex justify-center rounded-[6px] px-0.5 py-px text-center tabular-nums caret-transparent outline-none focus-visible:shadow-none",
                META[seg].width,
                segs[seg] === null && "text-ink-subtle",
              )}
            >
              {focused === seg && (
                <motion.span
                  aria-hidden
                  layoutId={reduce ? undefined : `${uid}-pill`}
                  transition={{ type: "spring", stiffness: 520, damping: 36 }}
                  className="absolute inset-0 rounded-[6px] bg-volt-soft ring-1 ring-volt-ink/40"
                />
              )}
              <span aria-hidden className="relative inline-grid overflow-hidden">
                <AnimatePresence mode="popLayout" initial={false} custom={roll}>
                  <motion.span
                    key={show(seg, segs[seg])}
                    custom={roll}
                    initial={reduce ? { opacity: 0 } : { y: roll * 10, opacity: 0, filter: "blur(3px)" }}
                    animate={{ y: 0, opacity: 1, filter: "blur(0px)" }}
                    exit={reduce ? { opacity: 0 } : { y: roll * -10, opacity: 0, filter: "blur(3px)" }}
                    transition={{ type: "spring", stiffness: 520, damping: 34 }}
                    className={cn("block", focused === seg && segs[seg] !== null && "text-ink")}
                  >
                    {show(seg, segs[seg])}
                  </motion.span>
                </AnimatePresence>
              </span>
            </span>
          </React.Fragment>
        ))}
      </span>
      {trailing}
      {name && <input type="hidden" name={name} value={current ? toISODate(current) : ""} />}
    </div>
  );

  if (bare) return well();
  return (
    <Field label={label} hint={hint} error={error} required={required} disabled={disabled} className={className}>
      {(ids) => well(ids)}
    </Field>
  );
}

```
