{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "time-picker",
  "title": "Time Picker",
  "description": "A controlled or uncontrolled time picker with 12- and 24-hour cycles, optional seconds, stepped controls, and form values.",
  "dependencies": [
    "@base-ui/react",
    "lucide-react"
  ],
  "files": [
    {
      "path": "registry/rivetui/components/time-picker/time-picker.tsx",
      "content": "\"use client\";\n\nimport { NumberField as NumberFieldPrimitive } from \"@base-ui/react/number-field\";\nimport { Popover as PopoverPrimitive } from \"@base-ui/react/popover\";\nimport { ChevronDown, ChevronUp, Clock3 } from \"lucide-react\";\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\ntype TimeParts = {\n  hours: number;\n  minutes: number;\n  seconds: number;\n};\n\ntype TimePickerProps = Omit<\n  React.ComponentProps<\"button\">,\n  \"defaultValue\" | \"name\" | \"onChange\" | \"value\"\n> & {\n  defaultOpen?: boolean;\n  defaultValue?: string;\n  hourCycle?: 12 | 24;\n  minuteStep?: number;\n  name?: string;\n  onOpenChange?: PopoverPrimitive.Root.Props[\"onOpenChange\"];\n  onValueChange?: (value: string | undefined) => void;\n  open?: boolean;\n  placeholder?: string;\n  secondStep?: number;\n  showSeconds?: boolean;\n  value?: string;\n};\n\nconst emptyTime: TimeParts = { hours: 12, minutes: 0, seconds: 0 };\n\nfunction TimePicker({\n  children,\n  className,\n  defaultOpen,\n  defaultValue,\n  disabled,\n  hourCycle = 24,\n  minuteStep: minuteStepProp = 1,\n  name,\n  onOpenChange,\n  onValueChange,\n  open,\n  placeholder = \"Select time\",\n  secondStep: secondStepProp = 1,\n  showSeconds = false,\n  value: valueProp,\n  ...props\n}: TimePickerProps) {\n  const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue);\n  const selectedValue = valueProp ?? uncontrolledValue;\n  const selected = parseTime(selectedValue);\n  const [draft, setDraft] = React.useState<TimeParts>(() => selected ?? emptyTime);\n  const actionsRef = React.useRef<PopoverPrimitive.Root.Actions>(null);\n  const minuteStep = normalizeStep(minuteStepProp, 59);\n  const secondStep = normalizeStep(secondStepProp, 59);\n\n  const commitValue = (nextValue: string | undefined) => {\n    if (valueProp === undefined) {\n      setUncontrolledValue(nextValue);\n    }\n\n    onValueChange?.(nextValue);\n  };\n\n  const commitTime = (nextTime: TimeParts) => {\n    setDraft(nextTime);\n    commitValue(formatTimeValue(nextTime, showSeconds));\n  };\n\n  const handleOpenChange: PopoverPrimitive.Root.Props[\"onOpenChange\"] = (\n    nextOpen,\n    eventDetails,\n  ) => {\n    if (nextOpen) {\n      setDraft(selected ?? emptyTime);\n    }\n\n    onOpenChange?.(nextOpen, eventDetails);\n  };\n\n  const handleClear = () => {\n    commitValue(undefined);\n    actionsRef.current?.close();\n  };\n\n  return (\n    <PopoverPrimitive.Root\n      actionsRef={actionsRef}\n      defaultOpen={defaultOpen}\n      open={open}\n      onOpenChange={handleOpenChange}\n    >\n      <PopoverPrimitive.Trigger\n        render={\n          <button\n            type=\"button\"\n            data-slot=\"time-picker-trigger\"\n            className={cn(\n              \"flex h-10 w-full min-w-0 cursor-pointer items-center gap-2 border border-input bg-background px-3 text-left text-sm outline-none transition-[background-color,border-color,box-shadow,color] duration-100 hover:border-foreground/40 focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40 disabled:cursor-not-allowed disabled:bg-muted disabled:text-muted-foreground disabled:opacity-60 aria-invalid:border-destructive aria-invalid:focus-visible:border-destructive aria-invalid:focus-visible:ring-destructive/25 motion-reduce:transition-none\",\n              !selected && \"text-muted-foreground\",\n              className,\n            )}\n            disabled={disabled}\n            {...props}\n          />\n        }\n      >\n        {children ?? (\n          <>\n            <Clock3 className=\"size-4 shrink-0\" aria-hidden=\"true\" />\n            <span className=\"min-w-0 flex-1 truncate font-mono tabular-nums\">\n              {selected ? formatDisplayTime(selected, hourCycle, showSeconds) : placeholder}\n            </span>\n            <ChevronDown className=\"size-4 shrink-0 text-muted-foreground\" aria-hidden=\"true\" />\n          </>\n        )}\n      </PopoverPrimitive.Trigger>\n\n      <PopoverPrimitive.Portal>\n        <PopoverPrimitive.Positioner\n          align=\"start\"\n          side=\"bottom\"\n          sideOffset={6}\n          className=\"isolate z-50\"\n        >\n          <PopoverPrimitive.Popup\n            data-slot=\"time-picker-content\"\n            aria-label={props[\"aria-label\"] ? `${props[\"aria-label\"]} options` : \"Time picker\"}\n            className={cn(\n              \"overflow-hidden border border-border bg-popover text-popover-foreground outline-none transition-opacity duration-100 data-ending-style:opacity-0 data-instant:transition-none data-starting-style:opacity-0 motion-reduce:transition-none\",\n              hourCycle === 12\n                ? showSeconds\n                  ? \"w-[min(18.5rem,var(--available-width))]\"\n                  : \"w-[min(13.5rem,var(--available-width))]\"\n                : showSeconds\n                  ? \"w-[min(15rem,var(--available-width))]\"\n                  : \"w-[min(11rem,var(--available-width))]\",\n            )}\n          >\n            <div className=\"flex items-center justify-between gap-3 border-b border-border px-3 py-3\">\n              <span className=\"font-mono text-[0.625rem] uppercase tracking-[0.14em] text-muted-foreground\">\n                Time\n              </span>\n              <output className=\"font-mono text-sm tabular-nums\" aria-live=\"polite\">\n                {formatDisplayTime(draft, hourCycle, showSeconds)}\n              </output>\n            </div>\n\n            <fieldset className=\"flex items-end justify-center gap-2 px-3 py-4\">\n              <legend className=\"sr-only\">Time</legend>\n              <TimeSegment\n                label=\"Hour\"\n                value={hourCycle === 12 ? toTwelveHour(draft.hours) : draft.hours}\n                min={hourCycle === 12 ? 1 : 0}\n                max={hourCycle === 12 ? 12 : 23}\n                autoFocus\n                onValueChange={(hours) => {\n                  commitTime({\n                    ...draft,\n                    hours:\n                      hourCycle === 12\n                        ? fromTwelveHour(hours, draft.hours >= 12 ? \"PM\" : \"AM\")\n                        : hours,\n                  });\n                }}\n              />\n\n              <TimeDivider />\n\n              <TimeSegment\n                label=\"Minute\"\n                value={draft.minutes}\n                min={0}\n                max={59}\n                step={minuteStep}\n                onValueChange={(minutes) => {\n                  commitTime({ ...draft, minutes });\n                }}\n              />\n\n              {showSeconds ? (\n                <>\n                  <TimeDivider />\n                  <TimeSegment\n                    label=\"Second\"\n                    value={draft.seconds}\n                    min={0}\n                    max={59}\n                    step={secondStep}\n                    onValueChange={(seconds) => {\n                      commitTime({ ...draft, seconds });\n                    }}\n                  />\n                </>\n              ) : null}\n\n              {hourCycle === 12 ? (\n                <div className=\"grid gap-2\">\n                  <span className=\"font-mono text-[0.625rem] uppercase tracking-[0.12em] text-muted-foreground\">\n                    Period\n                  </span>\n                  <div className=\"grid h-[6.75rem] grid-rows-2 border border-border\">\n                    {([\"AM\", \"PM\"] as const).map((period) => {\n                      const active = period === (draft.hours >= 12 ? \"PM\" : \"AM\");\n\n                      return (\n                        <button\n                          key={period}\n                          type=\"button\"\n                          className=\"min-w-11 cursor-pointer border-b border-border px-2 font-mono text-xs outline-none transition-colors duration-100 last:border-b-0 hover:bg-muted focus-visible:z-10 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/40 data-active:bg-foreground data-active:text-background motion-reduce:transition-none\"\n                          data-active={active ? \"\" : undefined}\n                          aria-pressed={active}\n                          onClick={() => {\n                            commitTime({\n                              ...draft,\n                              hours: fromTwelveHour(toTwelveHour(draft.hours), period),\n                            });\n                          }}\n                        >\n                          {period}\n                        </button>\n                      );\n                    })}\n                  </div>\n                </div>\n              ) : null}\n            </fieldset>\n\n            <div className=\"flex items-center border-t border-border p-2\">\n              <button\n                type=\"button\"\n                className=\"h-8 cursor-pointer px-2.5 font-mono text-[0.625rem] uppercase tracking-[0.12em] text-muted-foreground outline-none transition-colors duration-100 hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/40 motion-reduce:transition-none\"\n                onClick={() => commitTime(getCurrentTime())}\n              >\n                Now\n              </button>\n              {selected ? (\n                <button\n                  type=\"button\"\n                  className=\"h-8 cursor-pointer px-2.5 font-mono text-[0.625rem] uppercase tracking-[0.12em] text-muted-foreground outline-none transition-colors duration-100 hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/40 motion-reduce:transition-none\"\n                  onClick={handleClear}\n                >\n                  Clear\n                </button>\n              ) : null}\n            </div>\n          </PopoverPrimitive.Popup>\n        </PopoverPrimitive.Positioner>\n      </PopoverPrimitive.Portal>\n\n      {name ? (\n        <input\n          type=\"hidden\"\n          name={name}\n          value={selected ? formatTimeValue(selected, showSeconds) : \"\"}\n          disabled={disabled}\n        />\n      ) : null}\n    </PopoverPrimitive.Root>\n  );\n}\n\nfunction TimeSegment({\n  autoFocus,\n  label,\n  max,\n  min,\n  onValueChange,\n  step = 1,\n  value,\n}: {\n  autoFocus?: boolean;\n  label: string;\n  max: number;\n  min: number;\n  onValueChange: (value: number) => void;\n  step?: number;\n  value: number;\n}) {\n  return (\n    <NumberFieldPrimitive.Root\n      value={value}\n      min={min}\n      max={max}\n      step={step}\n      snapOnStep\n      format={{ minimumIntegerDigits: 2, useGrouping: false }}\n      onValueChange={(nextValue) => {\n        if (nextValue !== null) {\n          onValueChange(nextValue);\n        }\n      }}\n      className=\"grid gap-2\"\n    >\n      <span className=\"font-mono text-[0.625rem] uppercase tracking-[0.12em] text-muted-foreground\">\n        {label}\n      </span>\n      <div className=\"grid w-14 border border-border\">\n        <NumberFieldPrimitive.Increment\n          className=\"flex h-8 cursor-pointer items-center justify-center border-b border-border text-muted-foreground outline-none transition-colors duration-100 hover:bg-muted hover:text-foreground focus-visible:z-10 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/40 disabled:pointer-events-none disabled:opacity-30 motion-reduce:transition-none\"\n          aria-label={`Increase ${label.toLowerCase()}`}\n        >\n          <ChevronUp className=\"size-3.5\" aria-hidden=\"true\" />\n        </NumberFieldPrimitive.Increment>\n        <NumberFieldPrimitive.Input\n          autoFocus={autoFocus}\n          className=\"h-10 w-full bg-transparent px-1 text-center font-mono text-base tabular-nums outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/40\"\n          aria-label={label}\n          inputMode=\"numeric\"\n        />\n        <NumberFieldPrimitive.Decrement\n          className=\"flex h-8 cursor-pointer items-center justify-center border-t border-border text-muted-foreground outline-none transition-colors duration-100 hover:bg-muted hover:text-foreground focus-visible:z-10 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/40 disabled:pointer-events-none disabled:opacity-30 motion-reduce:transition-none\"\n          aria-label={`Decrease ${label.toLowerCase()}`}\n        >\n          <ChevronDown className=\"size-3.5\" aria-hidden=\"true\" />\n        </NumberFieldPrimitive.Decrement>\n      </div>\n    </NumberFieldPrimitive.Root>\n  );\n}\n\nfunction TimeDivider() {\n  return (\n    <span className=\"mb-[2.375rem] font-mono text-base text-muted-foreground\" aria-hidden=\"true\">\n      :\n    </span>\n  );\n}\n\nfunction parseTime(value: string | undefined): TimeParts | undefined {\n  if (!value) {\n    return undefined;\n  }\n\n  const match = /^(\\d{2}):(\\d{2})(?::(\\d{2}))?$/.exec(value);\n\n  if (!match) {\n    return undefined;\n  }\n\n  const hours = Number(match[1]);\n  const minutes = Number(match[2]);\n  const seconds = Number(match[3] ?? 0);\n\n  if (hours > 23 || minutes > 59 || seconds > 59) {\n    return undefined;\n  }\n\n  return { hours, minutes, seconds };\n}\n\nfunction formatTimeValue(time: TimeParts, showSeconds: boolean) {\n  const value = `${padTimePart(time.hours)}:${padTimePart(time.minutes)}`;\n\n  return showSeconds ? `${value}:${padTimePart(time.seconds)}` : value;\n}\n\nfunction formatDisplayTime(time: TimeParts, hourCycle: 12 | 24, showSeconds: boolean) {\n  const hours = hourCycle === 12 ? toTwelveHour(time.hours) : time.hours;\n  const value = `${padTimePart(hours)}:${padTimePart(time.minutes)}`;\n  const withSeconds = showSeconds ? `${value}:${padTimePart(time.seconds)}` : value;\n\n  return hourCycle === 12 ? `${withSeconds} ${time.hours >= 12 ? \"PM\" : \"AM\"}` : withSeconds;\n}\n\nfunction padTimePart(value: number) {\n  return String(value).padStart(2, \"0\");\n}\n\nfunction toTwelveHour(hours: number) {\n  return hours % 12 || 12;\n}\n\nfunction fromTwelveHour(hours: number, period: \"AM\" | \"PM\") {\n  return (hours % 12) + (period === \"PM\" ? 12 : 0);\n}\n\nfunction normalizeStep(step: number, max: number) {\n  if (!Number.isFinite(step)) {\n    return 1;\n  }\n\n  return Math.min(max, Math.max(1, Math.floor(step)));\n}\n\nfunction getCurrentTime(): TimeParts {\n  const now = new Date();\n\n  return {\n    hours: now.getHours(),\n    minutes: now.getMinutes(),\n    seconds: now.getSeconds(),\n  };\n}\n\nexport type { TimePickerProps };\nexport { TimePicker };\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}