{"version":3,"file":"range-picker.cjs","sources":["../../../components/calendar/range-picker.tsx"],"sourcesContent":["'use client';\n\nimport { cx } from 'class-variance-authority';\nimport dayjs from 'dayjs';\nimport { useCallback, useEffect, useMemo, useState } from 'react';\nimport { DateRange, PropsBase } from 'react-day-picker';\nimport { CalendarIcon } from '~/icons';\nimport { Flex } from '../flex';\nimport { Input } from '../input';\nimport { InputProps } from '../input/input';\nimport { Popover } from '../popover';\nimport { PopoverContentProps } from '../popover/popover';\nimport { Calendar, type CalendarPropsExtended } from './calendar';\nimport styles from './calendar.module.css';\nimport { usePickerPopover } from './use-picker-popover';\n\n/*\n * Picker-specific calendar surface. `mode` is owned by the picker; the other\n * forced keys (`selected`/`onSelect`/`required`) aren't in `PropsBase` so\n * they're already unreachable.\n */\ntype RangePickerCalendarSlot = Omit<PropsBase, 'mode'> & CalendarPropsExtended;\n\ninterface RangePickerSlotProps {\n  startInput?: InputProps;\n  endInput?: InputProps;\n  calendar?: RangePickerCalendarSlot;\n  popover?: PopoverContentProps;\n}\n\ninterface RangePickerProps {\n  dateFormat?: string;\n  /**\n   * Props for each picker slot. When both this and the legacy\n   * `inputsProps`/`calendarProps`/`popoverProps` are set, `slotProps` wins.\n   */\n  slotProps?: RangePickerSlotProps;\n  /** @deprecated Use `slotProps.startInput` / `slotProps.endInput` instead. */\n  inputsProps?: { startDate?: InputProps; endDate?: InputProps };\n  /** @deprecated Use `slotProps.calendar` instead. */\n  calendarProps?: RangePickerCalendarSlot;\n  /** @deprecated Use `slotProps.popover` instead. */\n  popoverProps?: PopoverContentProps;\n  onSelect?: (date: DateRange) => void;\n  pickerGroupClassName?: string;\n  value?: DateRange;\n  defaultValue?: DateRange;\n  children?:\n    | React.ReactNode\n    | ((props: { startDate: string; endDate: string }) => React.ReactNode);\n  showCalendarIcon?: boolean;\n  footer?: React.ReactNode;\n  timeZone?: string;\n}\n\ntype RangeFields = keyof DateRange;\n\nexport function RangePicker({\n  dateFormat = 'DD MMM YYYY',\n  slotProps,\n  inputsProps: legacyInputsProps = {},\n  calendarProps: legacyCalendarProps,\n  popoverProps: legacyPopoverProps,\n  onSelect = () => undefined,\n  value,\n  /*\n   * No inline default — the state machine's \"first click sets `from`\" branch\n   * needs an empty range to fire.\n   */\n  defaultValue,\n  pickerGroupClassName,\n  children,\n  showCalendarIcon = true,\n  footer,\n  timeZone\n}: RangePickerProps) {\n  // Merge legacy props with slotProps; slotProps wins when both are set.\n  const startInputProps = {\n    ...legacyInputsProps.startDate,\n    ...slotProps?.startInput\n  };\n  const endInputProps = {\n    ...legacyInputsProps.endDate,\n    ...slotProps?.endInput\n  };\n  const calendarProps = { ...legacyCalendarProps, ...slotProps?.calendar };\n  const popoverProps = { ...legacyPopoverProps, ...slotProps?.popover };\n  /*\n   * Gate the popover whenever either input is disabled. Partial-disable\n   * leaks: the range state machine rewrites both `from` and `to` regardless\n   * of which input was clicked, and the trailing icon's click bubbles to\n   * `Popover.Trigger` even when the input is disabled. For \"fix one side,\n   * pick the other\", constrain the calendar via `calendarProps` instead.\n   */\n  const isDisabled = !!startInputProps.disabled || !!endInputProps.disabled;\n  /*\n   * Hook owns open/close, outside-click dismissal, and the year/month\n   * dropdown carve-out. Inputs stay `readOnly`, so we arm the listener on\n   * open (click-to-open path) instead of on input blur (typed-input path).\n   */\n  const popover = usePickerPopover({\n    onOutsideClick: () => popover.disengage()\n  });\n\n  // biome-ignore lint/correctness/useExhaustiveDependencies: engage/disengage are stable\n  useEffect(() => {\n    if (popover.isOpen) popover.engage();\n    else popover.disengage();\n  }, [popover.isOpen]);\n\n  const [currentRangeField, setCurrentRangeField] =\n    useState<RangeFields>('from');\n  const [internalValue, setInternalValue] = useState<DateRange | undefined>(\n    value ?? defaultValue\n  );\n  const [currentMonth, setCurrentMonth] = useState<Date | undefined>(\n    internalValue?.from\n  );\n\n  /*\n   * Sync visible month when controlled `value.from` changes externally\n   * (form reset, preset buttons, sync-from-URL). Sync runs whenever\n   * `value` is defined — including when `value.from` is cleared — so a\n   * parent reset (`setValue({ from: undefined })`) actually unpins the\n   * calendar. Uncontrolled mode (value === undefined) skips entirely.\n   */\n  const valueFromTime = value?.from?.getTime();\n  const isControlled = value !== undefined;\n  // biome-ignore lint/correctness/useExhaustiveDependencies: compare on timestamp, not Date identity\n  useEffect(() => {\n    if (isControlled) setCurrentMonth(value.from);\n  }, [valueFromTime, isControlled]);\n\n  // Empty-range fallback so downstream `.from`/`.to` reads don't need guards.\n  const selectedRange: DateRange = value ??\n    internalValue ?? { from: undefined };\n\n  const startDate = selectedRange.from\n    ? dayjs(selectedRange.from).format(dateFormat)\n    : '';\n  const endDate = selectedRange.to\n    ? dayjs(selectedRange.to).format(dateFormat)\n    : '';\n\n  /*\n   * Ensures two months are visible even when the current month is the last\n   * allowed month (endMonth). Skips when `currentMonth` is undefined —\n   * `dayjs(undefined)` returns \"now\" and would falsely match `endMonth` if\n   * endMonth happens to be the current month, forcing the calendar away\n   * from its own default.\n   */\n  const computedDefaultMonth = useMemo(() => {\n    if (!currentMonth || !calendarProps?.endMonth) return currentMonth;\n    const endMonth = dayjs(calendarProps.endMonth);\n    if (dayjs(currentMonth).isSame(endMonth, 'month')) {\n      return endMonth.subtract(1, 'month').toDate();\n    }\n    return currentMonth;\n  }, [currentMonth, calendarProps?.endMonth]);\n\n  const onTriggerClick = useCallback(\n    (e: React.MouseEvent<HTMLInputElement>) => {\n      const field = e.currentTarget.dataset.rangeField;\n      if (field === 'start') {\n        setCurrentRangeField('from');\n      } else {\n        setCurrentRangeField('to');\n      }\n      if (popover.isOpen) {\n        e.preventDefault();\n        e.stopPropagation();\n      }\n    },\n    [popover.isOpen]\n  );\n\n  /*\n   * State machine branches on `from`/`to`, not the focused input:\n   *   A.  !from           -> set `from`, advance to 'to'\n   *   B1. from, before    -> reset\n   *   B2. from, after     -> commit `to`, close\n   *   C.  from && to      -> restart\n   * `onSelect` fires on every step; consumers gate on `range.to` for completed\n   * ranges.\n   */\n  const handleSelect = (_: DateRange, selectedDay: Date) => {\n    const { from, to } = selectedRange;\n    let newRange: DateRange;\n    let newField: RangeFields = 'to';\n    let shouldClose = false;\n\n    if (!from) {\n      // A: empty -> set from, advance\n      newRange = { from: selectedDay };\n    } else if (!to) {\n      if (dayjs(selectedDay).isBefore(dayjs(from))) {\n        // B1: click before from -> reset\n        newRange = { from: selectedDay };\n      } else {\n        // B2: complete range -> close\n        newRange = { from, to: selectedDay };\n        newField = 'from';\n        shouldClose = true;\n      }\n    } else {\n      // C: both set -> restart\n      newRange = { from: selectedDay };\n    }\n\n    if (newField !== currentRangeField) setCurrentRangeField(newField);\n    // Only update internal state when uncontrolled — controlled consumers own `value`.\n    if (!isControlled) setInternalValue(newRange);\n    onSelect(newRange);\n    if (shouldClose) popover.disengage();\n  };\n\n  const defaultTrigger = (\n    <Flex\n      gap={5}\n      className={pickerGroupClassName}\n      data-slot='range-picker-trigger-group'\n    >\n      <Input\n        size='small'\n        placeholder='Select start date'\n        trailingIcon={showCalendarIcon ? <CalendarIcon /> : undefined}\n        className={styles.datePickerInput}\n        data-slot='range-picker-start-input'\n        {...startInputProps}\n        value={startDate}\n        readOnly\n        data-range-field='start'\n        data-active={popover.isOpen && currentRangeField === 'from'}\n        onClick={onTriggerClick}\n      />\n\n      <Input\n        size='small'\n        placeholder='Select end date'\n        trailingIcon={showCalendarIcon ? <CalendarIcon /> : undefined}\n        className={styles.datePickerInput}\n        data-slot='range-picker-end-input'\n        {...endInputProps}\n        value={endDate}\n        readOnly\n        data-range-field='end'\n        data-active={popover.isOpen && currentRangeField === 'to'}\n        onClick={onTriggerClick}\n      />\n    </Flex>\n  );\n\n  /*\n   * Always wrap the trigger in a `<div>` so the rendered outer element is\n   * never a `<button>`. This keeps `nativeButton={false}` correct regardless\n   * of what the consumer passes (string, host element, React component that\n   * happens to render a button, etc.) — avoiding Base UI's button-nesting\n   * warning.\n   */\n  const triggerContent =\n    typeof children === 'function'\n      ? children({ startDate, endDate })\n      : children || defaultTrigger;\n\n  return (\n    <Popover\n      open={isDisabled ? false : popover.isOpen}\n      onOpenChange={open => {\n        if (isDisabled) return;\n        popover.onOpenChange(open);\n      }}\n    >\n      <Popover.Trigger\n        nativeButton={false}\n        render={<div data-slot='range-picker-trigger'>{triggerContent}</div>}\n      />\n      <Popover.Content\n        ref={popover.contentRef}\n        data-slot='range-picker-positioner'\n        {...popoverProps}\n        className={cx(styles.calendarPopover, popoverProps?.className)}\n        side={popoverProps?.side ?? 'top'}\n      >\n        <div data-slot='range-picker-content'>\n          <Calendar\n            /*\n             * No `captionLayout` default — 'dropdown' renders Apsara Selects\n             * inside the popover whose unmount loops (\"Maximum update depth\").\n             * Consumers can opt in via `calendarProps.captionLayout`.\n             */\n            showOutsideDays={false}\n            numberOfMonths={2}\n            defaultMonth={selectedRange.from}\n            {...calendarProps}\n            /*\n             * Must stay after spread: `required` is the discriminator for\n             * RDP's prop union, and a widened value would break the narrowing.\n             */\n            required={true}\n            timeZone={timeZone}\n            onDropdownOpen={popover.markDropdownOpen}\n            mode='range'\n            month={computedDefaultMonth}\n            selected={selectedRange}\n            onSelect={handleSelect}\n            onMonthChange={setCurrentMonth}\n          />\n          {footer && (\n            <Flex\n              align='center'\n              justify='center'\n              className={styles.calendarFooter}\n              data-slot='range-picker-footer'\n            >\n              {footer}\n            </Flex>\n          )}\n        </div>\n      </Popover.Content>\n    </Popover>\n  );\n}\n\nRangePicker.displayName = 'RangePicker';\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAyDgB;AAQd;;;AAGG;AACH;;AAQA;;;;AAIA;;;;;;AAMA;;;;;;AAMG;AACH;AACA;;;;AAIG;;AAED;AACD;;;;;;;AAMD;;AAIA;AAGA;AAIA;;;;;;AAMG;;AAEH;;;AAGE;AAAkB;AACpB;;;AAIE;AAEF;;;AAGA;;;AAIA;;;;;;AAMG;AACH;AACE;AAA+C;;AAE/C;;;AAGA;;AAGF;;AAGI;;;;;;AAKA;;;;AAIF;AAIF;;;;;;;;AAQG;AACH;AACE;AACA;;;;;AAME;;;AAEA;;AAEE;;;;;;;;;;;AASF;;;;;AAKF;;;AAEA;;AACF;AAEA;AAoCA;;;;;;AAMG;AACH;;AAGI;;AAME;;AACA;;AAgBI;;;;AAIG;;AAJH;;;;AAIG;AACH;AAIA;;;AAGG;AACH;AAuBZ;AAEA;;"}