{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "time-picker",
  "title": "Time Picker",
  "description": "Hour, minute and second columns driven as listboxes.",
  "registryDependencies": [
    "https://indiacn.in/r/theme.json",
    "https://indiacn.in/r/typography.json",
    "https://indiacn.in/r/button.json"
  ],
  "files": [
    {
      "path": "components/ui/time-picker.tsx",
      "content": "'use client';\n\nimport { KeyboardEvent, useCallback, useEffect, useMemo, useRef } from 'react';\n\nimport { Button } from '@/components/ui/button';\nimport { Label1 } from '@/components/ui/typography';\nimport { cn } from '@/lib/utils';\n\n/*\n * UX4G time picker: three scrolling columns of 56x28 items. Hover fills\n * #faefff (primary-50) and the current value fills #ecd0ff (primary-100), with\n * 1px #dee2e6 dividers between the columns.\n *\n * Each column is a listbox where selection follows focus, so the arrow keys\n * change the value directly instead of needing a second confirm keystroke.\n * That is the right pattern for a spinner and it keeps each column to one tab\n * stop rather than sixty.\n *\n * The kit's guidance asks twice for a 12-hour option — \"include an option for\n * AM/PM where applicable\" and \"allow users to customize the format based on\n * regional preferences\" — so `hourCycle` adds a fourth column. The value stays\n * 24-hour throughout: only the display changes, because a component that hands\n * back \"7\" without saying which seven is worse than useless.\n */\nconst HOURS_IN_DAY = 24;\nconst MINUTES_IN_HOUR = 60;\n\nconst MERIDIEM_OPTIONS = [\n  { value: 0, label: 'AM' },\n  { value: 1, label: 'PM' },\n];\n\ninterface ITimeValue {\n  /** Always 0-23. `hourCycle` changes the display, never the value. */\n  hours: number;\n  minutes: number;\n  seconds: number;\n}\n\ntype THourCycle = 12 | 24;\n\n/** Zero-padded two-digit label, the only form the kit shows. */\nfunction pad(value: number) {\n  return String(value).padStart(2, '0');\n}\n\n/** \"14:05:00\", \"14:05\", or \"02:05 PM\" on a 12-hour clock. */\nfunction formatTime(value: ITimeValue, showSeconds: boolean, hourCycle: THourCycle = 24) {\n  const hours = hourCycle === 12 ? toClockHour(value.hours) : value.hours;\n  const base = `${pad(hours)}:${pad(value.minutes)}`;\n  const withSeconds = showSeconds ? `${base}:${pad(value.seconds)}` : base;\n\n  return hourCycle === 12 ? `${withSeconds} ${value.hours < 12 ? 'AM' : 'PM'}` : withSeconds;\n}\n\n/** The face a 24-hour value shows on a 12-hour clock: 0 and 12 both read 12. */\nfunction toClockHour(hours: number) {\n  return hours % 12 === 0 ? 12 : hours % 12;\n}\n\n/** Rebuilds a 24-hour value from a clock face and a meridiem. */\nfunction fromClockHour(clockHour: number, isAfternoon: boolean) {\n  return (clockHour % 12) + (isAfternoon ? 12 : 0);\n}\n\n/** Wall-clock time from a Date, with seconds. */\nfunction toTimeValue(date: Date): ITimeValue {\n  return { hours: date.getHours(), minutes: date.getMinutes(), seconds: date.getSeconds() };\n}\n\ninterface ITimeOptionProps {\n  option: number;\n  label: string;\n  selected: boolean;\n  onSelect: (value: number) => void;\n  registerRef: (option: number, node: HTMLButtonElement | null) => void;\n}\n\n/** One entry in a column. Split out so its callbacks stay stable. */\nfunction TimeOption({ option, label, selected, onSelect, registerRef }: ITimeOptionProps) {\n  const handleClick = useCallback(() => onSelect(option), [onSelect, option]);\n  const handleRef = useCallback(\n    (node: HTMLButtonElement | null) => registerRef(option, node),\n    [registerRef, option],\n  );\n\n  return (\n    <button\n      ref={handleRef}\n      type='button'\n      role='option'\n      aria-selected={selected}\n      tabIndex={selected ? 0 : -1}\n      onClick={handleClick}\n      className={cn(\n        'focus-visible:shadow-focus-primary text-neutral hover:bg-primary-50 flex h-7 w-14 shrink-0 cursor-pointer items-center justify-center rounded-md text-sm transition-colors focus-visible:outline-none',\n        selected && 'bg-primary-100 font-medium',\n      )}\n    >\n      {label}\n    </button>\n  );\n}\n\ninterface ITimeOption {\n  value: number;\n  label: string;\n}\n\ninterface ITimeColumnProps {\n  label: string;\n  options: ITimeOption[];\n  value: number;\n  onValueChange: (value: number) => void;\n}\n\n/**\n * One scrolling column, keyboard-driven as a listbox.\n *\n * Takes an options list rather than a count so the hour column can show 12, 1,\n * 2 … while still reporting 0-23, and so AM/PM is the same component.\n */\nfunction TimeColumn({ label, options, value, onValueChange }: ITimeColumnProps) {\n  const count = options.length;\n  const nodes = useRef(new Map<number, HTMLButtonElement>());\n  const list = useRef<HTMLDivElement>(null);\n\n  const registerRef = useCallback((option: number, node: HTMLButtonElement | null) => {\n    if (node) nodes.current.set(option, node);\n    else nodes.current.delete(option);\n  }, []);\n\n  // scrollTop rather than scrollIntoView: the latter also scrolls every\n  // ancestor, which would yank the page on mount. The container is `relative`\n  // so that offsetTop is measured against it and not the document body.\n  useEffect(() => {\n    const node = nodes.current.get(value);\n    const container = list.current;\n    if (!node || !container) return;\n    container.scrollTop = node.offsetTop - (container.clientHeight - node.offsetHeight) / 2;\n  }, [value]);\n\n  const handleKeyDown = useCallback(\n    (event: KeyboardEvent<HTMLDivElement>) => {\n      const steps: Record<string, number> = {\n        ArrowUp: -1,\n        ArrowDown: 1,\n        PageUp: -10,\n        PageDown: 10,\n      };\n      const current = options.findIndex(option => option.value === value);\n      const move = (index: number) => {\n        const next = options[((index % count) + count) % count];\n        onValueChange(next.value);\n        nodes.current.get(next.value)?.focus();\n      };\n\n      if (event.key in steps) {\n        event.preventDefault();\n        move(current + steps[event.key]);\n\n        return;\n      }\n      if (event.key === 'Home' || event.key === 'End') {\n        event.preventDefault();\n        move(event.key === 'Home' ? 0 : count - 1);\n      }\n    },\n    [value, count, options, onValueChange],\n  );\n\n  return (\n    <div\n      ref={list}\n      role='listbox'\n      aria-label={label}\n      onKeyDown={handleKeyDown}\n      // h-56 is exactly eight 28px rows. With a gap the eighth row was sliced\n      // through the middle of its digits, which reads as a rendering fault.\n      className='relative flex h-56 flex-col overflow-y-auto px-1.5'\n    >\n      {options.map(option => (\n        <TimeOption\n          key={option.value}\n          option={option.value}\n          label={option.label}\n          selected={option.value === value}\n          onSelect={onValueChange}\n          registerRef={registerRef}\n        />\n      ))}\n    </div>\n  );\n}\n\ninterface ITimeColumnsProps {\n  value: ITimeValue;\n  onValueChange: (value: ITimeValue) => void;\n  showSeconds?: boolean;\n  /** 12 adds an AM/PM column and shows hours as 12, 1, 2 … */\n  hourCycle?: THourCycle;\n  className?: string;\n}\n\n/**\n * The three columns on their own, for composing into a larger surface.\n *\n * DateTimePicker uses this beside a Calendar; TimePicker wraps it in the card\n * with the header and footer.\n */\nfunction TimeColumns({\n  value,\n  onValueChange,\n  showSeconds = true,\n  hourCycle = 24,\n  className,\n}: ITimeColumnsProps) {\n  const isAfternoon = value.hours >= 12;\n\n  const setHours = useCallback(\n    (hours: number) => onValueChange({ ...value, hours }),\n    [onValueChange, value],\n  );\n  const setClockHour = useCallback(\n    (clockHour: number) =>\n      onValueChange({ ...value, hours: fromClockHour(clockHour, value.hours >= 12) }),\n    [onValueChange, value],\n  );\n  const setMeridiem = useCallback(\n    (afternoon: number) =>\n      onValueChange({ ...value, hours: fromClockHour(toClockHour(value.hours), afternoon === 1) }),\n    [onValueChange, value],\n  );\n  const setMinutes = useCallback(\n    (minutes: number) => onValueChange({ ...value, minutes }),\n    [onValueChange, value],\n  );\n  const setSeconds = useCallback(\n    (seconds: number) => onValueChange({ ...value, seconds }),\n    [onValueChange, value],\n  );\n\n  const hourOptions = useMemo(\n    () =>\n      hourCycle === 12\n        ? // 12 first, matching every analogue clock face\n          [12, ...Array.from({ length: 11 }, (_, index) => index + 1)].map(hour => ({\n            value: hour,\n            label: pad(hour),\n          }))\n        : Array.from({ length: HOURS_IN_DAY }, (_, hour) => ({ value: hour, label: pad(hour) })),\n    [hourCycle],\n  );\n  const minuteOptions = useMemo(\n    () => Array.from({ length: MINUTES_IN_HOUR }, (_, unit) => ({ value: unit, label: pad(unit) })),\n    [],\n  );\n\n  return (\n    <div className={cn('flex divide-x divide-neutral-100', className)}>\n      <TimeColumn\n        label='Hour'\n        options={hourOptions}\n        value={hourCycle === 12 ? toClockHour(value.hours) : value.hours}\n        onValueChange={hourCycle === 12 ? setClockHour : setHours}\n      />\n      <TimeColumn\n        label='Minute'\n        options={minuteOptions}\n        value={value.minutes}\n        onValueChange={setMinutes}\n      />\n      {showSeconds && (\n        <TimeColumn\n          label='Second'\n          options={minuteOptions}\n          value={value.seconds}\n          onValueChange={setSeconds}\n        />\n      )}\n      {hourCycle === 12 && (\n        <TimeColumn\n          label='AM or PM'\n          options={MERIDIEM_OPTIONS}\n          value={isAfternoon ? 1 : 0}\n          onValueChange={setMeridiem}\n        />\n      )}\n    </div>\n  );\n}\n\ninterface ITimePickerProps {\n  value: ITimeValue;\n  onValueChange: (value: ITimeValue) => void;\n  showSeconds?: boolean;\n  /** 12 adds an AM/PM column and shows hours as 12, 1, 2 … */\n  hourCycle?: THourCycle;\n  /** Shows the header echoing the current value. */\n  showHeader?: boolean;\n  /** Called by the Ok button. Omit it and the button is not rendered. */\n  onConfirm?: () => void;\n  className?: string;\n}\n\n/** Time picker card: the value echoed above the columns, Now and Ok below. */\nfunction TimePicker({\n  value,\n  onValueChange,\n  showSeconds = true,\n  hourCycle = 24,\n  showHeader = false,\n  onConfirm,\n  className,\n}: ITimePickerProps) {\n  const handleNow = useCallback(() => onValueChange(toTimeValue(new Date())), [onValueChange]);\n\n  return (\n    <div\n      className={cn(\n        'bg-neutral-0 inline-flex flex-col divide-y divide-neutral-100 rounded-lg shadow-md',\n        className,\n      )}\n    >\n      {showHeader && (\n        <Label1 className='text-neutral px-4 py-2.5 text-center tabular-nums'>\n          {formatTime(value, showSeconds, hourCycle)}\n        </Label1>\n      )}\n      <TimeColumns\n        value={value}\n        onValueChange={onValueChange}\n        showSeconds={showSeconds}\n        hourCycle={hourCycle}\n        className='py-2'\n      />\n      <div className='flex items-center justify-between gap-3 p-2.5'>\n        <Button variant='text' size='sm' onClick={handleNow}>\n          Now\n        </Button>\n        {onConfirm && (\n          <Button size='sm' onClick={onConfirm}>\n            Ok\n          </Button>\n        )}\n      </div>\n    </div>\n  );\n}\n\nexport type { ITimeValue, THourCycle };\n\nexport { TimePicker, TimeColumns, formatTime, toTimeValue, toClockHour, fromClockHour };\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}
