{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "date-time-segments",
  "title": "Date Time Segments",
  "description": "The spinbutton segment engine behind the date and time fields.",
  "files": [
    {
      "path": "registry/tealmac/date-time-segments.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/**\n * Segmented input engine shared by DateField, TimeField and their relatives.\n * Each segment is a `spinbutton` (WAI-ARIA): digits type into it and advance\n * when the value is complete, Up/Down step with wrap-around, PageUp/PageDown\n * take a bigger step, Home/End jump to the limits, Backspace clears, and the\n * arrow keys move between segments (reversed in right-to-left text).\n */\n\nexport type SegmentKind =\n  \"day\" | \"month\" | \"year\" | \"hour\" | \"minute\" | \"second\" | \"dayPeriod\"\n\nexport interface SegmentSpec {\n  kind: SegmentKind\n  /** Accessible name: \"day\", \"month\", \"hour\". */\n  label: string\n  /** Digits shown when filled: 2 for \"05\", 4 for a year. */\n  digits: number\n  /** Shown while empty. */\n  placeholder: string\n  /** Step of PageUp/PageDown. */\n  pageStep?: number\n  /** Step of ArrowUp/ArrowDown. */\n  step?: number\n}\n\nexport type SegmentItem =\n  { type: \"segment\"; spec: SegmentSpec } | { type: \"literal\"; text: string }\n\nexport type SegmentValues = Partial<Record<SegmentKind, number>>\n\nexport interface SegmentLimits {\n  min: number\n  max: number\n}\n\nexport interface SegmentGroupProps extends Omit<\n  React.ComponentProps<\"div\">,\n  \"onChange\" | \"role\"\n> {\n  items: SegmentItem[]\n  values: SegmentValues\n  limits: (kind: SegmentKind, values: SegmentValues) => SegmentLimits\n  onSegmentChange: (kind: SegmentKind, value: number | undefined) => void\n  /** Text for the screen reader and the display: \"05\", \"PM\". */\n  format: (kind: SegmentKind, value: number) => string\n  /** Letters that pick the day period: [am, pm] first letters. */\n  periodKeys?: [string, string]\n  disabled?: boolean\n  readOnly?: boolean\n  invalid?: boolean\n  /** Value written to a hidden input for native forms. */\n  name?: string\n  formValue?: string\n  /** Value a segment takes on its first ArrowUp/ArrowDown. */\n  initial?: (kind: SegmentKind) => number\n}\n\nfunction readDirection(el: HTMLElement | null) {\n  const holder = el?.closest(\"[dir]\") as HTMLElement | null\n  return (holder?.getAttribute(\"dir\") ??\n    (typeof document !== \"undefined\" ? document.documentElement.dir : \"\")) ===\n    \"rtl\"\n    ? \"rtl\"\n    : \"ltr\"\n}\n\nfunction SegmentGroup({\n  items,\n  values,\n  limits,\n  onSegmentChange,\n  format,\n  periodKeys = [\"a\", \"p\"],\n  disabled,\n  readOnly,\n  invalid,\n  name,\n  formValue,\n  initial,\n  className,\n  children,\n  ...props\n}: SegmentGroupProps) {\n  const root = React.useRef<HTMLDivElement>(null)\n  const buffer = React.useRef<{ kind: SegmentKind; text: string }>({\n    kind: \"day\",\n    text: \"\",\n  })\n\n  function focusSegment(from: SegmentKind, delta: 1 | -1) {\n    // Fields that hold several groups (a range, a date and a time) share a scope,\n    // so typing and the arrow keys carry on into the next group.\n    const scope =\n      root.current?.closest<HTMLElement>(\"[data-segment-scope]\") ?? root.current\n    const nodes = Array.from(\n      scope?.querySelectorAll<HTMLElement>(\"[data-segment]\") ?? []\n    )\n    const at = nodes.findIndex((n) => n === document.activeElement)\n    nodes[at + delta]?.focus()\n  }\n\n  function commitDigit(spec: SegmentSpec, digit: number) {\n    const { min, max } = limits(spec.kind, values)\n    let text =\n      buffer.current.kind === spec.kind\n        ? buffer.current.text + digit\n        : String(digit)\n    if (Number(text) > max) text = String(digit)\n    if (Number(text) > max) return\n    const n = Number(text)\n    buffer.current = { kind: spec.kind, text }\n    // A hand-typed hour of 0 in a 1-12 field waits for the next digit.\n    onSegmentChange(spec.kind, n === 0 && min > 0 ? min : n)\n    const full = text.length >= spec.digits || n * 10 > max\n    if (full) {\n      buffer.current = { kind: spec.kind, text: \"\" }\n      focusSegment(spec.kind, 1)\n    }\n  }\n\n  function onKeyDown(spec: SegmentSpec, e: React.KeyboardEvent<HTMLElement>) {\n    if (disabled || readOnly) return\n    if (e.ctrlKey || e.metaKey || e.altKey) return\n    const { min, max } = limits(spec.kind, values)\n    const current = values[spec.kind]\n    const rtl = readDirection(root.current) === \"rtl\"\n\n    if (/^\\d$/.test(e.key) && spec.kind !== \"dayPeriod\") {\n      e.preventDefault()\n      commitDigit(spec, Number(e.key))\n      return\n    }\n    switch (e.key) {\n      case \"ArrowUp\":\n      case \"ArrowDown\":\n      case \"PageUp\":\n      case \"PageDown\": {\n        e.preventDefault()\n        const dir = e.key === \"ArrowUp\" || e.key === \"PageUp\" ? 1 : -1\n        const step = (e.key.startsWith(\"Page\") ? spec.pageStep : spec.step) ?? 1\n        if (current === undefined) {\n          onSegmentChange(\n            spec.kind,\n            initial ? initial(spec.kind) : dir > 0 ? min : max\n          )\n        } else {\n          const span = max - min + 1\n          const next =\n            ((((current - min + dir * step) % span) + span) % span) + min\n          onSegmentChange(spec.kind, next)\n        }\n        buffer.current = { kind: spec.kind, text: \"\" }\n        return\n      }\n      case \"Home\":\n        e.preventDefault()\n        onSegmentChange(spec.kind, min)\n        return\n      case \"End\":\n        e.preventDefault()\n        onSegmentChange(spec.kind, max)\n        return\n      case \"Backspace\":\n      case \"Delete\": {\n        e.preventDefault()\n        const text =\n          buffer.current.kind === spec.kind ? buffer.current.text : \"\"\n        if (text.length > 1) {\n          const rest = text.slice(0, -1)\n          buffer.current = { kind: spec.kind, text: rest }\n          onSegmentChange(spec.kind, Number(rest))\n        } else {\n          buffer.current = { kind: spec.kind, text: \"\" }\n          onSegmentChange(spec.kind, undefined)\n        }\n        return\n      }\n      case \"ArrowLeft\":\n        e.preventDefault()\n        focusSegment(spec.kind, rtl ? 1 : -1)\n        return\n      case \"ArrowRight\":\n        e.preventDefault()\n        focusSegment(spec.kind, rtl ? -1 : 1)\n        return\n      default:\n    }\n    if (spec.kind === \"dayPeriod\" && e.key.length === 1) {\n      const k = e.key.toLowerCase()\n      if (k === periodKeys[0].toLowerCase()) {\n        e.preventDefault()\n        onSegmentChange(\"dayPeriod\", 0)\n      } else if (k === periodKeys[1].toLowerCase()) {\n        e.preventDefault()\n        onSegmentChange(\"dayPeriod\", 1)\n      }\n      return\n    }\n    // Everything printable is swallowed so the editable span stays untouched.\n    if (e.key.length === 1) e.preventDefault()\n  }\n\n  return (\n    <div\n      ref={root}\n      role=\"group\"\n      data-slot=\"segment-group\"\n      data-disabled={disabled ? \"\" : undefined}\n      data-invalid={invalid ? \"\" : undefined}\n      className={cn(\n        \"border-input bg-background dark:bg-input/30 inline-flex h-9 min-w-0 items-center rounded-md border px-2.5 text-sm shadow-xs transition-[color,box-shadow]\",\n        \"focus-within:border-ring focus-within:ring-ring/50 focus-within:ring-[3px]\",\n        \"data-[invalid]:border-destructive data-[invalid]:focus-within:ring-destructive/20 dark:data-[invalid]:focus-within:ring-destructive/40\",\n        \"data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50\",\n        className\n      )}\n      {...props}\n    >\n      {items.map((item, i) => {\n        if (item.type === \"literal\")\n          return (\n            <span\n              key={`l${i}`}\n              aria-hidden=\"true\"\n              className=\"text-muted-foreground px-px select-none\"\n            >\n              {item.text}\n            </span>\n          )\n        const { spec } = item\n        const value = values[spec.kind]\n        const { min, max } = limits(spec.kind, values)\n        const text =\n          value === undefined ? spec.placeholder : format(spec.kind, value)\n        return (\n          <span\n            key={spec.kind}\n            role=\"spinbutton\"\n            data-segment={spec.kind}\n            data-placeholder={value === undefined ? \"\" : undefined}\n            aria-label={spec.label}\n            aria-valuemin={min}\n            aria-valuemax={max}\n            aria-valuenow={value}\n            aria-valuetext={value === undefined ? \"Empty\" : text}\n            aria-disabled={disabled || undefined}\n            aria-readonly={readOnly || undefined}\n            aria-invalid={invalid || undefined}\n            tabIndex={disabled ? -1 : 0}\n            contentEditable={!disabled && !readOnly}\n            suppressContentEditableWarning\n            inputMode=\"numeric\"\n            spellCheck={false}\n            autoCorrect=\"off\"\n            autoCapitalize=\"off\"\n            onFocus={() => {\n              buffer.current = { kind: spec.kind, text: \"\" }\n            }}\n            onKeyDown={(e) => onKeyDown(spec, e)}\n            onBeforeInput={(e) => {\n              // Soft keyboards report text here instead of through keydown.\n              e.preventDefault()\n              const data = (e.nativeEvent as InputEvent).data ?? \"\"\n              if (\n                !disabled &&\n                !readOnly &&\n                /^\\d$/.test(data) &&\n                spec.kind !== \"dayPeriod\"\n              )\n                commitDigit(spec, Number(data))\n            }}\n            onPaste={(e) => e.preventDefault()}\n            onDrop={(e) => e.preventDefault()}\n            className={cn(\n              \"caret-transparent outline-none selection:bg-transparent\",\n              \"rounded-sm px-0.5 text-center tabular-nums\",\n              \"focus:bg-primary focus:text-primary-foreground\",\n              \"data-[placeholder]:text-muted-foreground data-[placeholder]:focus:text-primary-foreground\",\n              spec.kind === \"dayPeriod\" && \"uppercase\"\n            )}\n          >\n            {text}\n          </span>\n        )\n      })}\n      {name ? (\n        <input type=\"hidden\" name={name} value={formValue ?? \"\"} />\n      ) : null}\n      {children}\n    </div>\n  )\n}\n\n/** Uncontrolled-or-controlled value helper shared by the fields. */\nfunction useControllableValue<T>(\n  value: T | undefined,\n  defaultValue: T,\n  onChange?: (value: T) => void\n) {\n  const [inner, setInner] = React.useState<T>(defaultValue)\n  const controlled = value !== undefined\n  const current = controlled ? (value as T) : inner\n  const set = React.useCallback(\n    (next: T) => {\n      if (!controlled) setInner(next)\n      onChange?.(next)\n    },\n    [controlled, onChange]\n  )\n  return [current, set] as const\n}\n\nexport { SegmentGroup, useControllableValue }\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}