{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "time-picker",
  "title": "Time Picker",
  "description": "An hour/minute/second wheel-based time picker, with a responsive Popover-on-desktop, Drawer-on-mobile composition.",
  "dependencies": [
    "@ncdai/react-wheel-picker",
    "@base-ui/react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.persian-labs.ir/r/button.json",
    "https://ui.persian-labs.ir/r/wheel-picker.json",
    "https://ui.persian-labs.ir/r/popover.json",
    "https://ui.persian-labs.ir/r/drawer.json",
    "https://ui.persian-labs.ir/r/persian-date.json"
  ],
  "files": [
    {
      "path": "registry/base/ui/time-picker.tsx",
      "content": "\"use client\"\n\nimport { CheckIcon } from \"lucide-react\"\nimport * as React from \"react\"\n\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Drawer,\n  DrawerFooter,\n  DrawerPanel,\n  DrawerPopup,\n  DrawerTitle,\n  DrawerTrigger,\n} from \"@/components/ui/drawer\"\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\"\nimport type { WheelPickerOption } from \"@/components/ui/wheel-picker\"\nimport { WheelPicker, WheelPickerWrapper } from \"@/components/ui/wheel-picker\"\nimport { useIsMobile } from \"@/hooks/use-media-query\"\nimport { toPersianDigits, type DigitStyle } from \"@/lib/persian-date\"\nimport { cn } from \"@/lib/utils\"\n\n/** Time-of-day value. Plain fields, not a `Date` -- a clock has no calendar date. */\nexport interface TimePickerValue {\n  hour: number\n  minute: number\n  second?: number\n}\n\n/** Mirrors persian-date's `DigitStyle`, kept local so this file has no digit-only dependency on it. */\nexport type TimePickerDigits = DigitStyle\n\nexport type TimePickerHourFormat = \"24\" | \"12\"\n\nconst DEFAULT_VALUE: TimePickerValue = { hour: 0, minute: 0 }\n\nfunction clamp(value: number, min: number, max: number): number {\n  return Math.min(max, Math.max(min, value))\n}\n\nfunction pad(value: number): string {\n  return value.toString().padStart(2, \"0\")\n}\n\nfunction label(value: number, digits: TimePickerDigits): string {\n  const text = pad(value)\n  return digits === \"fa\" ? toPersianDigits(text) : text\n}\n\nfunction to12Hour(hour: number): { hour12: number; period: \"AM\" | \"PM\" } {\n  const period: \"AM\" | \"PM\" = hour >= 12 ? \"PM\" : \"AM\"\n  const hour12 = hour % 12 === 0 ? 12 : hour % 12\n  return { hour12, period }\n}\n\nfunction from12Hour(hour12: number, period: \"AM\" | \"PM\"): number {\n  const base = hour12 % 12\n  return period === \"PM\" ? base + 12 : base\n}\n\nfunction periodOptions(\n  digits: TimePickerDigits\n): WheelPickerOption<\"AM\" | \"PM\">[] {\n  return digits === \"fa\"\n    ? [\n        { label: \"ق.ظ\", value: \"AM\" },\n        { label: \"ب.ظ\", value: \"PM\" },\n      ]\n    : [\n        { label: \"AM\", value: \"AM\" },\n        { label: \"PM\", value: \"PM\" },\n      ]\n}\n\n/** Formats a `TimePickerValue` for display, e.g. \"09:30\" or \"09:30 ب.ظ\". */\nexport function formatTimePickerValue(\n  value: TimePickerValue,\n  options: {\n    hourFormat?: TimePickerHourFormat\n    showSeconds?: boolean\n    digits?: TimePickerDigits\n  } = {}\n): string {\n  const hourFormat = options.hourFormat ?? \"24\"\n  const showSeconds = options.showSeconds ?? false\n  const digits = options.digits ?? \"fa\"\n\n  const second = value.second ?? 0\n  const parts: string[] = []\n\n  if (hourFormat === \"12\") {\n    const { hour12, period } = to12Hour(value.hour)\n    parts.push(label(hour12, digits))\n    parts.push(label(value.minute, digits))\n    if (showSeconds) parts.push(label(second, digits))\n    const periodLabel = periodOptions(digits).find(\n      (o) => o.value === period\n    )?.label\n    return `${parts.join(\":\")} ${periodLabel}`\n  }\n\n  parts.push(label(value.hour, digits))\n  parts.push(label(value.minute, digits))\n  if (showSeconds) parts.push(label(second, digits))\n  return parts.join(\":\")\n}\n\nexport interface TimePickerProps {\n  /** Current value in controlled mode. */\n  value?: TimePickerValue\n  /** Initial value in uncontrolled mode. @default { hour: 0, minute: 0 } */\n  defaultValue?: TimePickerValue\n  onValueChange?: (value: TimePickerValue) => void\n  /** Adds a seconds wheel. @default false */\n  showSeconds?: boolean\n  /** Shows h, m, and s labels above their wheel columns. @default false */\n  showLabels?: boolean\n  /** 24-hour or 12-hour (with an AM/PM wheel) display. @default \"24\" */\n  hourFormat?: TimePickerHourFormat\n  /** Wheel label digit style. @default \"fa\" */\n  digits?: TimePickerDigits\n  className?: string\n}\n\nfunction TimePicker({\n  value: valueProp,\n  defaultValue = DEFAULT_VALUE,\n  onValueChange,\n  showSeconds = false,\n  showLabels = false,\n  hourFormat = \"24\",\n  digits = \"fa\",\n  className,\n}: TimePickerProps) {\n  const [uncontrolledValue, setUncontrolledValue] =\n    React.useState<TimePickerValue>(defaultValue)\n  const isControlled = valueProp !== undefined\n  const value = isControlled ? valueProp : uncontrolledValue\n\n  const setValue = React.useCallback(\n    (next: TimePickerValue) => {\n      if (!isControlled) setUncontrolledValue(next)\n      onValueChange?.(next)\n    },\n    [isControlled, onValueChange]\n  )\n\n  const hourOptions = React.useMemo<WheelPickerOption<number>[]>(() => {\n    if (hourFormat === \"12\") {\n      return Array.from({ length: 12 }, (_, index) => {\n        const hour12 = index + 1\n        return { label: label(hour12, digits), value: hour12 }\n      })\n    }\n    return Array.from({ length: 24 }, (_, hour) => ({\n      label: label(hour, digits),\n      value: hour,\n    }))\n  }, [hourFormat, digits])\n\n  const minuteOptions = React.useMemo<WheelPickerOption<number>[]>(\n    () =>\n      Array.from({ length: 60 }, (_, minute) => ({\n        label: label(minute, digits),\n        value: minute,\n      })),\n    [digits]\n  )\n\n  const secondOptions = minuteOptions\n\n  const { hour12, period } = to12Hour(value.hour)\n  const second = clamp(value.second ?? 0, 0, 59)\n\n  return (\n    <div className={cn(\"flex flex-col\", className)}>\n      {showLabels && (\n        <div\n          aria-hidden=\"true\"\n          dir=\"ltr\"\n          className=\"grid auto-cols-fr grid-flow-col px-1 pb-1 text-center text-xs text-muted-foreground\"\n        >\n          <span>h</span>\n          <span>m</span>\n          {showSeconds && <span>s</span>}\n          {hourFormat === \"12\" && <span>AM/PM</span>}\n        </div>\n      )}\n      <WheelPickerWrapper>\n        <WheelPicker\n          options={hourOptions}\n          value={hourFormat === \"12\" ? hour12 : value.hour}\n          onValueChange={(nextHour) => {\n            const hour =\n              hourFormat === \"12\" ? from12Hour(nextHour, period) : nextHour\n            setValue({ ...value, hour })\n          }}\n          infinite\n        />\n        <WheelPicker\n          options={minuteOptions}\n          value={value.minute}\n          onValueChange={(minute) => setValue({ ...value, minute })}\n          infinite\n        />\n        {showSeconds && (\n          <WheelPicker\n            options={secondOptions}\n            value={second}\n            onValueChange={(nextSecond) =>\n              setValue({ ...value, second: nextSecond })\n            }\n            infinite\n          />\n        )}\n        {hourFormat === \"12\" && (\n          <WheelPicker\n            options={periodOptions(digits)}\n            value={period}\n            onValueChange={(nextPeriod) =>\n              setValue({ ...value, hour: from12Hour(hour12, nextPeriod) })\n            }\n          />\n        )}\n      </WheelPickerWrapper>\n    </div>\n  )\n}\n\n/**\n * A responsive wrapper around TimePicker: a Popover trigger + content on\n * desktop, a Drawer trigger + panel on mobile. Follows the same\n * root/trigger/content composition as ResponsiveDialog and ResponsiveMenu.\n *\n * Note: Base UI's `render` prop only picks up children placed *inside* the\n * render target, not children passed to the wrapping trigger --\n * `<ResponsiveTimePickerTrigger render={<Button/>}>{icon}</ResponsiveTimePickerTrigger>`\n * silently renders an empty button. Put trigger content on the target\n * element itself instead: `<ResponsiveTimePickerTrigger render={<Button>{icon}</Button>} />`.\n */\ntype ResponsiveTimePickerContextValue = { isDesktop: boolean }\n\nconst ResponsiveTimePickerContext =\n  React.createContext<ResponsiveTimePickerContextValue | null>(null)\n\nfunction useResponsiveTimePickerContext() {\n  const context = React.useContext(ResponsiveTimePickerContext)\n  if (!context) {\n    throw new Error(\n      \"ResponsiveTimePicker components must be used within <ResponsiveTimePicker>\"\n    )\n  }\n  return context\n}\n\ntype ResponsiveTimePickerProps = {\n  children: React.ReactNode\n  open?: boolean\n  defaultOpen?: boolean\n  onOpenChange?: (open: boolean) => void\n}\n\nfunction ResponsiveTimePicker({\n  children,\n  open,\n  defaultOpen,\n  onOpenChange,\n}: ResponsiveTimePickerProps) {\n  const isDesktop = !useIsMobile()\n  const contextValue = React.useMemo(() => ({ isDesktop }), [isDesktop])\n\n  return (\n    <ResponsiveTimePickerContext.Provider value={contextValue}>\n      {isDesktop ? (\n        <Popover\n          open={open}\n          defaultOpen={defaultOpen}\n          onOpenChange={onOpenChange}\n        >\n          {children}\n        </Popover>\n      ) : (\n        <Drawer\n          open={open}\n          defaultOpen={defaultOpen}\n          onOpenChange={onOpenChange}\n        >\n          {children}\n        </Drawer>\n      )}\n    </ResponsiveTimePickerContext.Provider>\n  )\n}\n\nfunction ResponsiveTimePickerTrigger(\n  props: React.ComponentProps<typeof PopoverTrigger>\n) {\n  const { isDesktop } = useResponsiveTimePickerContext()\n  return isDesktop ? (\n    <PopoverTrigger {...props} />\n  ) : (\n    <DrawerTrigger {...(props as React.ComponentProps<typeof DrawerTrigger>)} />\n  )\n}\n\ntype ResponsiveTimePickerContentProps = TimePickerProps & {\n  align?: React.ComponentProps<typeof PopoverContent>[\"align\"]\n  side?: React.ComponentProps<typeof PopoverContent>[\"side\"]\n  sideOffset?: number\n  drawerTitle?: React.ReactNode\n  drawerConfirmLabel?: React.ReactNode\n  onDrawerConfirm?: () => void\n}\n\nfunction ResponsiveTimePickerContent({\n  align = \"center\",\n  side = \"bottom\",\n  sideOffset = 6,\n  drawerTitle = \"انتخاب زمان\",\n  drawerConfirmLabel,\n  onDrawerConfirm,\n  className,\n  ...timePickerProps\n}: ResponsiveTimePickerContentProps) {\n  const { isDesktop } = useResponsiveTimePickerContext()\n\n  if (isDesktop) {\n    return (\n      <PopoverContent\n        align={align}\n        side={side}\n        sideOffset={sideOffset}\n        className=\"w-auto p-3\"\n      >\n        {onDrawerConfirm && (\n          <div dir=\"ltr\" className=\"mb-1 flex w-full justify-end\">\n            <Button\n              variant=\"ghost\"\n              size=\"icon-xs\"\n              aria-label={\n                typeof drawerConfirmLabel === \"string\"\n                  ? drawerConfirmLabel\n                  : \"تایید زمان\"\n              }\n              onClick={onDrawerConfirm}\n            >\n              <CheckIcon />\n            </Button>\n          </div>\n        )}\n        <TimePicker className={className} {...timePickerProps} />\n      </PopoverContent>\n    )\n  }\n\n  return (\n    <DrawerPopup showBar>\n      <DrawerTitle className=\"sr-only\">{drawerTitle}</DrawerTitle>\n      <DrawerPanel\n        scrollable={false}\n        className={cn(\"flex items-center justify-center pb-8\")}\n      >\n        <TimePicker className={className} {...timePickerProps} />\n      </DrawerPanel>\n      {drawerConfirmLabel && (\n        <DrawerFooter>\n          <Button className=\"w-full\" onClick={onDrawerConfirm}>\n            {drawerConfirmLabel}\n          </Button>\n        </DrawerFooter>\n      )}\n    </DrawerPopup>\n  )\n}\n\nexport {\n  ResponsiveTimePicker,\n  ResponsiveTimePickerContent,\n  ResponsiveTimePickerTrigger,\n  TimePicker,\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/base/hooks/use-media-query.ts",
      "content": "\"use client\"\n\nimport { useCallback, useSyncExternalStore } from \"react\"\n\nconst BREAKPOINTS = {\n  \"2xl\": 1536,\n  \"3xl\": 1600,\n  \"4xl\": 2000,\n  lg: 1024,\n  md: 800,\n  sm: 640,\n  xl: 1280,\n} as const\n\ntype Breakpoint = keyof typeof BREAKPOINTS\ntype BreakpointQuery =\n  Breakpoint | `max-${Breakpoint}` | `${Breakpoint}:max-${Breakpoint}`\n\nfunction resolveMin(value: Breakpoint | number): string {\n  const px = typeof value === \"number\" ? value : BREAKPOINTS[value]\n  return `(min-width: ${px}px)`\n}\n\nfunction resolveMax(value: Breakpoint | number): string {\n  const px = typeof value === \"number\" ? value : BREAKPOINTS[value]\n  return `(max-width: ${px - 1}px)`\n}\n\nfunction parseQuery(\n  query: BreakpointQuery | MediaQueryInput | (string & {})\n): string {\n  if (typeof query !== \"string\") {\n    const parts: string[] = []\n    if (query.min != null) parts.push(resolveMin(query.min))\n    if (query.max != null) parts.push(resolveMax(query.max))\n    if (query.pointer === \"coarse\") parts.push(\"(pointer: coarse)\")\n    if (query.pointer === \"fine\") parts.push(\"(pointer: fine)\")\n    if (parts.length === 0) return \"(min-width: 0px)\"\n    return parts.join(\" and \")\n  }\n\n  if (query.startsWith(\"(\")) return query\n\n  const parts: string[] = []\n  for (const segment of query.split(\":\")) {\n    if (segment.startsWith(\"max-\")) {\n      const bp = segment.slice(4)\n      if (bp in BREAKPOINTS) parts.push(resolveMax(bp as Breakpoint))\n    } else if (segment in BREAKPOINTS) {\n      parts.push(resolveMin(segment as Breakpoint))\n    }\n  }\n\n  return parts.length > 0 ? parts.join(\" and \") : query\n}\n\nfunction getServerSnapshot(): boolean {\n  return false\n}\n\nexport type MediaQueryInput = {\n  min?: Breakpoint | number\n  max?: Breakpoint | number\n  /** Touch-like input (finger). Use \"fine\" for mouse/trackpad. */\n  pointer?: \"coarse\" | \"fine\"\n}\n\n/** Tracks a CSS media query with Tailwind-like breakpoint syntax. Returns `false` during SSR. */\nexport function useMediaQuery(\n  query: BreakpointQuery | MediaQueryInput | (string & {})\n): boolean {\n  const mediaQuery = parseQuery(query)\n\n  const subscribe = useCallback(\n    (callback: () => void) => {\n      if (typeof window === \"undefined\") return () => {}\n      const mql = window.matchMedia(mediaQuery)\n      mql.addEventListener(\"change\", callback)\n      return () => mql.removeEventListener(\"change\", callback)\n    },\n    [mediaQuery]\n  )\n\n  const getSnapshot = useCallback(() => {\n    if (typeof window === \"undefined\") return false\n    return window.matchMedia(mediaQuery).matches\n  }, [mediaQuery])\n\n  return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)\n}\n\nexport function useIsMobile(): boolean {\n  return useMediaQuery(\"max-md\")\n}\n",
      "type": "registry:hook"
    }
  ],
  "type": "registry:ui"
}