{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "time-picker",
  "title": "Time Picker",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/time-picker/time-picker.component.ts",
      "content": "import {\n  AfterViewInit,\n  ChangeDetectorRef,\n  Component,\n  Directive,\n  ElementRef,\n  EventEmitter,\n  Input,\n  OnChanges,\n  OnDestroy,\n  OnInit,\n  Output,\n  ViewChild,\n  booleanAttribute,\n  forwardRef,\n  inject,\n  numberAttribute,\n  signal,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { NG_VALUE_ACCESSOR, type ControlValueAccessor } from '@angular/forms'\nimport { cn } from '@/lib/utils'\nimport { buttonVariants } from '@/ui/button/button.component'\nimport { UiPopoverComponent, UiPopoverContentComponent } from '@/ui/popover/popover.component'\nimport { UiScrollAreaComponent } from '@/ui/scroll-area/scroll-area.component'\n\nexport type TimeFormat = 'HH:mm' | 'HH:mm:ss' | 'hh:mm A'\n\nexport interface TimeParts {\n  hour24: number\n  minute: number\n  second: number\n}\n\nexport interface TimePreset {\n  label: string\n  value: string\n}\n\nexport interface TimeRangePreset {\n  label: string\n  value: [string, string]\n}\n\nexport type TimePickerSize = 'small' | 'middle' | 'large'\nexport type TimePickerStatus = 'error' | 'warning'\n\n/** Parses 24h `HH:mm` / `HH:mm:ss`; null when empty or out of range. */\nexport function parseTime(v: string): TimeParts | null {\n  if (!v) return null\n  const m = /^(\\d{1,2}):(\\d{2})(?::(\\d{2}))?$/.exec(v.trim())\n  if (!m) return null\n  const h = Number(m[1])\n  const min = Number(m[2])\n  const s = m[3] ? Number(m[3]) : 0\n  if ([h, min, s].some(Number.isNaN) || h < 0 || h > 23 || min < 0 || min > 59 || s < 0 || s > 59) return null\n  return { hour24: h, minute: min, second: s }\n}\n\nconst toMinutes = (p: TimeParts) => p.hour24 * 60 + p.minute + p.second / 60\nconst pad = (n: number) => String(n).padStart(2, '0')\n\nfunction formatDisplay(format: TimeFormat, h: number, m: number, s: number): string {\n  if (format === 'hh:mm A') return `${pad(h % 12 === 0 ? 12 : h % 12)}:${pad(m)} ${h >= 12 ? 'PM' : 'AM'}`\n  if (format === 'HH:mm:ss') return `${pad(h)}:${pad(m)}:${pad(s)}`\n  return `${pad(h)}:${pad(m)}`\n}\n\n/** \"Now\" snapped to the minute / second steps (carrying into the next minute / hour). */\nfunction nowSnapped(format: TimeFormat, minuteStep: number, secondStep: number): string {\n  const d = new Date()\n  const sStep = Math.max(1, secondStep)\n  const snappedS = Math.round(d.getSeconds() / sStep) * sStep\n  const s = snappedS % 60\n  const mStep = Math.max(1, minuteStep)\n  const snappedM = Math.round((d.getMinutes() + Math.floor(snappedS / 60)) / mStep) * mStep\n  const min = snappedM % 60\n  const h = (d.getHours() + Math.floor(snappedM / 60)) % 24\n  return format === 'HH:mm:ss' ? `${pad(h)}:${pad(min)}:${pad(s)}` : `${pad(h)}:${pad(min)}`\n}\n\nconst SIZE_CLASSES: Record<TimePickerSize, string> = {\n  small: 'h-8 text-xs px-2.5 py-1',\n  middle: 'h-9 text-sm px-3 py-1.5',\n  large: 'h-11 text-base px-4 py-2',\n}\nconst STATUS_CLASSES: Record<string, string> = {\n  error: 'border-destructive focus-visible:ring-destructive',\n  warning: 'border-warning focus-visible:ring-warning',\n}\n\nconst CELL =\n  'hover:bg-accent focus-visible:ring-ring rounded px-2 py-1 text-center text-sm tabular-nums transition-colors focus-visible:ring-2 focus-visible:outline-none disabled:opacity-30 disabled:hover:bg-transparent'\nconst ACTIVE = 'bg-primary text-primary-foreground hover:bg-primary'\nconst PRESET =\n  'hover:bg-accent focus-visible:ring-ring rounded-md px-2 py-1.5 text-left text-xs transition-colors focus-visible:ring-2 focus-visible:outline-none'\nconst NOW =\n  'text-muted-foreground hover:text-foreground focus-visible:ring-ring text-xs focus-visible:ring-2 focus-visible:outline-none'\nconst CLEAR =\n  'text-muted-foreground hover:text-foreground focus-visible:ring-ring -mr-1 inline-flex size-5 items-center justify-center rounded transition-colors focus-visible:ring-2 focus-visible:outline-none'\n\nconst CLOCK_SVG = `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-clock size-4 shrink-0\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"10\" /><path d=\"M12 6v6l4 2\" /></svg>`\nconst X_SVG = `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-x size-3.5\" aria-hidden=\"true\"><path d=\"M18 6 6 18\" /><path d=\"m6 6 12 12\" /></svg>`\n\n/**\n * TimeColumns (React `TimeColumns`): the scrollable hour / minute / second (+ AM/PM)\n * columns shared by TimePicker, TimeRangePicker and DatePicker's time pane. Emits 24h\n * `HH:mm` / `HH:mm:ss` via `valueChange`; min / max / disabled* callbacks grey out (or with\n * `hideDisabledOptions` drop) values; active rows scroll into view while `visible`.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-time-columns, [ui-time-columns]',\n  standalone: true,\n  imports: [UiScrollAreaComponent],\n  host: {\n    class: 'flex divide-x',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-slot]': '\"time-columns\"',\n  },\n  template: `\n    <ui-scroll-area #hourCol class=\"h-56\">\n      <div class=\"flex w-14 flex-col p-1\">\n        @for (h of hours; track h) {\n          <button\n            type=\"button\"\n            [attr.data-active]=\"isHourActive(h)\"\n            [disabled]=\"isHourDisabled(h)\"\n            [class]=\"cellClass(isHourActive(h))\"\n            (click)=\"effective12Hour ? pickHour12(h) : pickHour24(h)\"\n          >\n            {{ pad(h) }}\n          </button>\n        }\n      </div>\n    </ui-scroll-area>\n    <ui-scroll-area #minCol class=\"h-56\">\n      <div class=\"flex w-14 flex-col p-1\">\n        @for (m of minutes; track m) {\n          <button\n            type=\"button\"\n            [attr.data-active]=\"parts?.minute === m\"\n            [disabled]=\"isMinuteDisabled(m)\"\n            [class]=\"cellClass(parts?.minute === m)\"\n            (click)=\"pickMinute(m)\"\n          >\n            {{ pad(m) }}\n          </button>\n        }\n      </div>\n    </ui-scroll-area>\n    @if (showSeconds) {\n      <ui-scroll-area #secCol class=\"h-56\">\n        <div class=\"flex w-14 flex-col p-1\">\n          @for (s of seconds; track s) {\n            <button\n              type=\"button\"\n              [attr.data-active]=\"parts?.second === s\"\n              [disabled]=\"isSecondDisabled(s)\"\n              [class]=\"cellClass(parts?.second === s)\"\n              (click)=\"pickSecond(s)\"\n            >\n              {{ pad(s) }}\n            </button>\n          }\n        </div>\n      </ui-scroll-area>\n    }\n    @if (effective12Hour) {\n      <div class=\"flex w-12 flex-col p-1\">\n        @for (p of periods; track p) {\n          <button type=\"button\" [class]=\"periodClass(p)\" (click)=\"pickPeriod(p)\">{{ p }}</button>\n        }\n      </div>\n    }\n  `,\n})\nexport class UiTimeColumnsComponent implements OnChanges, AfterViewInit, OnDestroy {\n  /** Time value, HH:mm or HH:mm:ss depending on format. Empty = no selection. */\n  @Input() value = ''\n  @Input({ transform: booleanAttribute }) use24Hour = false\n  @Input({ transform: booleanAttribute }) use12Hours = false\n  @Input({ transform: numberAttribute }) minuteStep = 5\n  @Input({ transform: numberAttribute }) hourStep = 1\n  @Input({ transform: numberAttribute }) secondStep = 1\n  /** 24h HH:mm or HH:mm:ss. */\n  @Input() minTime?: string\n  /** 24h HH:mm or HH:mm:ss. */\n  @Input() maxTime?: string\n  @Input() format: TimeFormat = 'HH:mm'\n  @Input() disabledHours?: () => number[]\n  @Input() disabledMinutes?: (selectedHour: number) => number[]\n  @Input() disabledSeconds?: (selectedHour: number, selectedMinute: number) => number[]\n  @Input({ transform: booleanAttribute }) hideDisabledOptions = false\n  /** Auto-scroll active rows into view when this becomes true. */\n  @Input({ transform: booleanAttribute }) visible = true\n  @Output() readonly valueChange = new EventEmitter<string>()\n\n  @ViewChild('hourCol', { read: ElementRef }) hourCol?: ElementRef<HTMLElement>\n  @ViewChild('minCol', { read: ElementRef }) minCol?: ElementRef<HTMLElement>\n  @ViewChild('secCol', { read: ElementRef }) secCol?: ElementRef<HTMLElement>\n\n  readonly periods = ['AM', 'PM'] as const\n  readonly pad = pad\n  private raf = 0\n  private viewReady = false\n\n  get parts(): TimeParts | null {\n    return parseTime(this.value)\n  }\n\n  get showSeconds(): boolean {\n    return this.format === 'HH:mm:ss'\n  }\n\n  /** 24h columns by default; 12h via format=\"hh:mm A\" or use12Hours. */\n  get effective12Hour(): boolean {\n    return this.format === 'hh:mm A' || this.use12Hours\n  }\n\n  get period(): 'AM' | 'PM' {\n    const p = this.parts\n    return p && p.hour24 >= 12 ? 'PM' : 'AM'\n  }\n\n  private withinBounds(p: TimeParts): boolean {\n    const min = parseTime(this.minTime ?? '')\n    const max = parseTime(this.maxTime ?? '')\n    const m = toMinutes(p)\n    if (min && m < toMinutes(min)) return false\n    if (max && m > toMinutes(max)) return false\n    return true\n  }\n\n  private get cur(): TimeParts {\n    return this.parts ?? { hour24: 0, minute: 0, second: 0 }\n  }\n\n  private isHourDisabledItem(h: number): boolean {\n    if (this.disabledHours?.().includes(h)) return true\n    const c = this.cur\n    if (this.effective12Hour) {\n      return !this.withinBounds({\n        hour24: (h % 12) + (this.period === 'PM' ? 12 : 0),\n        minute: c.minute,\n        second: c.second,\n      })\n    }\n    return !this.withinBounds({ hour24: h, minute: c.minute, second: c.second })\n  }\n\n  private isMinuteDisabledItem(m: number): boolean {\n    if (this.disabledMinutes?.(this.parts?.hour24 ?? 0).includes(m)) return true\n    const c = this.cur\n    return !this.withinBounds({ hour24: c.hour24, minute: m, second: c.second })\n  }\n\n  private isSecondDisabledItem(s: number): boolean {\n    if (this.disabledSeconds?.(this.parts?.hour24 ?? 0, this.parts?.minute ?? 0).includes(s)) return true\n    const c = this.cur\n    return !this.withinBounds({ hour24: c.hour24, minute: c.minute, second: s })\n  }\n\n  get hours(): number[] {\n    const step = Math.max(1, this.hourStep)\n    const list = this.effective12Hour\n      ? Array.from({ length: 12 }, (_, i) => i + 1).filter((h) => (h - 1) % step === 0)\n      : Array.from({ length: 24 }, (_, i) => i).filter((h) => h % step === 0)\n    return list.filter((h) => !this.hideDisabledOptions || !this.isHourDisabledItem(h))\n  }\n\n  get minutes(): number[] {\n    const step = Math.max(1, Math.min(60, this.minuteStep))\n    return Array.from({ length: Math.ceil(60 / step) }, (_, i) => i * step).filter(\n      (m) => !this.hideDisabledOptions || !this.isMinuteDisabledItem(m),\n    )\n  }\n\n  get seconds(): number[] {\n    const step = Math.max(1, Math.min(60, this.secondStep))\n    return Array.from({ length: Math.ceil(60 / step) }, (_, i) => i * step).filter(\n      (s) => !this.hideDisabledOptions || !this.isSecondDisabledItem(s),\n    )\n  }\n\n  isHourActive(h: number): boolean {\n    const p = this.parts\n    if (!p) return false\n    return this.effective12Hour ? (p.hour24 % 12 === 0 ? 12 : p.hour24 % 12) === h : p.hour24 === h\n  }\n\n  isHourDisabled(h: number): boolean {\n    return !this.hideDisabledOptions && this.isHourDisabledItem(h)\n  }\n\n  isMinuteDisabled(m: number): boolean {\n    return !this.hideDisabledOptions && this.isMinuteDisabledItem(m)\n  }\n\n  isSecondDisabled(s: number): boolean {\n    return !this.hideDisabledOptions && this.isSecondDisabledItem(s)\n  }\n\n  cellClass(active: boolean): string {\n    return cn(CELL, active ? ACTIVE : '')\n  }\n\n  periodClass(p: 'AM' | 'PM'): string {\n    return cn(\n      'hover:bg-accent focus-visible:ring-ring rounded px-2 py-1 text-center text-sm transition-colors focus-visible:ring-2 focus-visible:outline-none',\n      this.period === p ? ACTIVE : '',\n    )\n  }\n\n  private commit(p: TimeParts): void {\n    if (!this.withinBounds(p)) return\n    this.valueChange.emit(\n      this.format === 'HH:mm:ss'\n        ? `${pad(p.hour24)}:${pad(p.minute)}:${pad(p.second)}`\n        : `${pad(p.hour24)}:${pad(p.minute)}`,\n    )\n  }\n\n  pickHour12(h: number): void {\n    const c = this.cur\n    this.commit({ hour24: (h % 12) + (this.period === 'PM' ? 12 : 0), minute: c.minute, second: c.second })\n  }\n\n  pickHour24(h: number): void {\n    const c = this.cur\n    this.commit({ hour24: h, minute: c.minute, second: c.second })\n  }\n\n  pickMinute(m: number): void {\n    const c = this.cur\n    this.commit({ hour24: c.hour24, minute: m, second: c.second })\n  }\n\n  pickSecond(s: number): void {\n    const c = this.cur\n    this.commit({ hour24: c.hour24, minute: c.minute, second: s })\n  }\n\n  pickPeriod(p: 'AM' | 'PM'): void {\n    const c = this.cur\n    this.commit({ hour24: (c.hour24 % 12) + (p === 'PM' ? 12 : 0), minute: c.minute, second: c.second })\n  }\n\n  ngOnChanges(): void {\n    if (this.viewReady) this.scheduleScroll()\n  }\n\n  ngAfterViewInit(): void {\n    this.viewReady = true\n    this.scheduleScroll()\n  }\n\n  /** React effect on [visible, value]: centre the active row of each column. */\n  private scheduleScroll(): void {\n    if (!this.visible || typeof requestAnimationFrame === 'undefined') return\n    cancelAnimationFrame(this.raf)\n    this.raf = requestAnimationFrame(() => {\n      for (const col of [this.hourCol, this.minCol, this.secCol]) {\n        col?.nativeElement.querySelector('[data-active=\"true\"]')?.scrollIntoView({ block: 'center' })\n      }\n    })\n  }\n\n  ngOnDestroy(): void {\n    if (typeof cancelAnimationFrame !== 'undefined') cancelAnimationFrame(this.raf)\n  }\n}\n\n/** Shared trigger behaviour: host = the outline Button, `(click)` toggles the popover. */\n@Directive()\nabstract class TimeTriggerBase implements OnInit {\n  protected readonly host = inject<ElementRef<HTMLButtonElement>>(ElementRef).nativeElement\n  protected readonly cdr = inject(ChangeDetectorRef)\n  @ViewChild(UiPopoverComponent, { static: true }) popover!: UiPopoverComponent\n\n  @Input() placeholder = ''\n  @Input({ transform: booleanAttribute }) disabled = false\n  @Input({ transform: booleanAttribute }) readOnly = false\n  @Input({ transform: booleanAttribute }) clearable = true\n  @Input() allowClear?: boolean\n  @Input({ transform: numberAttribute }) minuteStep = 5\n  @Input({ transform: numberAttribute }) hourStep = 1\n  @Input({ transform: numberAttribute }) secondStep = 1\n  /** Backward-compat flag (24-hour columns are the default). */\n  @Input({ transform: booleanAttribute }) use24Hour = false\n  /** Show the AM/PM selector (12-hour columns). */\n  @Input({ transform: booleanAttribute }) use12Hours = false\n  @Input() minTime?: string\n  @Input() maxTime?: string\n  @Input() format: TimeFormat = 'HH:mm'\n  @Input() disabledHours?: () => number[]\n  @Input() disabledMinutes?: (selectedHour: number) => number[]\n  @Input() disabledSeconds?: (selectedHour: number, selectedMinute: number) => number[]\n  @Input({ transform: booleanAttribute }) hideDisabledOptions = false\n  @Input() size: TimePickerSize = 'middle'\n  @Input() status?: TimePickerStatus\n  @Input() triggerClassName?: string\n  @Input('class') className?: string\n\n  readonly open = signal(false)\n  protected readonly formDisabled = signal(false)\n  protected onTouched: () => void = () => {}\n\n  get isDisabled(): boolean {\n    return this.disabled || this.formDisabled()\n  }\n\n  get effectiveAllowClear(): boolean {\n    return this.allowClear !== undefined ? this.allowClear : this.clearable\n  }\n\n  get contentId(): string | null {\n    return this.popover?.contentId ?? null\n  }\n\n  ngOnInit(): void {\n    // The host button is the Popover trigger (Radix PopoverTrigger asChild).\n    this.popover.trigger = this.host\n  }\n\n  protected triggerClass(minWidth: string, empty: boolean): string {\n    return cn(\n      buttonVariants({ variant: 'outline', size: 'default' }),\n      cn(\n        `${minWidth} justify-start gap-2 text-left font-normal`,\n        empty && 'text-muted-foreground',\n        SIZE_CLASSES[this.size],\n        this.status && STATUS_CLASSES[this.status],\n        this.triggerClassName,\n        this.className,\n      ),\n    )\n  }\n\n  toggle(): void {\n    if (this.isDisabled) return\n    this.open.set(!this.open())\n  }\n\n  setOpen(v: boolean): void {\n    this.open.set(v)\n    if (!v) this.onTouched()\n  }\n\n  registerOnTouched(fn: () => void): void {\n    this.onTouched = fn\n  }\n\n  setDisabledState(isDisabled: boolean): void {\n    this.formDisabled.set(isDisabled)\n    this.cdr.markForCheck()\n  }\n}\n\nconst TRIGGER_HOST = {\n  type: 'button',\n  '[class]': 'hostClass',\n  '[disabled]': 'isDisabled',\n  '[attr.data-uipkge]': '\"\"',\n  '[attr.data-variant]': '\"outline\"',\n  '[attr.data-size]': '\"default\"',\n  '[attr.aria-haspopup]': '\"dialog\"',\n  '[attr.aria-expanded]': 'open()',\n  '[attr.aria-controls]': 'open() ? contentId : null',\n  '[attr.data-state]': 'open() ? \"open\" : \"closed\"',\n  '(click)': 'toggle()',\n}\n\n/**\n * Angular port of the UIPKGE React TimePicker. Put it on a button\n * (`<button ui-time-picker>`): the host IS the outline trigger Button (React renders\n * PopoverTrigger asChild > Button), with the Clock icon, the value / placeholder and a\n * clear control; the popover holds presets, a \"Now\" shortcut and TimeColumns. `value` /\n * `defaultValue` / `valueChange` (24h HH:mm or HH:mm:ss) work controlled or uncontrolled,\n * and it is a ControlValueAccessor for [formControl] / ngModel.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'button[ui-time-picker]',\n  standalone: true,\n  imports: [UiPopoverComponent, UiPopoverContentComponent, UiTimeColumnsComponent],\n  providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => UiTimePickerComponent), multi: true }],\n  host: { ...TRIGGER_HOST, '[attr.data-slot]': '\"time-picker\"' },\n  template: `\n    ${CLOCK_SVG}<span class=\"flex-1 truncate\">{{ display || placeholder }}</span>\n    @if (effectiveAllowClear && parsed && !isDisabled && !readOnly) {\n      <span\n        role=\"button\"\n        tabindex=\"-1\"\n        class=\"${CLEAR}\"\n        aria-label=\"Clear time\"\n        (click)=\"clear($event)\"\n        (mousedown)=\"$event.preventDefault()\"\n        >${X_SVG}</span\n      >\n    }\n    <ui-popover [open]=\"open()\" (openChange)=\"setOpen($event)\">\n      <ui-popover-content class=\"w-auto p-0\" align=\"start\">\n        @if (presets.length > 0) {\n          <div class=\"flex flex-col gap-0.5 border-b p-2\">\n            @for (p of presets; track p.label) {\n              <button type=\"button\" class=\"${PRESET}\" (click)=\"applyPreset(p)\">{{ p.label }}</button>\n            }\n          </div>\n        }\n        <div class=\"flex items-center justify-between border-b px-3 py-2\">\n          <span class=\"text-muted-foreground text-xs tracking-widest uppercase\">Time</span>\n          <button type=\"button\" class=\"${NOW}\" (click)=\"pickNow()\">Now</button>\n        </div>\n        <ui-time-columns\n          [value]=\"currentValue\"\n          [use24Hour]=\"use24Hour\"\n          [use12Hours]=\"use12Hours\"\n          [minuteStep]=\"minuteStep\"\n          [hourStep]=\"hourStep\"\n          [secondStep]=\"secondStep\"\n          [minTime]=\"minTime\"\n          [maxTime]=\"maxTime\"\n          [format]=\"format\"\n          [disabledHours]=\"disabledHours\"\n          [disabledMinutes]=\"disabledMinutes\"\n          [disabledSeconds]=\"disabledSeconds\"\n          [hideDisabledOptions]=\"hideDisabledOptions\"\n          [visible]=\"open()\"\n          (valueChange)=\"emitTime($event)\"\n        />\n      </ui-popover-content>\n    </ui-popover>\n  `,\n})\nexport class UiTimePickerComponent extends TimeTriggerBase implements ControlValueAccessor {\n  /** Controlled value as 24h HH:mm or HH:mm:ss. Empty string = no selection. */\n  @Input() value?: string\n  /** Uncontrolled initial value. */\n  @Input() defaultValue = ''\n  @Input() presets: TimePreset[] = []\n  @Output() readonly valueChange = new EventEmitter<string>()\n\n  override placeholder = 'Pick a time'\n  private readonly internal = signal<string | null>(null)\n  private onChange: (v: string) => void = () => {}\n\n  get currentValue(): string {\n    return this.value !== undefined ? this.value : (this.internal() ?? this.defaultValue)\n  }\n\n  get parsed(): TimeParts | null {\n    return parseTime(this.currentValue)\n  }\n\n  get display(): string {\n    const p = this.parsed\n    return p ? formatDisplay(this.format, p.hour24, p.minute, p.second) : ''\n  }\n\n  get hostClass(): string {\n    return this.triggerClass('min-w-[160px]', !this.parsed)\n  }\n\n  emitTime(v: string): void {\n    if (this.value === undefined) this.internal.set(v)\n    this.valueChange.emit(v)\n    this.onChange(v)\n  }\n\n  pickNow(): void {\n    if (this.readOnly) return\n    this.emitTime(nowSnapped(this.format, this.minuteStep, this.secondStep))\n  }\n\n  applyPreset(p: TimePreset): void {\n    if (this.readOnly) return\n    this.emitTime(p.value)\n    this.setOpen(false)\n  }\n\n  clear(e: Event): void {\n    e.stopPropagation()\n    if (this.isDisabled || this.readOnly) return\n    this.emitTime('')\n  }\n\n  writeValue(v: string | null): void {\n    this.internal.set(v ?? '')\n    this.cdr.markForCheck()\n  }\n\n  registerOnChange(fn: (v: string) => void): void {\n    this.onChange = fn\n  }\n}\n\n/**\n * Angular port of the UIPKGE React TimeRangePicker (`<button ui-time-range-picker>`): one\n * trigger showing `start ~ end`, a popover with presets, \"Now (Start)\" / \"Now (End)\" and two\n * TimeColumns side by side. `value` is a `[start, end]` tuple (or null).\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'button[ui-time-range-picker]',\n  standalone: true,\n  imports: [UiPopoverComponent, UiPopoverContentComponent, UiTimeColumnsComponent],\n  providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => UiTimeRangePickerComponent), multi: true }],\n  host: { ...TRIGGER_HOST, '[attr.data-slot]': '\"time-range-picker\"' },\n  template: `\n    ${CLOCK_SVG}<span class=\"flex-1 truncate\">{{ display || placeholder }}</span>\n    @if (effectiveAllowClear && display && !isDisabled && !readOnly) {\n      <span\n        role=\"button\"\n        tabindex=\"-1\"\n        class=\"${CLEAR}\"\n        aria-label=\"Clear time range\"\n        (click)=\"clear($event)\"\n        (mousedown)=\"$event.preventDefault()\"\n        >${X_SVG}</span\n      >\n    }\n    <ui-popover [open]=\"open()\" (openChange)=\"setOpen($event)\">\n      <ui-popover-content class=\"w-auto p-0\" align=\"start\">\n        @if (presets.length > 0) {\n          <div class=\"flex flex-col gap-0.5 border-b p-2\">\n            @for (p of presets; track p.label) {\n              <button type=\"button\" class=\"${PRESET}\" (click)=\"applyPreset(p)\">{{ p.label }}</button>\n            }\n          </div>\n        }\n        <div class=\"flex items-center justify-between border-b px-3 py-2\">\n          <span class=\"text-muted-foreground text-xs tracking-widest uppercase\">Time Range</span>\n          <div class=\"flex gap-2\">\n            <button type=\"button\" class=\"${NOW}\" (click)=\"pickNow('start')\">Now (Start)</button>\n            <button type=\"button\" class=\"${NOW}\" (click)=\"pickNow('end')\">Now (End)</button>\n          </div>\n        </div>\n        <div class=\"flex\">\n          <div class=\"flex flex-col\">\n            <div class=\"text-muted-foreground border-b px-3 py-1.5 text-center text-xs font-medium\">Start</div>\n            <ui-time-columns\n              [value]=\"startValue\"\n              [use24Hour]=\"use24Hour\"\n              [use12Hours]=\"use12Hours\"\n              [minuteStep]=\"minuteStep\"\n              [hourStep]=\"hourStep\"\n              [secondStep]=\"secondStep\"\n              [minTime]=\"minTime\"\n              [maxTime]=\"maxTime\"\n              [format]=\"format\"\n              [disabledHours]=\"disabledHours\"\n              [disabledMinutes]=\"disabledMinutes\"\n              [disabledSeconds]=\"disabledSeconds\"\n              [hideDisabledOptions]=\"hideDisabledOptions\"\n              [visible]=\"open()\"\n              (valueChange)=\"emitStart($event)\"\n            />\n          </div>\n          <div class=\"bg-border w-px\"></div>\n          <div class=\"flex flex-col\">\n            <div class=\"text-muted-foreground border-b px-3 py-1.5 text-center text-xs font-medium\">End</div>\n            <ui-time-columns\n              [value]=\"endValue\"\n              [use24Hour]=\"use24Hour\"\n              [use12Hours]=\"use12Hours\"\n              [minuteStep]=\"minuteStep\"\n              [hourStep]=\"hourStep\"\n              [secondStep]=\"secondStep\"\n              [minTime]=\"minTime\"\n              [maxTime]=\"maxTime\"\n              [format]=\"format\"\n              [disabledHours]=\"disabledHours\"\n              [disabledMinutes]=\"disabledMinutes\"\n              [disabledSeconds]=\"disabledSeconds\"\n              [hideDisabledOptions]=\"hideDisabledOptions\"\n              [visible]=\"open()\"\n              (valueChange)=\"emitEnd($event)\"\n            />\n          </div>\n        </div>\n      </ui-popover-content>\n    </ui-popover>\n  `,\n})\nexport class UiTimeRangePickerComponent extends TimeTriggerBase implements ControlValueAccessor {\n  /** Controlled `[start, end]` (24h). `undefined` = uncontrolled. */\n  @Input() value?: [string, string] | null\n  @Input() presets: TimeRangePreset[] = []\n  @Output() readonly valueChange = new EventEmitter<[string, string] | null>()\n\n  override placeholder = 'Pick a time range'\n  private readonly internal = signal<[string, string] | null>(null)\n  private onChange: (v: [string, string] | null) => void = () => {}\n\n  get currentValue(): [string, string] | null {\n    return this.value !== undefined ? this.value : this.internal()\n  }\n\n  get startValue(): string {\n    return this.currentValue?.[0] ?? ''\n  }\n\n  get endValue(): string {\n    return this.currentValue?.[1] ?? ''\n  }\n\n  get display(): string {\n    const s = parseTime(this.startValue)\n    const e = parseTime(this.endValue)\n    if (!s && !e) return ''\n    const startStr = s ? formatDisplay(this.format, s.hour24, s.minute, s.second) : ''\n    const endStr = e ? formatDisplay(this.format, e.hour24, e.minute, e.second) : ''\n    if (s && e) return `${startStr} ~ ${endStr}`\n    return startStr || endStr\n  }\n\n  get hostClass(): string {\n    return this.triggerClass('min-w-[200px]', !this.display)\n  }\n\n  private emit(next: [string, string] | null): void {\n    if (this.value === undefined) this.internal.set(next)\n    this.valueChange.emit(next)\n    this.onChange(next)\n  }\n\n  emitStart(v: string): void {\n    this.emit([v, this.endValue || v])\n  }\n\n  emitEnd(v: string): void {\n    this.emit([this.startValue || v, v])\n  }\n\n  pickNow(which: 'start' | 'end'): void {\n    if (this.readOnly) return\n    const v = nowSnapped(this.format, this.minuteStep, this.secondStep)\n    if (which === 'start') this.emitStart(v)\n    else this.emitEnd(v)\n  }\n\n  applyPreset(p: TimeRangePreset): void {\n    if (this.readOnly) return\n    this.emit(p.value)\n    this.setOpen(false)\n  }\n\n  clear(e: Event): void {\n    e.stopPropagation()\n    if (this.isDisabled || this.readOnly) return\n    this.emit(null)\n  }\n\n  writeValue(v: [string, string] | null): void {\n    this.internal.set(v ?? null)\n    this.cdr.markForCheck()\n  }\n\n  registerOnChange(fn: (v: [string, string] | null) => void): void {\n    this.onChange = fn\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/time-picker/time-picker.component.ts"
    },
    {
      "path": "packages/registry-angular/components/time-picker/index.ts",
      "content": "export {\n  UiTimePickerComponent,\n  UiTimeColumnsComponent,\n  UiTimeRangePickerComponent,\n  type TimeFormat,\n  type TimeParts,\n  type TimePreset,\n  type TimeRangePreset,\n  type TimePickerSize,\n  type TimePickerStatus,\n} from './time-picker.component'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/time-picker/index.ts"
    }
  ],
  "dependencies": [
    "@angular/forms"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/button.json",
    "https://uipkge.dev/r/angular/popover.json",
    "https://uipkge.dev/r/angular/scroll-area.json"
  ],
  "description": "Standalone time input — hours, minutes, optional seconds, and 12h/24h modes. Pairs with Date Picker for full datetime entry.",
  "categories": [
    "date-time"
  ]
}