{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "time-picker",
  "title": "Time Picker",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-svelte/components/time-picker/TimeColumns.svelte",
      "content": "<script lang=\"ts\" module>\n  export interface TimeParts {\n    hour24: number\n    minute: number\n    second: number\n  }\n\n  export type TimeFormat = 'HH:mm' | 'HH:mm:ss' | 'hh:mm A'\n\n  export interface TimeColumnsProps {\n    /** Time value. HH:mm or HH:mm:ss depending on format. Empty = no selection. */\n    value?: 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    /** Called with the new 24h time string after every pick. */\n    onValueChange?: (value: string) => void\n  }\n</script>\n\n<script lang=\"ts\">\n  import { tick } from 'svelte'\n  import { cn } from '$lib/utils'\n\n  let {\n    value = $bindable(''),\n    use24Hour = false,\n    use12Hours = false,\n    minuteStep = 5,\n    hourStep = 1,\n    secondStep = 1,\n    minTime,\n    maxTime,\n    format = 'HH:mm',\n    disabledHours,\n    disabledMinutes,\n    disabledSeconds,\n    hideDisabledOptions = false,\n    visible = true,\n    onValueChange,\n  }: TimeColumnsProps = $props()\n\n  function 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\n  function format24(p: TimeParts) {\n    if (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\n  function toMinutes(p: TimeParts) {\n    return p.hour24 * 60 + p.minute + p.second / 60\n  }\n\n  let parts: TimeParts | null = $state(parse(value))\n  $effect(() => {\n    parts = parse(value)\n  })\n\n  const showSeconds = $derived(format === 'HH:mm:ss')\n\n  // Default to 24h columns. Opt into 12h via format=\"hh:mm A\" or use12Hours.\n  // (use24Hour remains as an explicit 24h force for API compat; it no longer\n  // inverts the default when false — that made HH:mm demos show AM/PM by accident.)\n  const effective12Hour = $derived.by(() => {\n    if (format === 'hh:mm A') return true\n    if (use12Hours) return true\n    if (use24Hour) return false\n    return false\n  })\n\n  const hour12 = $derived.by(() => {\n    if (!parts) return null\n    const h = parts.hour24 % 12\n    return h === 0 ? 12 : h\n  })\n  const period = $derived(parts && parts.hour24 >= 12 ? 'PM' : 'AM')\n\n  const minBound = $derived(parse(minTime ?? '') ?? null)\n  const maxBound = $derived(parse(maxTime ?? '') ?? null)\n\n  function withinBounds(p: TimeParts) {\n    const m = toMinutes(p)\n    if (minBound && m < toMinutes(minBound)) return false\n    if (maxBound && m > toMinutes(maxBound)) return false\n    return true\n  }\n\n  const disabledHoursSet = $derived(new Set(disabledHours ? disabledHours() : []))\n\n  const disabledMinutesSet = $derived.by(() => {\n    const h = parts?.hour24 ?? 0\n    return new Set(disabledMinutes ? disabledMinutes(h) : [])\n  })\n\n  const disabledSecondsSet = $derived.by(() => {\n    const h = parts?.hour24 ?? 0\n    const m = parts?.minute ?? 0\n    return new Set(disabledSeconds ? disabledSeconds(h, m) : [])\n  })\n\n  function isHourDisabledItem(h: number) {\n    if (disabledHoursSet.has(h)) return true\n    const cur = parts ?? { hour24: 0, minute: 0, second: 0 }\n    if (effective12Hour) {\n      const isPM = period === '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\n  function isMinuteDisabledItem(m: number) {\n    if (disabledMinutesSet.has(m)) return true\n    const cur = parts ?? { hour24: 0, minute: 0, second: 0 }\n    return !withinBounds({ hour24: cur.hour24, minute: m, second: cur.second })\n  }\n\n  function isSecondDisabledItem(s: number) {\n    if (disabledSecondsSet.has(s)) return true\n    const cur = parts ?? { hour24: 0, minute: 0, second: 0 }\n    return !withinBounds({ hour24: cur.hour24, minute: cur.minute, second: s })\n  }\n\n  const hours12List = $derived.by(() => {\n    const step = Math.max(1, hourStep)\n    const list = Array.from({ length: 12 }, (_, i) => i + 1).filter((h) => (h - 1) % step === 0)\n    if (!hideDisabledOptions) return list\n    return list.filter((h) => !isHourDisabledItem(h))\n  })\n\n  const hours24List = $derived.by(() => {\n    const step = Math.max(1, hourStep)\n    const list = Array.from({ length: 24 }, (_, i) => i).filter((h) => h % step === 0)\n    if (!hideDisabledOptions) return list\n    return list.filter((h) => !isHourDisabledItem(h))\n  })\n\n  const minutesList = $derived.by(() => {\n    const step = Math.max(1, Math.min(60, minuteStep))\n    const list = Array.from({ length: Math.ceil(60 / step) }, (_, i) => i * step)\n    if (!hideDisabledOptions) return list\n    return list.filter((m) => !isMinuteDisabledItem(m))\n  })\n\n  const secondsList = $derived.by(() => {\n    const step = Math.max(1, Math.min(60, secondStep))\n    const list = Array.from({ length: Math.ceil(60 / step) }, (_, i) => i * step)\n    if (!hideDisabledOptions) return list\n    return list.filter((s) => !isSecondDisabledItem(s))\n  })\n\n  function commit(p: TimeParts) {\n    if (!withinBounds(p)) return\n    parts = p\n    value = format24(p)\n    onValueChange?.(value)\n  }\n\n  function pickHour12(h: number) {\n    const cur = parts ?? { hour24: 0, minute: 0, second: 0 }\n    const isPM = period === 'PM'\n    commit({ hour24: (h % 12) + (isPM ? 12 : 0), minute: cur.minute, second: cur.second })\n  }\n\n  function pickHour24(h: number) {\n    const cur = parts ?? { hour24: 0, minute: 0, second: 0 }\n    commit({ hour24: h, minute: cur.minute, second: cur.second })\n  }\n\n  function pickMinute(m: number) {\n    const cur = parts ?? { hour24: 0, minute: 0, second: 0 }\n    commit({ hour24: cur.hour24, minute: m, second: cur.second })\n  }\n\n  function pickSecond(s: number) {\n    const cur = parts ?? { hour24: 0, minute: 0, second: 0 }\n    commit({ hour24: cur.hour24, minute: cur.minute, second: s })\n  }\n\n  function pickPeriod(p: 'AM' | 'PM') {\n    const cur = parts ?? { 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\n  let hourCol: HTMLDivElement | null = $state(null)\n  let minCol: HTMLDivElement | null = $state(null)\n  let secCol: HTMLDivElement | null = $state(null)\n\n  function scrollActiveIntoView() {\n    for (const col of [hourCol, minCol, secCol]) {\n      col?.querySelector<HTMLElement>('[data-active=\"true\"]')?.scrollIntoView({ block: 'center' })\n    }\n  }\n\n  $effect(() => {\n    if (!visible) return\n    tick().then(scrollActiveIntoView)\n  })\n\n  function isHourActive(h: number) {\n    if (!parts) return false\n    return effective12Hour ? hour12 === h : parts.hour24 === h\n  }\n\n  function isHourDisabled(h: number) {\n    if (!hideDisabledOptions) return isHourDisabledItem(h)\n    return false\n  }\n\n  function isMinuteDisabled(m: number) {\n    if (!hideDisabledOptions) return isMinuteDisabledItem(m)\n    return false\n  }\n\n  function isSecondDisabled(s: number) {\n    if (!hideDisabledOptions) return isSecondDisabledItem(s)\n    return false\n  }\n\n  const optionClass =\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'\n</script>\n\n<div class=\"flex divide-x\" data-uipkge=\"\" data-slot=\"time-columns\">\n  <div bind:this={hourCol} class=\"h-56 overflow-y-auto\">\n    <div class=\"flex w-14 flex-col p-1\">\n      {#each effective12Hour ? hours12List : hours24List as h (h)}\n        <button\n          type=\"button\"\n          data-active={isHourActive(h)}\n          disabled={isHourDisabled(h)}\n          class={cn(optionClass, isHourActive(h) && 'bg-primary text-primary-foreground hover:bg-primary')}\n          onclick={() => (effective12Hour ? pickHour12(h) : pickHour24(h))}\n        >\n          {String(h).padStart(2, '0')}\n        </button>\n      {/each}\n    </div>\n  </div>\n\n  <div bind:this={minCol} class=\"h-56 overflow-y-auto\">\n    <div class=\"flex w-14 flex-col p-1\">\n      {#each minutesList as m (m)}\n        <button\n          type=\"button\"\n          data-active={parts?.minute === m}\n          disabled={isMinuteDisabled(m)}\n          class={cn(optionClass, parts?.minute === m && 'bg-primary text-primary-foreground hover:bg-primary')}\n          onclick={() => pickMinute(m)}\n        >\n          {String(m).padStart(2, '0')}\n        </button>\n      {/each}\n    </div>\n  </div>\n\n  {#if showSeconds}\n    <div bind:this={secCol} class=\"h-56 overflow-y-auto\">\n      <div class=\"flex w-14 flex-col p-1\">\n        {#each secondsList as s (s)}\n          <button\n            type=\"button\"\n            data-active={parts?.second === s}\n            disabled={isSecondDisabled(s)}\n            class={cn(optionClass, parts?.second === s && 'bg-primary text-primary-foreground hover:bg-primary')}\n            onclick={() => pickSecond(s)}\n          >\n            {String(s).padStart(2, '0')}\n          </button>\n        {/each}\n      </div>\n    </div>\n  {/if}\n\n  {#if effective12Hour}\n    <div class=\"flex w-12 flex-col p-1\">\n      {#each ['AM', 'PM'] as p (p)}\n        <button\n          type=\"button\"\n          class={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            period === p && 'bg-primary text-primary-foreground hover:bg-primary',\n          )}\n          onclick={() => pickPeriod(p as 'AM' | 'PM')}\n        >\n          {p}\n        </button>\n      {/each}\n    </div>\n  {/if}\n</div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/time-picker/TimeColumns.svelte"
    },
    {
      "path": "packages/registry-svelte/components/time-picker/TimePicker.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { TimeFormat } from './TimeColumns.svelte'\n\n  export interface TimePreset {\n    label: string\n    value: string\n  }\n\n  export type TimePickerSize = 'small' | 'middle' | 'large'\n  export type TimePickerStatus = 'error' | 'warning'\n\n  export interface TimePickerProps {\n    /** Controlled value as 24h HH:mm or HH:mm:ss. Empty string = no selection. */\n    value?: 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?: string\n    class?: string\n    /** Called with the new 24h time string after every change. */\n    onValueChange?: (value: string) => void\n    ref?: HTMLDivElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { cn } from '$lib/utils'\n  import { buttonVariants } from '$lib/components/ui/button/button.variants'\n  import { Clock, X } from '@lucide/svelte'\n  import TimeColumns from './TimeColumns.svelte'\n\n  let {\n    value = $bindable(''),\n    placeholder = 'Pick a time',\n    disabled = false,\n    readOnly = false,\n    clearable = true,\n    allowClear,\n    minuteStep = 5,\n    hourStep = 1,\n    secondStep = 1,\n    use24Hour = false,\n    use12Hours = false,\n    minTime,\n    maxTime,\n    format = 'HH:mm',\n    disabledHours,\n    disabledMinutes,\n    disabledSeconds,\n    hideDisabledOptions = false,\n    presets = [],\n    size = 'middle',\n    status,\n    triggerClass,\n    class: className,\n    onValueChange,\n    ref = $bindable(null),\n  }: TimePickerProps = $props()\n\n  let open = $state(false)\n\n  const effectiveAllowClear = $derived(allowClear !== undefined ? allowClear : clearable)\n\n  function 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\n  const parsed = $derived(parse(value))\n\n  function formatDisplay(h: number, m: number, s: number) {\n    if (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 (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\n  const display = $derived.by(() => {\n    if (!parsed) return ''\n    return formatDisplay(parsed.hour24, parsed.minute, parsed.second)\n  })\n\n  function emitTime(v: string) {\n    value = v\n    onValueChange?.(v)\n  }\n\n  function pickNow() {\n    if (readOnly) return\n    const d = new Date()\n    const sStep = Math.max(1, 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, 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) % 24\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(format === 'HH:mm:ss' ? `${hourStr}:${minStr}:${secStr}` : `${hourStr}:${minStr}`)\n  }\n\n  function applyPreset(preset: TimePreset) {\n    if (readOnly) return\n    emitTime(preset.value)\n    open = false\n  }\n\n  function clear(event: Event) {\n    event.stopPropagation()\n    if (disabled || readOnly) return\n    emitTime('')\n  }\n\n  const 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\n  const statusClasses: Record<string, string> = {\n    error: 'border-destructive focus-visible:ring-destructive',\n    warning: 'border-warning focus-visible:ring-warning',\n  }\n\n  const triggerClasses = $derived(\n    cn(\n      buttonVariants({ variant: 'outline' }),\n      'min-w-40 justify-start gap-2 text-left font-normal',\n      !parsed && 'text-muted-foreground',\n      sizeClasses[size],\n      status && statusClasses[status],\n      triggerClass,\n      className,\n    ),\n  )\n\n  // Pulse the columns when reopen happens (passes via prop reactivity).\n  let columnsVisible = $state(false)\n  $effect(() => {\n    columnsVisible = open\n  })\n\n  /** Close on outside pointer-down or Escape. Hand-rolled popover behavior (no bits-ui). */\n  function dismissable(node: HTMLElement) {\n    function onPointerDown(e: PointerEvent) {\n      if (!node.contains(e.target as Node)) open = false\n    }\n    function onKeyDown(e: KeyboardEvent) {\n      if (e.key === 'Escape') open = false\n    }\n    document.addEventListener('pointerdown', onPointerDown)\n    document.addEventListener('keydown', onKeyDown)\n    return {\n      destroy() {\n        document.removeEventListener('pointerdown', onPointerDown)\n        document.removeEventListener('keydown', onKeyDown)\n      },\n    }\n  }\n</script>\n\n<div bind:this={ref} use:dismissable data-uipkge=\"\" data-slot=\"time-picker\" class=\"relative inline-block\">\n  <button\n    type=\"button\"\n    {disabled}\n    class={triggerClasses}\n    aria-haspopup=\"dialog\"\n    aria-expanded={open}\n    onclick={() => {\n      if (!disabled) open = !open\n    }}\n  >\n    <Clock class=\"size-4 shrink-0\" aria-hidden=\"true\" />\n    <span class=\"flex-1 truncate\">{display || placeholder}</span>\n    <!-- span (not button): nested interactive elements inside a button are invalid HTML -->\n    {#if effectiveAllowClear && parsed && !disabled && !readOnly}\n      <span\n        role=\"button\"\n        tabindex=\"-1\"\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        onclick={clear}\n        onmousedown={(e) => e.preventDefault()}\n        onkeydown={(e) => {\n          if (e.key === 'Enter' || e.key === ' ') clear(e)\n        }}\n      >\n        <X class=\"size-3.5\" aria-hidden=\"true\" />\n      </span>\n    {/if}\n  </button>\n  {#if open}\n    <div\n      role=\"dialog\"\n      aria-label=\"Choose time\"\n      data-slot=\"time-picker-content\"\n      class=\"bg-popover text-popover-foreground absolute top-full left-0 z-50 mt-1 w-auto rounded-md border p-0 shadow-md\"\n    >\n      {#if presets.length}\n        <div class=\"flex flex-col gap-0.5 border-b p-2\">\n          {#each presets as p (p.label)}\n            <button\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              onclick={() => applyPreset(p)}\n            >\n              {p.label}\n            </button>\n          {/each}\n        </div>\n      {/if}\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          onclick={pickNow}\n        >\n          Now\n        </button>\n      </div>\n      <TimeColumns\n        bind:value\n        {use24Hour}\n        {use12Hours}\n        {minuteStep}\n        {hourStep}\n        {secondStep}\n        {minTime}\n        {maxTime}\n        {format}\n        {disabledHours}\n        {disabledMinutes}\n        {disabledSeconds}\n        {hideDisabledOptions}\n        visible={columnsVisible}\n        onValueChange={(v) => onValueChange?.(v)}\n      />\n    </div>\n  {/if}\n</div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/time-picker/TimePicker.svelte"
    },
    {
      "path": "packages/registry-svelte/components/time-picker/TimeRangePicker.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { TimeFormat } from './TimeColumns.svelte'\n\n  export interface TimeRangePreset {\n    label: string\n    value: [string, string]\n  }\n\n  export type TimeRangePickerSize = 'small' | 'middle' | 'large'\n  export type TimeRangePickerStatus = 'error' | 'warning'\n\n  export interface TimeRangePickerProps {\n    /** Controlled value as [startTime, endTime] in 24h format. */\n    value?: [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?: string\n    class?: string\n    /** Called with the new [start, end] range after every change. */\n    onValueChange?: (value: [string, string] | null) => void\n    ref?: HTMLDivElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { cn } from '$lib/utils'\n  import { buttonVariants } from '$lib/components/ui/button/button.variants'\n  import { Clock, X } from '@lucide/svelte'\n  import TimeColumns from './TimeColumns.svelte'\n\n  let {\n    value = $bindable(null),\n    placeholder = 'Pick a time range',\n    disabled = false,\n    readOnly = false,\n    clearable = true,\n    allowClear,\n    minuteStep = 5,\n    hourStep = 1,\n    secondStep = 1,\n    use24Hour = false,\n    use12Hours = false,\n    minTime,\n    maxTime,\n    format = 'HH:mm',\n    disabledHours,\n    disabledMinutes,\n    disabledSeconds,\n    hideDisabledOptions = false,\n    presets = [],\n    size = 'middle',\n    status,\n    triggerClass,\n    class: className,\n    onValueChange,\n    ref = $bindable(null),\n  }: TimeRangePickerProps = $props()\n\n  let open = $state(false)\n\n  const effectiveAllowClear = $derived(allowClear !== undefined ? allowClear : clearable)\n\n  function 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\n  const startValue = $derived(value?.[0] ?? '')\n  const endValue = $derived(value?.[1] ?? '')\n\n  const startParsed = $derived(parse(startValue))\n  const endParsed = $derived(parse(endValue))\n\n  function formatDisplay(h: number, m: number, s: number) {\n    if (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 (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\n  const display = $derived.by(() => {\n    const hasStart = startParsed !== null\n    const hasEnd = endParsed !== null\n    if (!hasStart && !hasEnd) return ''\n    const startStr = hasStart ? formatDisplay(startParsed!.hour24, startParsed!.minute, startParsed!.second) : ''\n    const endStr = hasEnd ? formatDisplay(endParsed!.hour24, endParsed!.minute, endParsed!.second) : ''\n    if (hasStart && hasEnd) return `${startStr} ~ ${endStr}`\n    return startStr || endStr\n  })\n\n  function emitRange(start: string, end: string) {\n    value = [start, end]\n    onValueChange?.([start, end])\n  }\n\n  function emitStart(v: string) {\n    emitRange(v, endValue || v)\n  }\n\n  function emitEnd(v: string) {\n    emitRange(startValue || v, v)\n  }\n\n  function pickNow(which: 'start' | 'end') {\n    if (readOnly) return\n    const d = new Date()\n    const sStep = Math.max(1, 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, 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) % 24\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 = format === 'HH:mm:ss' ? `${hourStr}:${minStr}:${secStr}` : `${hourStr}:${minStr}`\n    if (which === 'start') emitStart(v)\n    else emitEnd(v)\n  }\n\n  function applyPreset(preset: TimeRangePreset) {\n    if (readOnly) return\n    value = preset.value\n    onValueChange?.(preset.value)\n    open = false\n  }\n\n  function clear(event: Event) {\n    event.stopPropagation()\n    if (disabled || readOnly) return\n    value = null\n    onValueChange?.(null)\n  }\n\n  const 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\n  const statusClasses: Record<string, string> = {\n    error: 'border-destructive focus-visible:ring-destructive',\n    warning: 'border-warning focus-visible:ring-warning',\n  }\n\n  const triggerClasses = $derived(\n    cn(\n      buttonVariants({ variant: 'outline' }),\n      'min-w-48 justify-start gap-2 text-left font-normal',\n      !display && 'text-muted-foreground',\n      sizeClasses[size],\n      status && statusClasses[status],\n      triggerClass,\n      className,\n    ),\n  )\n\n  let columnsVisible = $state(false)\n  $effect(() => {\n    columnsVisible = open\n  })\n\n  /** Close on outside pointer-down or Escape. Hand-rolled popover behavior (no bits-ui). */\n  function dismissable(node: HTMLElement) {\n    function onPointerDown(e: PointerEvent) {\n      if (!node.contains(e.target as Node)) open = false\n    }\n    function onKeyDown(e: KeyboardEvent) {\n      if (e.key === 'Escape') open = false\n    }\n    document.addEventListener('pointerdown', onPointerDown)\n    document.addEventListener('keydown', onKeyDown)\n    return {\n      destroy() {\n        document.removeEventListener('pointerdown', onPointerDown)\n        document.removeEventListener('keydown', onKeyDown)\n      },\n    }\n  }\n</script>\n\n<div bind:this={ref} use:dismissable data-uipkge=\"\" data-slot=\"time-range-picker\" class=\"relative inline-block\">\n  <button\n    type=\"button\"\n    {disabled}\n    class={triggerClasses}\n    aria-haspopup=\"dialog\"\n    aria-expanded={open}\n    onclick={() => {\n      if (!disabled) open = !open\n    }}\n  >\n    <Clock class=\"size-4 shrink-0\" aria-hidden=\"true\" />\n    <span class=\"flex-1 truncate\">{display || placeholder}</span>\n    <!-- span (not button): nested interactive elements inside a button are invalid HTML -->\n    {#if effectiveAllowClear && display && !disabled && !readOnly}\n      <span\n        role=\"button\"\n        tabindex=\"-1\"\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        onclick={clear}\n        onmousedown={(e) => e.preventDefault()}\n        onkeydown={(e) => {\n          if (e.key === 'Enter' || e.key === ' ') clear(e)\n        }}\n      >\n        <X class=\"size-3.5\" aria-hidden=\"true\" />\n      </span>\n    {/if}\n  </button>\n  {#if open}\n    <div\n      role=\"dialog\"\n      aria-label=\"Choose time range\"\n      data-slot=\"time-range-picker-content\"\n      class=\"bg-popover text-popover-foreground absolute top-full left-0 z-50 mt-1 w-auto rounded-md border p-0 shadow-md\"\n    >\n      {#if presets.length}\n        <div class=\"flex flex-col gap-0.5 border-b p-2\">\n          {#each presets as p (p.label)}\n            <button\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              onclick={() => applyPreset(p)}\n            >\n              {p.label}\n            </button>\n          {/each}\n        </div>\n      {/if}\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            onclick={() => pickNow('start')}\n          >\n            Now (Start)\n          </button>\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            onclick={() => pickNow('end')}\n          >\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            value={startValue}\n            {use24Hour}\n            {use12Hours}\n            {minuteStep}\n            {hourStep}\n            {secondStep}\n            {minTime}\n            {maxTime}\n            {format}\n            {disabledHours}\n            {disabledMinutes}\n            {disabledSeconds}\n            {hideDisabledOptions}\n            visible={columnsVisible}\n            onValueChange={emitStart}\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          <TimeColumns\n            value={endValue}\n            {use24Hour}\n            {use12Hours}\n            {minuteStep}\n            {hourStep}\n            {secondStep}\n            {minTime}\n            {maxTime}\n            {format}\n            {disabledHours}\n            {disabledMinutes}\n            {disabledSeconds}\n            {hideDisabledOptions}\n            visible={columnsVisible}\n            onValueChange={emitEnd}\n          />\n        </div>\n      </div>\n    </div>\n  {/if}\n</div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/time-picker/TimeRangePicker.svelte"
    },
    {
      "path": "packages/registry-svelte/components/time-picker/index.ts",
      "content": "export { default as TimePicker, type TimePickerProps, type TimePreset, type TimePickerSize, type TimePickerStatus } from './TimePicker.svelte'\nexport { default as TimeRangePicker, type TimeRangePickerProps, type TimeRangePreset, type TimeRangePickerSize, type TimeRangePickerStatus } from './TimeRangePicker.svelte'\nexport { default as TimeColumns, type TimeColumnsProps, type TimeParts, type TimeFormat } from './TimeColumns.svelte'\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/time-picker/index.ts"
    }
  ],
  "dependencies": [
    "@lucide/svelte"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/svelte/button.json"
  ],
  "description": "Standalone time input — hours, minutes, optional seconds, and 12h/24h modes. Pairs with Date Picker for full datetime entry.",
  "categories": [
    "date-time"
  ]
}