{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "time-picker",
  "title": "Time Picker",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-vue/components/time-picker/TimeColumns.vue",
      "content": "<script setup lang=\"ts\">\nimport { computed, nextTick, ref, watch } from 'vue'\nimport { ScrollArea } from '@/components/ui/scroll-area'\n\nexport interface TimeParts {\n  hour24: number\n  minute: number\n  second: number\n}\n\nexport type TimeFormat = 'HH:mm' | 'HH:mm:ss' | 'hh:mm A'\n\ninterface Props {\n  /** Time value. HH:mm or HH:mm:ss depending on format. Empty = no selection. */\n  modelValue?: string\n  use24Hour?: boolean\n  use12Hours?: boolean\n  minuteStep?: number\n  hourStep?: number\n  secondStep?: number\n  /** 24h HH:mm or HH:mm:ss. */\n  minTime?: string\n  /** 24h HH:mm or HH:mm:ss. */\n  maxTime?: string\n  format?: TimeFormat\n  disabledHours?: () => number[]\n  disabledMinutes?: (selectedHour: number) => number[]\n  disabledSeconds?: (selectedHour: number, selectedMinute: number) => number[]\n  hideDisabledOptions?: boolean\n  /** Auto-scroll active rows into view when this becomes true. */\n  visible?: boolean\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  modelValue: '',\n  use24Hour: false,\n  use12Hours: false,\n  minuteStep: 5,\n  hourStep: 1,\n  secondStep: 1,\n  format: 'HH:mm',\n  hideDisabledOptions: false,\n  visible: true,\n})\n\nconst emits = defineEmits<{\n  'update:modelValue': [value: string]\n}>()\n\nfunction parse(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 (\n    Number.isNaN(h) ||\n    Number.isNaN(min) ||\n    Number.isNaN(s) ||\n    h < 0 ||\n    h > 23 ||\n    min < 0 ||\n    min > 59 ||\n    s < 0 ||\n    s > 59\n  )\n    return null\n  return { hour24: h, minute: min, second: s }\n}\n\nfunction format24(p: TimeParts) {\n  if (props.format === 'HH:mm:ss') {\n    return `${String(p.hour24).padStart(2, '0')}:${String(p.minute).padStart(2, '0')}:${String(p.second).padStart(2, '0')}`\n  }\n  return `${String(p.hour24).padStart(2, '0')}:${String(p.minute).padStart(2, '0')}`\n}\n\nfunction toMinutes(p: TimeParts) {\n  return p.hour24 * 60 + p.minute + p.second / 60\n}\n\nconst parts = ref<TimeParts | null>(parse(props.modelValue))\nwatch(\n  () => props.modelValue,\n  (v) => {\n    parts.value = parse(v)\n  },\n)\n\nconst showSeconds = computed(() => props.format === 'HH:mm:ss')\n\nconst effective12Hour = computed(() => {\n  if (props.format === 'hh:mm A') return true\n  if (props.use12Hours) return true\n  return !props.use24Hour\n})\n\nconst hour12 = computed(() => {\n  if (!parts.value) return null\n  const h = parts.value.hour24 % 12\n  return h === 0 ? 12 : h\n})\nconst period = computed(() => (parts.value && parts.value.hour24 >= 12 ? 'PM' : 'AM'))\n\nconst minBound = computed(() => parse(props.minTime ?? '') ?? null)\nconst maxBound = computed(() => parse(props.maxTime ?? '') ?? null)\n\nfunction withinBounds(p: TimeParts) {\n  const m = toMinutes(p)\n  if (minBound.value && m < toMinutes(minBound.value)) return false\n  if (maxBound.value && m > toMinutes(maxBound.value)) return false\n  return true\n}\n\nconst disabledHoursSet = computed(() => {\n  const arr = props.disabledHours ? props.disabledHours() : []\n  return new Set(arr)\n})\n\nconst disabledMinutesSet = computed(() => {\n  const h = parts.value?.hour24 ?? 0\n  const arr = props.disabledMinutes ? props.disabledMinutes(h) : []\n  return new Set(arr)\n})\n\nconst disabledSecondsSet = computed(() => {\n  const h = parts.value?.hour24 ?? 0\n  const m = parts.value?.minute ?? 0\n  const arr = props.disabledSeconds ? props.disabledSeconds(h, m) : []\n  return new Set(arr)\n})\n\nfunction isHourDisabledItem(h: number) {\n  if (disabledHoursSet.value.has(h)) return true\n  const cur = parts.value ?? { hour24: 0, minute: 0, second: 0 }\n  if (effective12Hour.value) {\n    const isPM = period.value === 'PM'\n    return !withinBounds({ hour24: (h % 12) + (isPM ? 12 : 0), minute: cur.minute, second: cur.second })\n  }\n  return !withinBounds({ hour24: h, minute: cur.minute, second: cur.second })\n}\n\nfunction isMinuteDisabledItem(m: number) {\n  if (disabledMinutesSet.value.has(m)) return true\n  const cur = parts.value ?? { hour24: 0, minute: 0, second: 0 }\n  return !withinBounds({ hour24: cur.hour24, minute: m, second: cur.second })\n}\n\nfunction isSecondDisabledItem(s: number) {\n  if (disabledSecondsSet.value.has(s)) return true\n  const cur = parts.value ?? { hour24: 0, minute: 0, second: 0 }\n  return !withinBounds({ hour24: cur.hour24, minute: cur.minute, second: s })\n}\n\nconst hours12List = computed(() => {\n  const step = Math.max(1, props.hourStep)\n  const list = Array.from({ length: 12 }, (_, i) => i + 1).filter((h) => (h - 1) % step === 0)\n  if (!props.hideDisabledOptions) return list\n  return list.filter((h) => !isHourDisabledItem(h))\n})\n\nconst hours24List = computed(() => {\n  const step = Math.max(1, props.hourStep)\n  const list = Array.from({ length: 24 }, (_, i) => i).filter((h) => h % step === 0)\n  if (!props.hideDisabledOptions) return list\n  return list.filter((h) => !isHourDisabledItem(h))\n})\n\nconst minutesList = computed(() => {\n  const step = Math.max(1, Math.min(60, props.minuteStep))\n  const list = Array.from({ length: Math.ceil(60 / step) }, (_, i) => i * step)\n  if (!props.hideDisabledOptions) return list\n  return list.filter((m) => !isMinuteDisabledItem(m))\n})\n\nconst secondsList = computed(() => {\n  const step = Math.max(1, Math.min(60, props.secondStep))\n  const list = Array.from({ length: Math.ceil(60 / step) }, (_, i) => i * step)\n  if (!props.hideDisabledOptions) return list\n  return list.filter((s) => !isSecondDisabledItem(s))\n})\n\nfunction commit(p: TimeParts) {\n  if (!withinBounds(p)) return\n  parts.value = p\n  emits('update:modelValue', format24(p))\n}\n\nfunction pickHour12(h: number) {\n  const cur = parts.value ?? { hour24: 0, minute: 0, second: 0 }\n  const isPM = period.value === 'PM'\n  commit({ hour24: (h % 12) + (isPM ? 12 : 0), minute: cur.minute, second: cur.second })\n}\n\nfunction pickHour24(h: number) {\n  const cur = parts.value ?? { hour24: 0, minute: 0, second: 0 }\n  commit({ hour24: h, minute: cur.minute, second: cur.second })\n}\n\nfunction pickMinute(m: number) {\n  const cur = parts.value ?? { hour24: 0, minute: 0, second: 0 }\n  commit({ hour24: cur.hour24, minute: m, second: cur.second })\n}\n\nfunction pickSecond(s: number) {\n  const cur = parts.value ?? { hour24: 0, minute: 0, second: 0 }\n  commit({ hour24: cur.hour24, minute: cur.minute, second: s })\n}\n\nfunction pickPeriod(p: 'AM' | 'PM') {\n  const cur = parts.value ?? { hour24: 0, minute: 0, second: 0 }\n  const h12 = cur.hour24 % 12\n  commit({ hour24: h12 + (p === 'PM' ? 12 : 0), minute: cur.minute, second: cur.second })\n}\n\nconst hourCol = ref<HTMLElement | null>(null)\nconst minCol = ref<HTMLElement | null>(null)\nconst secCol = ref<HTMLElement | null>(null)\nwatch(\n  () => props.visible,\n  (v) => {\n    if (!v) return\n    nextTick(() => {\n      hourCol.value?.querySelector('[data-active=\"true\"]')?.scrollIntoView({ block: 'center' })\n      minCol.value?.querySelector('[data-active=\"true\"]')?.scrollIntoView({ block: 'center' })\n      secCol.value?.querySelector('[data-active=\"true\"]')?.scrollIntoView({ block: 'center' })\n    })\n  },\n  { immediate: true },\n)\n\nfunction isHourActive(h: number) {\n  if (!parts.value) return false\n  return effective12Hour.value ? hour12.value === h : parts.value.hour24 === h\n}\n\nfunction isHourDisabled(h: number) {\n  if (!props.hideDisabledOptions) return isHourDisabledItem(h)\n  return false\n}\n\nfunction isMinuteDisabled(m: number) {\n  if (!props.hideDisabledOptions) return isMinuteDisabledItem(m)\n  return false\n}\n\nfunction isSecondDisabled(s: number) {\n  if (!props.hideDisabledOptions) return isSecondDisabledItem(s)\n  return false\n}\n</script>\n\n<template>\n  <div class=\"flex divide-x\" data-uipkge data-slot=\"time-columns\">\n    <ScrollArea ref=\"hourCol\" class=\"h-56\">\n      <div class=\"flex w-14 flex-col p-1\">\n        <button\n          v-for=\"h in effective12Hour ? hours12List : hours24List\"\n          :key=\"h\"\n          type=\"button\"\n          :data-active=\"isHourActive(h)\"\n          :disabled=\"isHourDisabled(h)\"\n          class=\"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\"\n          :class=\"isHourActive(h) ? 'bg-primary text-primary-foreground hover:bg-primary' : ''\"\n          @click=\"effective12Hour ? pickHour12(h) : pickHour24(h)\"\n        >\n          {{ String(h).padStart(2, '0') }}\n        </button>\n      </div>\n    </ScrollArea>\n\n    <ScrollArea ref=\"minCol\" class=\"h-56\">\n      <div class=\"flex w-14 flex-col p-1\">\n        <button\n          v-for=\"m in minutesList\"\n          :key=\"m\"\n          type=\"button\"\n          :data-active=\"parts?.minute === m\"\n          :disabled=\"isMinuteDisabled(m)\"\n          class=\"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\"\n          :class=\"parts?.minute === m ? 'bg-primary text-primary-foreground hover:bg-primary' : ''\"\n          @click=\"pickMinute(m)\"\n        >\n          {{ String(m).padStart(2, '0') }}\n        </button>\n      </div>\n    </ScrollArea>\n\n    <ScrollArea v-if=\"showSeconds\" ref=\"secCol\" class=\"h-56\">\n      <div class=\"flex w-14 flex-col p-1\">\n        <button\n          v-for=\"s in secondsList\"\n          :key=\"s\"\n          type=\"button\"\n          :data-active=\"parts?.second === s\"\n          :disabled=\"isSecondDisabled(s)\"\n          class=\"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\"\n          :class=\"parts?.second === s ? 'bg-primary text-primary-foreground hover:bg-primary' : ''\"\n          @click=\"pickSecond(s)\"\n        >\n          {{ String(s).padStart(2, '0') }}\n        </button>\n      </div>\n    </ScrollArea>\n\n    <div v-if=\"effective12Hour\" class=\"flex w-12 flex-col p-1\">\n      <button\n        v-for=\"p in ['AM', 'PM']\"\n        :key=\"p\"\n        type=\"button\"\n        class=\"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        :class=\"period === p ? 'bg-primary text-primary-foreground hover:bg-primary' : ''\"\n        @click=\"pickPeriod(p as 'AM' | 'PM')\"\n      >\n        {{ p }}\n      </button>\n    </div>\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/time-picker/TimeColumns.vue"
    },
    {
      "path": "packages/registry-vue/components/time-picker/TimePicker.vue",
      "content": "<script setup lang=\"ts\">\nimport type { HTMLAttributes } from 'vue'\nimport { computed, ref, watch } from 'vue'\nimport { Clock, X } from 'lucide-vue-next'\nimport { Button } from '@/components/ui/button'\nimport { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'\nimport TimeColumns, { type TimeFormat } from './TimeColumns.vue'\nimport { cn } from '@/lib/utils'\n\nexport interface TimePreset {\n  label: string\n  value: string\n}\n\nexport type TimePickerSize = 'small' | 'middle' | 'large'\nexport type TimePickerStatus = 'error' | 'warning'\n\ninterface Props {\n  /** Controlled value as 24h HH:mm or HH:mm:ss. Empty string = no selection. */\n  modelValue?: string\n  placeholder?: string\n  disabled?: boolean\n  readOnly?: boolean\n  clearable?: boolean\n  allowClear?: boolean\n  minuteStep?: number\n  hourStep?: number\n  secondStep?: number\n  /** Backward-compat: when true, render 24-hour selector. */\n  use24Hour?: boolean\n  /** When true, show AM/PM selector. */\n  use12Hours?: boolean\n  minTime?: string\n  maxTime?: string\n  format?: TimeFormat\n  disabledHours?: () => number[]\n  disabledMinutes?: (selectedHour: number) => number[]\n  disabledSeconds?: (selectedHour: number, selectedMinute: number) => number[]\n  hideDisabledOptions?: boolean\n  presets?: TimePreset[]\n  size?: TimePickerSize\n  status?: TimePickerStatus\n  triggerClass?: HTMLAttributes['class']\n  class?: HTMLAttributes['class']\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  modelValue: '',\n  placeholder: 'Pick a time',\n  disabled: false,\n  readOnly: false,\n  clearable: true,\n  allowClear: undefined,\n  minuteStep: 5,\n  hourStep: 1,\n  secondStep: 1,\n  use24Hour: false,\n  use12Hours: false,\n  format: 'HH:mm',\n  hideDisabledOptions: false,\n  presets: () => [],\n  size: 'middle',\n})\n\nconst emits = defineEmits<{\n  'update:modelValue': [value: string]\n}>()\n\nconst open = ref(false)\n\nconst effectiveAllowClear = computed(() => {\n  if (props.allowClear !== undefined) return props.allowClear\n  return props.clearable\n})\n\nfunction parse(v: string) {\n  if (!v) return null\n  const m = /^(\\d{1,2}):(\\d{2})(?::(\\d{2}))?$/.exec(v.trim())\n  if (!m) return null\n  return { hour24: Number(m[1]), minute: Number(m[2]), second: m[3] ? Number(m[3]) : 0 }\n}\n\nconst parsed = computed(() => parse(props.modelValue))\n\nconst effective12Hour = computed(() => {\n  if (props.format === 'hh:mm A') return true\n  if (props.use12Hours) return true\n  return !props.use24Hour\n})\n\nfunction formatDisplay(h: number, m: number, s: number) {\n  if (props.format === 'hh:mm A') {\n    const h12 = h % 12 === 0 ? 12 : h % 12\n    const period = h >= 12 ? 'PM' : 'AM'\n    return `${String(h12).padStart(2, '0')}:${String(m).padStart(2, '0')} ${period}`\n  }\n  if (props.format === 'HH:mm:ss') {\n    return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`\n  }\n  return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`\n}\n\nconst display = computed(() => {\n  if (!parsed.value) return ''\n  const { hour24, minute, second } = parsed.value\n  return formatDisplay(hour24, minute, second)\n})\n\nfunction emitTime(v: string) {\n  emits('update:modelValue', v)\n}\n\nfunction pickNow() {\n  if (props.readOnly) return\n  const d = new Date()\n  const sStep = Math.max(1, props.secondStep)\n  const rawS = d.getSeconds()\n  const snappedS = Math.round(rawS / sStep) * sStep\n  const secCarry = Math.floor(snappedS / 60)\n  const s = snappedS % 60\n\n  const mStep = Math.max(1, props.minuteStep)\n  const rawM = d.getMinutes() + secCarry\n  const snappedM = Math.round(rawM / mStep) * mStep\n  const minCarry = Math.floor(snappedM / 60)\n  const min = snappedM % 60\n\n  const h = d.getHours() + minCarry\n  const hourStr = String(h).padStart(2, '0')\n  const minStr = String(min).padStart(2, '0')\n  const secStr = String(s).padStart(2, '0')\n  emitTime(props.format === 'HH:mm:ss' ? `${hourStr}:${minStr}:${secStr}` : `${hourStr}:${minStr}`)\n}\n\nfunction applyPreset(preset: TimePreset) {\n  if (props.readOnly) return\n  emitTime(preset.value)\n  open.value = false\n}\n\nfunction clear(event: Event) {\n  event.stopPropagation()\n  if (props.disabled || props.readOnly) return\n  emitTime('')\n}\n\nconst sizeClasses: 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}\n\nconst statusClasses: Record<string, string> = {\n  error: 'border-destructive focus-visible:ring-destructive',\n  warning: 'border-warning focus-visible:ring-warning',\n}\n\nconst triggerClasses = computed(() =>\n  cn(\n    'min-w-[160px] justify-start gap-2 text-left font-normal',\n    !parsed.value && 'text-muted-foreground',\n    sizeClasses[props.size],\n    props.status && statusClasses[props.status],\n    props.triggerClass,\n    props.class,\n  ),\n)\n\n// Pulse the columns when reopen happens (passes via prop reactivity).\nconst columnsVisible = ref(false)\nwatch(open, (v) => {\n  columnsVisible.value = v\n})\n</script>\n\n<template>\n  <Popover v-model:open=\"open\">\n    <PopoverTrigger as-child>\n      <Button\n        type=\"button\"\n        variant=\"outline\"\n        :disabled=\"disabled\"\n        :class=\"triggerClasses\"\n        data-uipkge\n        data-slot=\"time-picker\"\n      >\n        <Clock class=\"size-4 shrink-0\" />\n        <span class=\"flex-1 truncate\">{{ display || placeholder }}</span>\n        <button\n          v-if=\"effectiveAllowClear && parsed && !disabled && !readOnly\"\n          type=\"button\"\n          class=\"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          aria-label=\"Clear time\"\n          @click.stop=\"clear\"\n        >\n          <X class=\"size-3.5\" aria-hidden=\"true\" />\n        </button>\n      </Button>\n    </PopoverTrigger>\n    <PopoverContent class=\"w-auto p-0\" align=\"start\">\n      <div v-if=\"presets.length\" class=\"flex flex-col gap-0.5 border-b p-2\">\n        <button\n          v-for=\"p in presets\"\n          :key=\"p.label\"\n          type=\"button\"\n          class=\"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\"\n          @click=\"applyPreset(p)\"\n        >\n          {{ p.label }}\n        </button>\n      </div>\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\n          type=\"button\"\n          class=\"text-muted-foreground hover:text-foreground focus-visible:ring-ring text-xs focus-visible:ring-2 focus-visible:outline-none\"\n          @click=\"pickNow\"\n        >\n          Now\n        </button>\n      </div>\n      <TimeColumns\n        :model-value=\"modelValue\"\n        :use24-hour=\"use24Hour\"\n        :use12-hours=\"use12Hours\"\n        :minute-step=\"minuteStep\"\n        :hour-step=\"hourStep\"\n        :second-step=\"secondStep\"\n        :min-time=\"minTime\"\n        :max-time=\"maxTime\"\n        :format=\"format\"\n        :disabled-hours=\"disabledHours\"\n        :disabled-minutes=\"disabledMinutes\"\n        :disabled-seconds=\"disabledSeconds\"\n        :hide-disabled-options=\"hideDisabledOptions\"\n        :visible=\"columnsVisible\"\n        @update:model-value=\"emitTime\"\n      />\n    </PopoverContent>\n  </Popover>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/time-picker/TimePicker.vue"
    },
    {
      "path": "packages/registry-vue/components/time-picker/TimeRangePicker.vue",
      "content": "<script setup lang=\"ts\">\nimport type { HTMLAttributes } from 'vue'\nimport { computed, ref, watch } from 'vue'\nimport { Clock, X } from 'lucide-vue-next'\nimport { Button } from '@/components/ui/button'\nimport { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'\nimport TimeColumns, { type TimeFormat } from './TimeColumns.vue'\nimport { cn } from '@/lib/utils'\n\nexport interface TimeRangePreset {\n  label: string\n  value: [string, string]\n}\n\nexport type TimeRangePickerSize = 'small' | 'middle' | 'large'\nexport type TimeRangePickerStatus = 'error' | 'warning'\n\ninterface Props {\n  /** Controlled value as [startTime, endTime] in 24h format. */\n  modelValue?: [string, string] | null\n  placeholder?: string\n  disabled?: boolean\n  readOnly?: boolean\n  clearable?: boolean\n  allowClear?: boolean\n  minuteStep?: number\n  hourStep?: number\n  secondStep?: number\n  use24Hour?: boolean\n  use12Hours?: boolean\n  minTime?: string\n  maxTime?: string\n  format?: TimeFormat\n  disabledHours?: () => number[]\n  disabledMinutes?: (selectedHour: number) => number[]\n  disabledSeconds?: (selectedHour: number, selectedMinute: number) => number[]\n  hideDisabledOptions?: boolean\n  presets?: TimeRangePreset[]\n  size?: TimeRangePickerSize\n  status?: TimeRangePickerStatus\n  triggerClass?: HTMLAttributes['class']\n  class?: HTMLAttributes['class']\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  modelValue: null,\n  placeholder: 'Pick a time range',\n  disabled: false,\n  readOnly: false,\n  clearable: true,\n  allowClear: undefined,\n  minuteStep: 5,\n  hourStep: 1,\n  secondStep: 1,\n  use24Hour: false,\n  use12Hours: false,\n  format: 'HH:mm',\n  hideDisabledOptions: false,\n  presets: () => [],\n  size: 'middle',\n})\n\nconst emits = defineEmits<{\n  'update:modelValue': [value: [string, string] | null]\n}>()\n\nconst open = ref(false)\n\nconst effectiveAllowClear = computed(() => {\n  if (props.allowClear !== undefined) return props.allowClear\n  return props.clearable\n})\n\nfunction parse(v: string) {\n  if (!v) return null\n  const m = /^(\\d{1,2}):(\\d{2})(?::(\\d{2}))?$/.exec(v.trim())\n  if (!m) return null\n  return { hour24: Number(m[1]), minute: Number(m[2]), second: m[3] ? Number(m[3]) : 0 }\n}\n\nconst startValue = computed(() => props.modelValue?.[0] ?? '')\nconst endValue = computed(() => props.modelValue?.[1] ?? '')\n\nconst startParsed = computed(() => parse(startValue.value))\nconst endParsed = computed(() => parse(endValue.value))\n\nconst effective12Hour = computed(() => {\n  if (props.format === 'hh:mm A') return true\n  if (props.use12Hours) return true\n  return !props.use24Hour\n})\n\nfunction formatDisplay(h: number, m: number, s: number) {\n  if (props.format === 'hh:mm A') {\n    const h12 = h % 12 === 0 ? 12 : h % 12\n    const period = h >= 12 ? 'PM' : 'AM'\n    return `${String(h12).padStart(2, '0')}:${String(m).padStart(2, '0')} ${period}`\n  }\n  if (props.format === 'HH:mm:ss') {\n    return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`\n  }\n  return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`\n}\n\nconst display = computed(() => {\n  const hasStart = startParsed.value !== null\n  const hasEnd = endParsed.value !== null\n  if (!hasStart && !hasEnd) return ''\n  const startStr = hasStart\n    ? formatDisplay(startParsed.value!.hour24, startParsed.value!.minute, startParsed.value!.second)\n    : ''\n  const endStr = hasEnd ? formatDisplay(endParsed.value!.hour24, endParsed.value!.minute, endParsed.value!.second) : ''\n  if (hasStart && hasEnd) return `${startStr} ~ ${endStr}`\n  return startStr || endStr\n})\n\nfunction emitRange(start: string, end: string) {\n  emits('update:modelValue', [start, end])\n}\n\nfunction emitStart(v: string) {\n  emitRange(v, endValue.value || v)\n}\n\nfunction emitEnd(v: string) {\n  emitRange(startValue.value || v, v)\n}\n\nfunction pickNow(which: 'start' | 'end') {\n  if (props.readOnly) return\n  const d = new Date()\n  const sStep = Math.max(1, props.secondStep)\n  const rawS = d.getSeconds()\n  const snappedS = Math.round(rawS / sStep) * sStep\n  const secCarry = Math.floor(snappedS / 60)\n  const s = snappedS % 60\n\n  const mStep = Math.max(1, props.minuteStep)\n  const rawM = d.getMinutes() + secCarry\n  const snappedM = Math.round(rawM / mStep) * mStep\n  const minCarry = Math.floor(snappedM / 60)\n  const min = snappedM % 60\n\n  const h = d.getHours() + minCarry\n  const hourStr = String(h).padStart(2, '0')\n  const minStr = String(min).padStart(2, '0')\n  const secStr = String(s).padStart(2, '0')\n  const v = props.format === 'HH:mm:ss' ? `${hourStr}:${minStr}:${secStr}` : `${hourStr}:${minStr}`\n  if (which === 'start') emitStart(v)\n  else emitEnd(v)\n}\n\nfunction applyPreset(preset: TimeRangePreset) {\n  if (props.readOnly) return\n  emits('update:modelValue', preset.value)\n  open.value = false\n}\n\nfunction clear(event: Event) {\n  event.stopPropagation()\n  if (props.disabled || props.readOnly) return\n  emits('update:modelValue', null)\n}\n\nconst sizeClasses: Record<TimeRangePickerSize, 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}\n\nconst statusClasses: Record<string, string> = {\n  error: 'border-destructive focus-visible:ring-destructive',\n  warning: 'border-warning focus-visible:ring-warning',\n}\n\nconst triggerClasses = computed(() =>\n  cn(\n    'min-w-[200px] justify-start gap-2 text-left font-normal',\n    !display.value && 'text-muted-foreground',\n    sizeClasses[props.size],\n    props.status && statusClasses[props.status],\n    props.triggerClass,\n    props.class,\n  ),\n)\n\nconst columnsVisible = ref(false)\nwatch(open, (v) => {\n  columnsVisible.value = v\n})\n</script>\n\n<template>\n  <Popover v-model:open=\"open\">\n    <PopoverTrigger as-child>\n      <Button\n        type=\"button\"\n        variant=\"outline\"\n        :disabled=\"disabled\"\n        :class=\"triggerClasses\"\n        data-uipkge\n        data-slot=\"time-range-picker\"\n      >\n        <Clock class=\"size-4 shrink-0\" />\n        <span class=\"flex-1 truncate\">{{ display || placeholder }}</span>\n        <button\n          v-if=\"effectiveAllowClear && display && !disabled && !readOnly\"\n          type=\"button\"\n          class=\"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          aria-label=\"Clear time range\"\n          @click.stop=\"clear\"\n        >\n          <X class=\"size-3.5\" aria-hidden=\"true\" />\n        </button>\n      </Button>\n    </PopoverTrigger>\n    <PopoverContent class=\"w-auto p-0\" align=\"start\">\n      <div v-if=\"presets.length\" class=\"flex flex-col gap-0.5 border-b p-2\">\n        <button\n          v-for=\"p in presets\"\n          :key=\"p.label\"\n          type=\"button\"\n          class=\"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\"\n          @click=\"applyPreset(p)\"\n        >\n          {{ p.label }}\n        </button>\n      </div>\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\n            type=\"button\"\n            class=\"text-muted-foreground hover:text-foreground focus-visible:ring-ring text-xs focus-visible:ring-2 focus-visible:outline-none\"\n            @click=\"pickNow('start')\"\n          >\n            Now (Start)\n          </button>\n          <button type=\"button\" class=\"text-muted-foreground hover:text-foreground text-xs\" @click=\"pickNow('end')\">\n            Now (End)\n          </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          <TimeColumns\n            :model-value=\"startValue\"\n            :use24-hour=\"use24Hour\"\n            :use12-hours=\"use12Hours\"\n            :minute-step=\"minuteStep\"\n            :hour-step=\"hourStep\"\n            :second-step=\"secondStep\"\n            :min-time=\"minTime\"\n            :max-time=\"maxTime\"\n            :format=\"format\"\n            :disabled-hours=\"disabledHours\"\n            :disabled-minutes=\"disabledMinutes\"\n            :disabled-seconds=\"disabledSeconds\"\n            :hide-disabled-options=\"hideDisabledOptions\"\n            :visible=\"columnsVisible\"\n            @update:model-value=\"emitStart\"\n          />\n        </div>\n        <div class=\"bg-border w-px\" />\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          <TimeColumns\n            :model-value=\"endValue\"\n            :use24-hour=\"use24Hour\"\n            :use12-hours=\"use12Hours\"\n            :minute-step=\"minuteStep\"\n            :hour-step=\"hourStep\"\n            :second-step=\"secondStep\"\n            :min-time=\"minTime\"\n            :max-time=\"maxTime\"\n            :format=\"format\"\n            :disabled-hours=\"disabledHours\"\n            :disabled-minutes=\"disabledMinutes\"\n            :disabled-seconds=\"disabledSeconds\"\n            :hide-disabled-options=\"hideDisabledOptions\"\n            :visible=\"columnsVisible\"\n            @update:model-value=\"emitEnd\"\n          />\n        </div>\n      </div>\n    </PopoverContent>\n  </Popover>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/time-picker/TimeRangePicker.vue"
    },
    {
      "path": "packages/registry-vue/components/time-picker/index.ts",
      "content": "export { default as TimePicker } from './TimePicker.vue'\nexport { default as TimeRangePicker } from './TimeRangePicker.vue'\nexport { default as TimeColumns } from './TimeColumns.vue'\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/time-picker/index.ts"
    }
  ],
  "dependencies": [
    "lucide-vue-next"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/vue/button.json",
    "https://uipkge.dev/r/vue/popover.json",
    "https://uipkge.dev/r/vue/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"
  ]
}