Vela UI Docsvenizia.ai

Calendar

A calendar for selecting a single date, a date range, or a date with time.

npx shadcn@latest add @velaui/calendar
npx shadcn@latest add @velaui/calendar

Examples

  • Default
  • Date range
  • Centered
  • Disabled dates
  • Range bounded
  • Static label
  • Month year picker

View code

Calendar · Default

Installation

pnpm dlx velaui add @velaui/calendar
pnpm dlx velaui add @velaui/calendar

Code

calendar-default.tsx
import * as React from "react";
import { Calendar } from "@/components/ui/calendar";

export default function Example() {
  const [selected, setSelected] = React.useState<Date | undefined>(
    new Date(2024, 5, 12),
  );

  return (
    <Calendar
      mode="single"
      selected={selected}
      onSelect={setSelected}
      defaultMonth={new Date(2024, 5)}
    />
  );
}

View code

Calendar · Date range

Installation

pnpm dlx velaui add @velaui/calendar
pnpm dlx velaui add @velaui/calendar

Code

calendar-date-range.tsx
import * as React from "react";
import { CalendarRange } from "@/components/ui/calendar";
import type { DateRange } from "react-day-picker";

export default function Example() {
  const [range, setRange] = React.useState<DateRange | undefined>({
    from: new Date(2024, 5, 20),
    to: new Date(2024, 6, 8),
  });

  return (
    <CalendarRange
      selected={range}
      onSelect={setRange}
      defaultMonth={new Date(2024, 5)}
    />
  );
}

View code

Calendar · Centered

Installation

pnpm dlx velaui add @velaui/calendar
pnpm dlx velaui add @velaui/calendar

Code

calendar-centered.tsx
import * as React from "react";
import { Calendar } from "@/components/ui/calendar";

export default function Example() {
  const [selected, setSelected] = React.useState<Date | undefined>(
    new Date(2024, 5, 12),
  );

  return (
    <Calendar
      mode="single"
      headerVariant="center"
      selected={selected}
      onSelect={setSelected}
      defaultMonth={new Date(2024, 5)}
    />
  );
}

View code

Calendar · Disabled dates

Installation

pnpm dlx velaui add @velaui/calendar
pnpm dlx velaui add @velaui/calendar

Code

calendar-disabled-dates.tsx
import * as React from "react";
import { Calendar } from "@/components/ui/calendar";

export default function Example() {
  const [selected, setSelected] = React.useState<Date | undefined>(
    new Date(2024, 5, 5),
  );

  return (
    <Calendar
      mode="single"
      selected={selected}
      onSelect={setSelected}
      defaultMonth={new Date(2024, 5)}
      disabled={{ dayOfWeek: [0, 6] }}
    />
  );
}

View code

Calendar · Range bounded

Installation

pnpm dlx velaui add @velaui/calendar
pnpm dlx velaui add @velaui/calendar

Code

calendar-range-bounded.tsx
import * as React from "react";
import { CalendarRange } from "@/components/ui/calendar";
import type { DateRange } from "react-day-picker";

export default function Example() {
  const [range, setRange] = React.useState<DateRange | undefined>({
    from: new Date(2024, 5, 20),
    to: new Date(2024, 8, 8),
  });

  // Open either month label: the left wheel greys out everything from the right
  // panel's month onward, and the right wheel greys out everything before the
  // left panel's — so the pair can never cross.
  return (
    <CalendarRange
      selected={range}
      onSelect={setRange}
      defaultMonth={new Date(2024, 5)}
      startMonth={new Date(2023, 0)}
      endMonth={new Date(2025, 11)}
    />
  );
}

View code

Calendar · Static label

Installation

pnpm dlx velaui add @velaui/calendar
pnpm dlx velaui add @velaui/calendar

Code

calendar-static-label.tsx
import * as React from "react";
import { Calendar } from "@/components/ui/calendar";

export default function Example() {
  const [selected, setSelected] = React.useState<Date | undefined>(
    new Date(2024, 5, 12),
  );

  return (
    <Calendar
      mode="single"
      monthLabelClickable={false}
      selected={selected}
      onSelect={setSelected}
      defaultMonth={new Date(2024, 5)}
    />
  );
}

View code

Calendar · Month year picker

Installation

pnpm dlx velaui add @velaui/calendar
pnpm dlx velaui add @velaui/calendar

Code

calendar-month-year-picker.tsx
import * as React from "react";
import { Calendar } from "@/components/ui/calendar";

export default function Example() {
  const [selected, setSelected] = React.useState<Date | undefined>(
    new Date(2024, 3, 12),
  );

  return (
    <Calendar
      mode="single"
      selected={selected}
      onSelect={setSelected}
      defaultMonth={new Date(2024, 3)}
      startMonth={new Date(2022, 0)}
      endMonth={new Date(2026, 11)}
    />
  );
}

Usage

import * as React from "react";
import { Calendar } from "@/components/ui/calendar";

export default function Example() {
  const [selected, setSelected] = React.useState<Date | undefined>(
    new Date(2024, 5, 12),
  );

  return (
    <Calendar
      mode="single"
      selected={selected}
      onSelect={setSelected}
      defaultMonth={new Date(2024, 5)}
    />
  );
}

Props

Calendar

PropTypeDefault
headerVariantCalendarHeaderVariant"default"
monthLabelClickablebooleantrue
onMonthLabelClick() => void—
pickerMaxMonthDate—
pickerMinMonthDate—
showOutsideDaysbooleantrue
weekStartsOn0 | 1 | 2 | 3 | 4 | 5 | 61

Takes these and every prop of CalendarProps.

CalendarCell

PropTypeDefault
dotbooleanfalse

Takes these and every prop of CalendarCellProps.

CalendarMonthYearPicker

Takes every prop of CalendarMonthYearPickerProps.

CalendarRange

PropTypeDefault
defaultMonthDate—
disabledMatcher | Matcher[]—
endMonthDate—
localePartial<import("/app/node_modules/.bun/react-day-picker@10.0.1+93dbe784b8baa420/node_modules/react-day-picker/dist/esm/index").DayPickerLocale>—
monthLabelClickableboolean—
onSelect(range: DateRange | undefined) => void—
selectedDateRange—
showOutsideDaysboolean—
startMonthDate—
weekStartsOn0 | 1 | 2 | 3 | 4 | 5 | 6—

Takes these and every prop of CalendarRangeProps.

CalendarPeriodHeader

PropTypeDefault
variant"center" | "default" | "first-in-range" | "last-in-range""default"

Takes these and every prop of CalendarPeriodHeaderProps.

CalendarPeriodIndicator

PropTypeDefault
clickablebooleantrue

Takes these and every prop of CalendarPeriodIndicatorProps.

CalendarPeriodNav

Takes every prop of CalendarPeriodNavProps.

CalendarPeriodNavButton

PropTypeDefault
asChildboolean—
size"sm" | "md" | "lg" | "xs" | "xl" | "icon-xs" | "icon-sm" | "icon-md" | "icon-lg" | "icon-xl"—
variant"outline" | "link" | "destructive" | "ghost" | "primary" | "secondary"—

Takes these and every prop of CalendarPeriodNavButtonProps.

Calendar

PropTypeDefault
headerVariantCalendarHeaderVariant"default"
monthLabelClickablebooleantrue
onMonthLabelClick() => void—
pickerMaxMonthDate—
pickerMinMonthDate—
showOutsideDaysbooleantrue
weekStartsOn0 | 1 | 2 | 3 | 4 | 5 | 61

Takes these and every prop of CalendarProps.

CalendarCell

PropTypeDefault
dotbooleanfalse

Takes these and every prop of CalendarCellProps.

CalendarMonthYearPicker

Takes every prop of CalendarMonthYearPickerProps.

CalendarRange

PropTypeDefault
defaultMonthDate—
disabledMatcher | Matcher[]—
endMonthDate—
localePartial<import("/app/node_modules/.bun/react-day-picker@10.0.1+93dbe784b8baa420/node_modules/react-day-picker/dist/esm/index").DayPickerLocale>—
monthLabelClickableboolean—
onSelect(range: DateRange | undefined) => void—
selectedDateRange—
showOutsideDaysboolean—
startMonthDate—
weekStartsOn0 | 1 | 2 | 3 | 4 | 5 | 6—

Takes these and every prop of CalendarRangeProps.

CalendarPeriodHeader

PropTypeDefault
variant"center" | "default" | "first-in-range" | "last-in-range""default"

Takes these and every prop of CalendarPeriodHeaderProps.

CalendarPeriodIndicator

PropTypeDefault
clickablebooleantrue

Takes these and every prop of CalendarPeriodIndicatorProps.

CalendarPeriodNav

Takes every prop of CalendarPeriodNavProps.

CalendarPeriodNavButton

PropTypeDefault
size"sm" | "md" | "lg" | "xs" | "xl" | "icon-xs" | "icon-sm" | "icon-md" | "icon-lg" | "icon-xl"—
variant"link" | "outline" | "destructive" | "ghost" | "primary" | "secondary"—

Takes these and every prop of CalendarPeriodNavButtonProps.

Dependencies

npm
class-variance-authority@0.7.1motion@13.1.0radix-ui@1.6.0react-day-picker@10.0.1
Installs with it
Button3 icons
npm
@base-ui/react@1.6.0class-variance-authority@0.7.1motion@13.1.0react-day-picker@10.0.1
Installs with it
Button3 icons

Source

components/ui/calendar.tsx
/* eslint-disable react-refresh/only-export-components */
import * as React from "react";
import { animate } from "motion/react";
import { Popover as PopoverPrimitive } from "radix-ui";
import { cva, type VariantProps } from "class-variance-authority";
import { ChevronDown } from "@/components/icons/chevron-down";
import { ChevronLeft } from "@/components/icons/chevron-left";
import { ChevronRight } from "@/components/icons/chevron-right";
import {
  DayPicker,
  getDefaultClassNames,
  useDayPicker,
  type DateRange,
  type Locale,
  type Matcher,
} from "react-day-picker";

import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";

type CalendarHeaderVariant =
  "default" | "center" | "first-in-range" | "last-in-range";

/* -------------------------------------------------------------------------- */
/*                                Calendar cell                               */
/* -------------------------------------------------------------------------- */

// One class string, no cva axes: every axis Figma draws on this part
// (Type = Default/Today/In range/Selected/Active/Another,
//  State = Default/Hover/Disabled) is a rendering state, so all of it lives in
// data-*/pseudo selectors. The focus border is reserved as transparent on the
// base so entering focus never reflows the 40px box.
const calendarCellClassName = cn(
  "relative isolate z-10 flex size-10 shrink-0 cursor-pointer items-center justify-center rounded-full border border-transparent text-sm leading-[1.4] font-normal text-text-strong transition-colors outline-none select-none",
  "hover:bg-bg-surface-1",
  "focus-visible:border-ring-neutral-outline focus-visible:ring-3 focus-visible:ring-ring-neutral-shadow",
  // Today and the keyboard-active day rest on a subtle disc and step up on hover.
  "data-[today=true]:bg-bg-surface-1 data-[today=true]:hover:bg-bg-surface-2",
  "data-[active=true]:bg-bg-surface-1 data-[active=true]:hover:bg-bg-surface-2",
  // Days from the neighbouring month.
  "data-[outside=true]:text-text-note",
  // Range endpoints flatten the edge that faces the band, so the disc and the
  // middle run together instead of leaving a notch at the join. Each side is
  // guarded on the opposite endpoint: a one-day range is both start and end,
  // and flattening both edges would square off a cell that should stay a disc.
  "data-[range-start=true]:not-data-[range-end=true]:rounded-r-none",
  "data-[range-end=true]:not-data-[range-start=true]:rounded-l-none",
  // Inside a range: a square full-bleed band rather than a disc.
  "data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-bg-surface-1 data-[range-middle=true]:text-text-strong data-[range-middle=true]:hover:bg-bg-surface-2",
  // Selected day / range endpoints — the filled disc, which does not shift on hover.
  "data-[selected=true]:bg-primary-default data-[selected=true]:text-text-inverse data-[selected=true]:hover:bg-primary-default",
  // Disabled reads differently depending on what the cell is.
  "disabled:pointer-events-none disabled:cursor-not-allowed disabled:text-text-disabled",
  "disabled:data-[today=true]:text-text-note",
  "disabled:data-[range-middle=true]:text-text-note",
  "disabled:data-[selected=true]:bg-bg-surface-2 disabled:data-[selected=true]:text-text-note",
);

export type CalendarCellProps = React.ComponentProps<"button"> & { dot?: boolean };

function CalendarCell({
  className,
  children,
  dot = false,
  ...props
}: CalendarCellProps) {
  return (
    <button
      type="button"
      data-slot="calendar-cell"
      className={cn(calendarCellClassName, className)}
      {...props}
    >
      {children}
      {dot ? (
        <span
          data-slot="calendar-cell-dot"
          className="absolute bottom-1 left-1/2 size-[5px] -translate-x-1/2 rounded-full bg-current"
        />
      ) : null}
    </button>
  );
}

/* -------------------------------------------------------------------------- */
/*                            Period header + parts                           */
/* -------------------------------------------------------------------------- */

const calendarPeriodHeaderVariants = cva(
  "flex h-10 w-full items-center gap-3",
  {
    variants: {
      variant: {
        // Label sits left, both nav buttons group to the right.
        default: "justify-between",
        // Nav buttons flank a centred label.
        center: "justify-between",
        // Leading month of a range — previous only.
        "first-in-range": "justify-start",
        // Trailing month of a range — next only.
        "last-in-range": "justify-between",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  },
);

export type CalendarPeriodHeaderProps = React.ComponentProps<"div"> &
  VariantProps<typeof calendarPeriodHeaderVariants>;

function CalendarPeriodHeader({
  className,
  variant = "default",
  ...props
}: CalendarPeriodHeaderProps) {
  return (
    <div
      data-slot="calendar-period-header"
      data-variant={variant}
      className={cn(calendarPeriodHeaderVariants({ variant }), className)}
      {...props}
    />
  );
}

// The label's type scale is fixed: Figma gives the indicator an isClickable ×
// State matrix and no size axis at all, so there is one set of measurements.
const calendarPeriodIndicatorVariants = cva(
  "inline-flex h-[34px] shrink-0 items-center gap-1 rounded-xl border border-transparent px-2 text-xl leading-[1.3] font-semibold text-text-strong transition-colors outline-none select-none [&_svg:not([class*='size-'])]:size-5",
  {
    variants: {
      // Only the clickable indicator carries a surface — the static label has
      // no state at all. Its chevron points down at rest and flips up while the
      // month & year picker is open, which is the design's only affordance for
      // "this panel is showing something other than the day grid".
      clickable: {
        true: "cursor-pointer hover:bg-bg-surface-1 active:bg-bg-surface-1 aria-expanded:bg-bg-surface-1 focus-visible:border-ring-neutral-outline focus-visible:ring-3 focus-visible:ring-ring-neutral-shadow [&_svg]:transition-transform [&_svg]:duration-200 aria-expanded:[&_svg]:rotate-180",
        false: "",
      },
    },
    defaultVariants: {
      clickable: true,
    },
  },
);

export type CalendarPeriodIndicatorProps = Omit<React.ComponentProps<"button">, "type"> &
  VariantProps<typeof calendarPeriodIndicatorVariants>;

function CalendarPeriodIndicator({
  className,
  clickable = true,
  children,
  ...props
}: CalendarPeriodIndicatorProps) {
  const classes = cn(calendarPeriodIndicatorVariants({ clickable }), className);

  if (!clickable) {
    const { onClick: _onClick, disabled: _disabled, ...spanProps } = props;
    return (
      <span
        data-slot="calendar-period-indicator"
        className={classes}
        {...(spanProps as React.ComponentProps<"span">)}
      >
        {children}
      </span>
    );
  }

  return (
    <button
      type="button"
      data-slot="calendar-period-indicator"
      className={classes}
      {...props}
    >
      {children}
      <ChevronDown aria-hidden />
    </button>
  );
}

export type CalendarPeriodNavProps = React.ComponentProps<"div">;

function CalendarPeriodNav({
  className,
  ...props
}: CalendarPeriodNavProps) {
  return (
    <div
      data-slot="calendar-period-nav"
      className={cn("flex items-center gap-1", className)}
      {...props}
    />
  );
}

export type CalendarPeriodNavButtonProps = React.ComponentProps<typeof Button>;

function CalendarPeriodNavButton({
  className,
  ...props
}: CalendarPeriodNavButtonProps) {
  return (
    <Button
      variant="outline"
      size="icon-sm"
      data-slot="calendar-period-nav-button"
      className={cn("text-icon-strong", className)}
      {...props}
    />
  );
}

/* -------------------------------------------------------------------------- */
/*                                  Calendar                                  */
/* -------------------------------------------------------------------------- */

type CalendarConfig = {
  headerVariant: CalendarHeaderVariant;
  monthLabelClickable: boolean;
  onMonthLabelClick?: () => void;
  locale?: Partial<Locale>;
  pickerOpen: boolean;
  setPickerOpen: (open: boolean) => void;
  /** Earliest month the month & year picker may land on, inclusive. */
  pickerMinMonth?: Date;
  /** Latest month the month & year picker may land on, inclusive. */
  pickerMaxMonth?: Date;
};

const CalendarConfigContext = React.createContext<CalendarConfig>({
  headerVariant: "default",
  monthLabelClickable: true,
  pickerOpen: false,
  setPickerOpen: () => {},
});

/* -------------------------------------------------------------------------- */
/*                            Month & year picker                             */
/* -------------------------------------------------------------------------- */

/** Row height of a wheel — the same 40px module the day cell is built on. */
const CALENDAR_WHEEL_ROW = 40;
/** Five rows are on screen; the pair either side of centre fades back. */
const CALENDAR_WHEEL_PAD = CALENDAR_WHEEL_ROW * 2;
/**
 * Whether the browser says when a scroller has come to rest. Everything but
 * Safari < 18 does; without it the spring falls back to an idle timer.
 */
const CALENDAR_WHEEL_SCROLLEND =
  typeof window !== "undefined" && "onscrollend" in window;
/** Fallback idle window for browsers with no `scrollend`. */
const CALENDAR_WHEEL_SETTLE_MS = 180;
/** How far an unbounded year wheel reaches either side of the current year. */
const CALENDAR_WHEEL_YEARS_BACK = 100;
const CALENDAR_WHEEL_YEARS_FORWARD = 10;
/**
 * Carries the wheel onto the nearest row once the gesture is over. Damping is
 * kept just under critical for the stiffness (2·√k ≈ 53), so it arrives fast
 * without bouncing; `restDelta` ends it as soon as it is inside half a pixel,
 * which is a position the scroller cannot render anyway.
 */
const CALENDAR_WHEEL_SPRING = {
  type: "spring",
  stiffness: 700,
  damping: 50,
  restDelta: 0.5,
} as const;
type CalendarWheelItem = { value: number; label: string; disabled?: boolean };

/** Months as a single ordinal, so a bound check is one integer comparison. */
function monthOrdinal(year: number, month: number) {
  return year * 12 + month;
}

/**
 * The design fades a wheel row by its distance from centre — full at centre,
 * a quarter one row out, a tenth two rows out — so the falloff is a lookup on
 * that distance rather than a selected/unselected pair.
 */
function calendarWheelOpacity(distance: number) {
  if (distance === 0) return "opacity-100";
  if (distance === 1) return "opacity-25";
  return "opacity-10";
}

/**
 * The reachable span of a wheel. Bounds always disable a contiguous block at
 * one or both ends, so the span is just the first and last enabled row.
 */
function calendarWheelBounds(items: CalendarWheelItem[]) {
  let first = -1;
  let last = -1;
  for (let i = 0; i < items.length; i += 1) {
    if (items[i].disabled) continue;
    if (first === -1) first = i;
    last = i;
  }
  // Nothing reachable is not a state the picker can produce — the current month
  // is always in bounds — but a caller-supplied list could. Fall back to the
  // whole range rather than pinning the wheel to row 0.
  return first === -1 ? { first: 0, last: items.length - 1 } : { first, last };
}

/**
 * One wheel column. The row under the centre line is the value, and the
 * scrolling itself is the browser's — there is no CSS snapping, which used to
 * pin the wheel outright, and nothing writes `scrollTop` while a gesture is
 * live. A spring carries the wheel onto the nearest row afterwards.
 */
function CalendarWheelColumn({
  items,
  value,
  onValueChange,
  align,
  className,
  ...props
}: Omit<React.ComponentProps<"div">, "onChange"> & {
  items: CalendarWheelItem[];
  value: number;
  onValueChange: (value: number) => void;
  align: "start" | "end";
}) {
  const ref = React.useRef<HTMLDivElement>(null);
  const spring = React.useRef<ReturnType<typeof animate> | null>(null);
  const settle = React.useRef<ReturnType<typeof setTimeout> | null>(null);
  const index = Math.max(
    0,
    items.findIndex((item) => item.value === value),
  );

  const { first, last } = calendarWheelBounds(items);

  // The opening position is assigned outright — the popover mounts with the
  // wheel already on the current row, so there is nothing to animate from.
  React.useLayoutEffect(() => {
    const node = ref.current;
    if (node) node.scrollTop = index * CALENDAR_WHEEL_ROW;
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  /** Ease onto a row, committing it when the wheel gets there. */
  const springTo = (target: number) => {
    const node = ref.current;
    if (!node) return;
    spring.current?.stop();
    spring.current = animate(node.scrollTop, target * CALENDAR_WHEEL_ROW, {
      ...CALENDAR_WHEEL_SPRING,
      onUpdate: (top) => {
        node.scrollTop = top;
      },
      onComplete: () => {
        spring.current = null;
        const item = items[target];
        if (item && !item.disabled && item.value !== value)
          onValueChange(item.value);
      },
    });
  };

  /**
   * Give the scroller back the moment the user touches it. A spring writes
   * `scrollTop` every frame and each write cancels whatever scroll the browser
   * was running, so a spring left going during a gesture eats it frame for
   * frame. Input events are the only reliable signal — the scroll event cannot
   * tell us, because the spring's write is the last one in the frame and the
   * position it reports is therefore ours.
   */
  React.useEffect(() => {
    const node = ref.current;
    if (!node) return;
    const release = () => {
      spring.current?.stop();
      spring.current = null;
    };
    // Passive keeps the scroller on the compositor; capture means a row cannot
    // swallow the event on the way down.
    const opts = { passive: true, capture: true } as const;
    node.addEventListener("wheel", release, opts);
    node.addEventListener("touchstart", release, opts);
    node.addEventListener("keydown", release, opts);
    return () => {
      node.removeEventListener("wheel", release, opts);
      node.removeEventListener("touchstart", release, opts);
      node.removeEventListener("keydown", release, opts);
      if (settle.current !== null) clearTimeout(settle.current);
      spring.current?.stop();
    };
  }, []);

  /** Whatever row is under the centre line is the value. */
  const handleScroll = () => {
    const node = ref.current;
    if (!node) return;
    const landed = Math.round(node.scrollTop / CALENDAR_WHEEL_ROW);
    // A disabled row is passed over, never committed — the value holds at the
    // last reachable row the gesture crossed.
    const item = items[Math.min(last, Math.max(first, landed))];
    if (item && !item.disabled && item.value !== value)
      onValueChange(item.value);

    if (CALENDAR_WHEEL_SCROLLEND) return;
    if (settle.current !== null) clearTimeout(settle.current);
    settle.current = setTimeout(handleSettle, CALENDAR_WHEEL_SETTLE_MS);
  };

  /**
   * Spring onto the nearest row — but only once the scroller has genuinely
   * stopped. `scrollend` fires after momentum has run out, so the spring can
   * never interrupt a gesture in flight; an idle timer, which is what this used
   * to be, fires while the wheel is still coasting and kills it dead.
   */
  const handleSettle = () => {
    const node = ref.current;
    if (!node || spring.current) return;
    const landed = Math.round(node.scrollTop / CALENDAR_WHEEL_ROW);
    const target = Math.min(last, Math.max(first, landed));
    if (Math.abs(node.scrollTop - target * CALENDAR_WHEEL_ROW) < 1) return;
    springTo(target);
  };

  return (
    <div
      ref={ref}
      data-slot="calendar-wheel-column"
      onScroll={handleScroll}
      onScrollEnd={handleSettle}
      className={cn(
        // No CSS snapping: `mandatory` constrains every scroll offset to a snap
        // point, so a delta too small to leave the current row's basin is undone
        // as fast as it arrives and the wheel refuses to move. The spring below
        // does the settling instead, after the gesture rather than during it.
        "h-[200px] overflow-y-auto overscroll-contain [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
        className,
      )}
      {...props}
    >
      <div style={{ paddingBlock: CALENDAR_WHEEL_PAD }}>
        {items.map((item, i) => {
          return (
            <button
              key={item.value}
              type="button"
              data-slot="calendar-wheel-item"
              data-selected={i === index || undefined}
              // `aria-disabled`, not `disabled`: Chrome does not dispatch mouse
              // events over a disabled form control, so a wheel row under the
              // cursor would swallow the gesture until the pointer moved off it.
              aria-disabled={item.disabled || undefined}
              aria-current={i === index ? "true" : undefined}
              onClick={() => {
                if (item.disabled) return;
                springTo(i);
              }}
              className={cn(
                // Same type scale as the period indicator the wheel replaces, so
                // the label a user just clicked and the row they land on read as
                // the same text.
                "flex h-10 w-full items-center text-xl leading-[1.3] font-semibold text-text-strong transition-opacity outline-none select-none",
                align === "start"
                  ? "justify-start text-left"
                  : "justify-end text-right",
                calendarWheelOpacity(Math.abs(i - index)),
                // A row out of bounds can never be the centred one, so it is
                // always already dimmed by distance — the grey only has to
                // separate "far away" from "not available".
                item.disabled && "cursor-not-allowed text-text-disabled",
              )}
            >
              {item.label}
            </button>
          );
        })}
      </div>
    </div>
  );
}

export type CalendarMonthYearPickerProps = React.ComponentProps<"div">;

/**
 * The wheels stage a month and a year; nothing moves until the footer button
 * commits them. Scrolling deliberately does NOT navigate — the design pairs the
 * wheels with a Done button, and a calendar that jumped under the cursor on
 * every scroll tick would make that button meaningless. Committing navigates
 * the calendar; it never touches the selection.
 */
function CalendarMonthYearPicker({
  className,
  ...props
}: CalendarMonthYearPickerProps) {
  const { locale, setPickerOpen, pickerMinMonth, pickerMaxMonth } =
    React.useContext(CalendarConfigContext);
  const { months, goToMonth, dayPickerProps } = useDayPicker();
  const current = months[0]?.date ?? new Date();

  // The picker is mounted fresh each time the popover opens, so seeding the
  // draft from the displayed month here is enough — no effect to resync it.
  const [draftMonth, setDraftMonth] = React.useState(() => current.getMonth());
  const [draftYear, setDraftYear] = React.useState(() => current.getFullYear());

  // The panel this picker belongs to may only reach part of the calendar — in a
  // range, the "from" month cannot pass "to" and vice versa. Both ends collapse
  // to an ordinal so every row check below is one comparison.
  const minOrdinal = pickerMinMonth
    ? monthOrdinal(pickerMinMonth.getFullYear(), pickerMinMonth.getMonth())
    : Number.NEGATIVE_INFINITY;
  const maxOrdinal = pickerMaxMonth
    ? monthOrdinal(pickerMaxMonth.getFullYear(), pickerMaxMonth.getMonth())
    : Number.POSITIVE_INFINITY;

  // Which months are reachable depends on the year the other wheel is parked
  // on, so this list is rebuilt whenever the draft year moves.
  const monthItems = React.useMemo<CalendarWheelItem[]>(
    () =>
      Array.from({ length: 12 }, (_, month) => {
        const ordinal = monthOrdinal(draftYear, month);
        return {
          value: month,
          label: new Date(2000, month, 1).toLocaleString(locale?.code, {
            month: "long",
          }),
          disabled: ordinal < minOrdinal || ordinal > maxOrdinal,
        };
      }),
    [locale?.code, draftYear, minOrdinal, maxOrdinal],
  );

  // Changing the year can strand the month wheel out of bounds — December 2024
  // is fine until the year drops to 2023 and the bound sits at June. Pull it
  // back to the nearest reachable month rather than leaving an invalid draft.
  React.useEffect(() => {
    const ordinal = monthOrdinal(draftYear, draftMonth);
    if (ordinal >= minOrdinal && ordinal <= maxOrdinal) return;
    const clamped = Math.min(Math.max(ordinal, minOrdinal), maxOrdinal);
    const month = clamped - draftYear * 12;
    if (month >= 0 && month <= 11) setDraftMonth(month);
  }, [draftYear, draftMonth, minOrdinal, maxOrdinal]);

  // Both anchors are frozen on open: deriving them from `current` would slide
  // the whole year list out from under the wheel on every pick.
  const [anchorYear] = React.useState(() => current.getFullYear());
  const [thisYear] = React.useState(() => new Date().getFullYear());

  const yearItems = React.useMemo<CalendarWheelItem[]>(() => {
    // An unbounded calendar offers a century back and a decade forward — far
    // enough for a date of birth, which is the case that needs the reach. The
    // displayed year widens the window if it somehow falls outside, so the
    // month on screen is always one the wheel can show.
    const from =
      dayPickerProps.startMonth?.getFullYear() ??
      Math.min(thisYear - CALENDAR_WHEEL_YEARS_BACK, anchorYear);
    const to =
      dayPickerProps.endMonth?.getFullYear() ??
      Math.max(thisYear + CALENDAR_WHEEL_YEARS_FORWARD, anchorYear);
    return Array.from({ length: Math.max(1, to - from + 1) }, (_, i) => {
      const year = from + i;
      return {
        value: year,
        label: String(year),
        // A year is out of reach only when EVERY month in it is — otherwise it
        // stays selectable and the month wheel does the narrowing.
        disabled:
          monthOrdinal(year, 11) < minOrdinal ||
          monthOrdinal(year, 0) > maxOrdinal,
      };
    });
  }, [
    dayPickerProps.startMonth,
    dayPickerProps.endMonth,
    anchorYear,
    thisYear,
    minOrdinal,
    maxOrdinal,
  ]);

  return (
    <div
      data-slot="calendar-month-year-picker"
      className={cn("flex w-[250px] flex-col", className)}
      {...props}
    >
      <div className="px-6 py-5">
        {/* "September" is roughly half again as wide as "2024", so the columns
            split the row 3:2 rather than evenly — an even split clips the long
            month names. */}
        <div className="flex items-center gap-1 px-2">
          <CalendarWheelColumn
            aria-label="Month"
            align="start"
            className="min-w-0 flex-[3]"
            items={monthItems}
            value={draftMonth}
            onValueChange={setDraftMonth}
          />
          <CalendarWheelColumn
            aria-label="Year"
            align="end"
            className="min-w-0 flex-[2]"
            items={yearItems}
            value={draftYear}
            onValueChange={setDraftYear}
          />
        </div>
      </div>
      <div
        data-slot="calendar-month-year-picker-footer"
        className="flex items-center gap-3 border-t border-border-default p-4"
      >
        <Button
          variant="secondary"
          className="w-full"
          onClick={() => {
            goToMonth(new Date(draftYear, draftMonth, 1));
            setPickerOpen(false);
          }}
        >
          Done
        </Button>
      </div>
    </div>
  );
}

function CalendarCaption({ month }: { month: Date }) {
  const {
    headerVariant,
    monthLabelClickable,
    onMonthLabelClick,
    locale,
    pickerOpen,
    setPickerOpen,
  } = React.useContext(CalendarConfigContext);
  const { goToMonth, previousMonth, nextMonth } = useDayPicker();

  const label = month.toLocaleString(locale?.code, {
    month: "long",
    year: "numeric",
  });

  const previous = (
    <CalendarPeriodNavButton
      aria-label="Previous month"
      disabled={!previousMonth}
      onClick={() => previousMonth && goToMonth(previousMonth)}
    >
      <ChevronLeft aria-hidden />
    </CalendarPeriodNavButton>
  );

  const next = (
    <CalendarPeriodNavButton
      aria-label="Next month"
      disabled={!nextMonth}
      onClick={() => nextMonth && goToMonth(nextMonth)}
    >
      <ChevronRight aria-hidden />
    </CalendarPeriodNavButton>
  );

  // The static label has nothing to open, so it stays a bare indicator — no
  // popover machinery around a span that cannot be clicked. Radix owns
  // `aria-expanded` on the trigger, which is what drives both the open surface
  // and the chevron flip in calendarPeriodIndicatorVariants.
  const indicator = monthLabelClickable ? (
    <PopoverPrimitive.Root
      open={pickerOpen}
      onOpenChange={(open) => {
        setPickerOpen(open);
        if (open) onMonthLabelClick?.();
      }}
    >
      <PopoverPrimitive.Trigger asChild>
        <CalendarPeriodIndicator clickable>{label}</CalendarPeriodIndicator>
      </PopoverPrimitive.Trigger>
      <PopoverPrimitive.Portal>
        <PopoverPrimitive.Content
          data-slot="calendar-period-popover"
          align="start"
          sideOffset={8}
          // Padding lives on the picker, not here — its footer is full-bleed
          // against the surface, so the panel has to clip rather than pad.
          className="z-50 origin-(--radix-popover-content-transform-origin) overflow-hidden rounded-xl border border-border-default bg-bg-main text-text-strong shadow-sm outline-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95"
        >
          <CalendarMonthYearPicker />
        </PopoverPrimitive.Content>
      </PopoverPrimitive.Portal>
    </PopoverPrimitive.Root>
  ) : (
    <CalendarPeriodIndicator clickable={false}>{label}</CalendarPeriodIndicator>
  );

  return (
    <CalendarPeriodHeader variant={headerVariant}>
      {headerVariant === "center" || headerVariant === "first-in-range"
        ? previous
        : null}
      {indicator}
      {headerVariant === "default" ? (
        <CalendarPeriodNav>
          {previous}
          {next}
        </CalendarPeriodNav>
      ) : null}
      {headerVariant === "center" || headerVariant === "last-in-range"
        ? next
        : null}
    </CalendarPeriodHeader>
  );
}

export type CalendarProps = React.ComponentProps<typeof DayPicker> & {
  headerVariant?: CalendarHeaderVariant;
  monthLabelClickable?: boolean;
  onMonthLabelClick?: () => void;
  pickerMinMonth?: Date;
  pickerMaxMonth?: Date;
};

function Calendar({
  className,
  classNames,
  showOutsideDays = true,
  weekStartsOn = 1,
  headerVariant = "default",
  monthLabelClickable = true,
  onMonthLabelClick,
  locale,
  formatters,
  components,
  pickerMinMonth,
  pickerMaxMonth,
  ...props
}: CalendarProps) {
  const defaultClassNames = getDefaultClassNames();
  const [pickerOpen, setPickerOpen] = React.useState(false);

  const config = React.useMemo<CalendarConfig>(
    () => ({
      headerVariant,
      monthLabelClickable,
      onMonthLabelClick,
      locale,
      pickerOpen,
      setPickerOpen,
      pickerMinMonth,
      pickerMaxMonth,
    }),
    [
      headerVariant,
      monthLabelClickable,
      onMonthLabelClick,
      locale,
      pickerOpen,
      pickerMinMonth,
      pickerMaxMonth,
    ],
  );

  return (
    <CalendarConfigContext value={config}>
      <DayPicker
        showOutsideDays={showOutsideDays}
        weekStartsOn={weekStartsOn}
        locale={locale}
        className={cn("group/calendar w-fit bg-bg-main px-6 py-5", className)}
        formatters={{
          formatWeekdayName: (date) =>
            date.toLocaleString(locale?.code, { weekday: "short" }).slice(0, 2),
          ...formatters,
        }}
        classNames={{
          root: cn("w-fit", defaultClassNames.root),
          months: cn("flex flex-col", defaultClassNames.months),
          month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
          // The header is rendered by MonthCaption — react-day-picker's own nav
          // row would duplicate it.
          nav: cn("hidden", defaultClassNames.nav),
          month_caption: cn("w-full", defaultClassNames.month_caption),
          month_grid: cn(
            "w-full border-collapse",
            defaultClassNames.month_grid,
          ),
          weekdays: cn("flex", defaultClassNames.weekdays),
          weekday: cn(
            "flex size-10 items-center justify-center text-sm leading-[1.4] font-normal text-text-note select-none",
            defaultClassNames.weekday,
          ),
          week: cn("flex w-full", defaultClassNames.week),
          day: cn("group/day relative p-0 text-center", defaultClassNames.day),
          // The range band is painted behind the endpoint disc — half a cell on
          // the inner side of each endpoint.
          range_start: cn(
            "relative isolate z-0 after:absolute after:inset-y-0 after:right-0 after:-z-10 after:w-5 after:bg-bg-surface-1",
            defaultClassNames.range_start,
          ),
          range_end: cn(
            "relative isolate z-0 after:absolute after:inset-y-0 after:left-0 after:-z-10 after:w-5 after:bg-bg-surface-1",
            defaultClassNames.range_end,
          ),
          range_middle: cn(
            "relative isolate z-0",
            defaultClassNames.range_middle,
          ),
          hidden: cn("invisible", defaultClassNames.hidden),
          ...classNames,
        }}
        components={{
          Root: ({ className, rootRef, ...rootProps }) => (
            <div
              data-slot="calendar"
              ref={rootRef}
              className={cn(className)}
              {...rootProps}
            />
          ),
          MonthCaption: ({ calendarMonth }) => (
            <CalendarCaption month={calendarMonth.date} />
          ),
          Chevron: ({ className, orientation, ...chevronProps }) => {
            if (orientation === "left") {
              return (
                <ChevronLeft
                  className={cn("size-4", className)}
                  {...chevronProps}
                />
              );
            }
            if (orientation === "right") {
              return (
                <ChevronRight
                  className={cn("size-4", className)}
                  {...chevronProps}
                />
              );
            }
            return (
              <ChevronDown
                className={cn("size-4", className)}
                {...chevronProps}
              />
            );
          },
          DayButton: ({ className, day, modifiers, ...dayProps }) => {
            const isEndpoint = !!modifiers.selected && !modifiers.range_middle;
            return (
              <CalendarCell
                data-day={day.date.toLocaleDateString(locale?.code)}
                data-selected={isEndpoint || undefined}
                data-range-start={modifiers.range_start || undefined}
                data-range-end={modifiers.range_end || undefined}
                data-range-middle={modifiers.range_middle || undefined}
                data-today={
                  (modifiers.today && !modifiers.selected) || undefined
                }
                data-active={modifiers.focused || undefined}
                data-outside={modifiers.outside || undefined}
                className={cn(className)}
                {...dayProps}
              />
            );
          },
          ...components,
        }}
        {...props}
      />
    </CalendarConfigContext>
  );
}

/* -------------------------------------------------------------------------- */
/*                                Calendar range                              */
/* -------------------------------------------------------------------------- */

export type CalendarRangeProps = Omit<
  React.ComponentProps<"div">,
  "onSelect"
> & {
  selected?: DateRange;
  onSelect?: (range: DateRange | undefined) => void;
  /** Month the left panel opens on; the right opens a month later. */
  defaultMonth?: Date;
  startMonth?: Date;
  endMonth?: Date;
  disabled?: Matcher | Matcher[];
  locale?: Partial<Locale>;
  showOutsideDays?: boolean;
  weekStartsOn?: React.ComponentProps<typeof Calendar>["weekStartsOn"];
  monthLabelClickable?: boolean;
};

/** First of the month — the two panels compare and store months, never days. */
function startOfMonth(date: Date) {
  return new Date(date.getFullYear(), date.getMonth(), 1);
}

function addMonths(date: Date, amount: number) {
  return new Date(date.getFullYear(), date.getMonth() + amount, 1);
}

/**
 * The design's dual-date layout: two panels split by a rule, the left carrying
 * only a previous arrow and the right only a next one.
 *
 * The panels hold their months INDEPENDENTLY — this is not one DayPicker
 * showing two months, because that forces them consecutive. Independence is
 * what makes `from <= to` violable, so each panel's month & year picker is
 * bounded by the other's month: the left can never pass the right, and the
 * right can never fall behind the left.
 */
function CalendarRange({
  className,
  selected,
  onSelect,
  defaultMonth,
  startMonth,
  endMonth,
  disabled,
  locale,
  showOutsideDays,
  weekStartsOn,
  monthLabelClickable,
  ...props
}: CalendarRangeProps) {
  const [fromMonth, setFromMonth] = React.useState(() =>
    startOfMonth(defaultMonth ?? selected?.from ?? new Date()),
  );
  const [toMonth, setToMonth] = React.useState(() =>
    addMonths(defaultMonth ?? selected?.from ?? new Date(), 1),
  );

  // The bound is `from <= to`, so both panels are allowed to rest on the SAME
  // month. The arrows can only widen the pair — the left has no next and the
  // right has no previous — so they cannot cross on their own. The pushes below
  // are the backstop for anything that sets a month directly.
  const handleFromMonth = (month: Date) => {
    const next = startOfMonth(month);
    setFromMonth(next);
    setToMonth((to) => (to < next ? next : to));
  };

  const handleToMonth = (month: Date) => {
    const next = startOfMonth(month);
    setToMonth(next);
    setFromMonth((from) => (from > next ? next : from));
  };

  const shared = {
    mode: "range",
    selected,
    onSelect,
    startMonth,
    endMonth,
    disabled,
    locale,
    showOutsideDays,
    weekStartsOn,
    monthLabelClickable,
  } as const;

  return (
    <div
      data-slot="calendar-range"
      className={cn("flex divide-x divide-border-default", className)}
      {...props}
    >
      <Calendar
        {...shared}
        headerVariant="first-in-range"
        month={fromMonth}
        onMonthChange={handleFromMonth}
        pickerMaxMonth={toMonth}
      />
      <Calendar
        {...shared}
        headerVariant="last-in-range"
        month={toMonth}
        onMonthChange={handleToMonth}
        pickerMinMonth={fromMonth}
      />
    </div>
  );
}

export {
  Calendar,
  CalendarCell,
  CalendarMonthYearPicker,
  CalendarRange,
  CalendarPeriodHeader,
  CalendarPeriodIndicator,
  CalendarPeriodNav,
  CalendarPeriodNavButton,
  calendarPeriodHeaderVariants,
  calendarPeriodIndicatorVariants,
};
components/ui/calendar.tsx
/* eslint-disable react-refresh/only-export-components */
import * as React from "react";
import { animate } from "motion/react";
import { Popover as PopoverPrimitive } from "@base-ui/react/popover";
import { cva, type VariantProps } from "class-variance-authority";
import { ChevronDown } from "@/components/icons/chevron-down";
import { ChevronLeft } from "@/components/icons/chevron-left";
import { ChevronRight } from "@/components/icons/chevron-right";
import {
  DayPicker,
  getDefaultClassNames,
  useDayPicker,
  type DateRange,
  type Locale,
  type Matcher,
} from "react-day-picker";

import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";

type CalendarHeaderVariant =
  "default" | "center" | "first-in-range" | "last-in-range";

/* -------------------------------------------------------------------------- */
/*                                Calendar cell                               */
/* -------------------------------------------------------------------------- */

// One class string, no cva axes: every axis Figma draws on this part
// (Type = Default/Today/In range/Selected/Active/Another,
//  State = Default/Hover/Disabled) is a rendering state, so all of it lives in
// data-*/pseudo selectors. The focus border is reserved as transparent on the
// base so entering focus never reflows the 40px box.
const calendarCellClassName = cn(
  "relative isolate z-10 flex size-10 shrink-0 cursor-pointer items-center justify-center rounded-full border border-transparent text-sm leading-[1.4] font-normal text-text-strong transition-colors outline-none select-none",
  "hover:bg-bg-surface-1",
  "focus-visible:border-ring-neutral-outline focus-visible:ring-3 focus-visible:ring-ring-neutral-shadow",
  // Today and the keyboard-active day rest on a subtle disc and step up on hover.
  "data-[today=true]:bg-bg-surface-1 data-[today=true]:hover:bg-bg-surface-2",
  "data-[active=true]:bg-bg-surface-1 data-[active=true]:hover:bg-bg-surface-2",
  // Days from the neighbouring month.
  "data-[outside=true]:text-text-note",
  // Range endpoints flatten the edge that faces the band, so the disc and the
  // middle run together instead of leaving a notch at the join. Each side is
  // guarded on the opposite endpoint: a one-day range is both start and end,
  // and flattening both edges would square off a cell that should stay a disc.
  "data-[range-start=true]:not-data-[range-end=true]:rounded-r-none",
  "data-[range-end=true]:not-data-[range-start=true]:rounded-l-none",
  // Inside a range: a square full-bleed band rather than a disc.
  "data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-bg-surface-1 data-[range-middle=true]:text-text-strong data-[range-middle=true]:hover:bg-bg-surface-2",
  // Selected day / range endpoints — the filled disc, which does not shift on hover.
  "data-[selected=true]:bg-primary-default data-[selected=true]:text-text-inverse data-[selected=true]:hover:bg-primary-default",
  // Disabled reads differently depending on what the cell is.
  "disabled:pointer-events-none disabled:cursor-not-allowed disabled:text-text-disabled",
  "disabled:data-[today=true]:text-text-note",
  "disabled:data-[range-middle=true]:text-text-note",
  "disabled:data-[selected=true]:bg-bg-surface-2 disabled:data-[selected=true]:text-text-note",
);

export type CalendarCellProps = React.ComponentProps<"button"> & { dot?: boolean };

function CalendarCell({
  className,
  children,
  dot = false,
  ...props
}: CalendarCellProps) {
  return (
    <button
      type="button"
      data-slot="calendar-cell"
      className={cn(calendarCellClassName, className)}
      {...props}
    >
      {children}
      {dot ? (
        <span
          data-slot="calendar-cell-dot"
          className="absolute bottom-1 left-1/2 size-[5px] -translate-x-1/2 rounded-full bg-current"
        />
      ) : null}
    </button>
  );
}

/* -------------------------------------------------------------------------- */
/*                            Period header + parts                           */
/* -------------------------------------------------------------------------- */

const calendarPeriodHeaderVariants = cva(
  "flex h-10 w-full items-center gap-3",
  {
    variants: {
      variant: {
        // Label sits left, both nav buttons group to the right.
        default: "justify-between",
        // Nav buttons flank a centred label.
        center: "justify-between",
        // Leading month of a range — previous only.
        "first-in-range": "justify-start",
        // Trailing month of a range — next only.
        "last-in-range": "justify-between",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  },
);

export type CalendarPeriodHeaderProps = React.ComponentProps<"div"> &
  VariantProps<typeof calendarPeriodHeaderVariants>;

function CalendarPeriodHeader({
  className,
  variant = "default",
  ...props
}: CalendarPeriodHeaderProps) {
  return (
    <div
      data-slot="calendar-period-header"
      data-variant={variant}
      className={cn(calendarPeriodHeaderVariants({ variant }), className)}
      {...props}
    />
  );
}

// The label's type scale is fixed: Figma gives the indicator an isClickable ×
// State matrix and no size axis at all, so there is one set of measurements.
const calendarPeriodIndicatorVariants = cva(
  "inline-flex h-[34px] shrink-0 items-center gap-1 rounded-xl border border-transparent px-2 text-xl leading-[1.3] font-semibold text-text-strong transition-colors outline-none select-none [&_svg:not([class*='size-'])]:size-5",
  {
    variants: {
      // Only the clickable indicator carries a surface — the static label has
      // no state at all. Its chevron points down at rest and flips up while the
      // month & year picker is open, which is the design's only affordance for
      // "this panel is showing something other than the day grid".
      clickable: {
        true: "cursor-pointer hover:bg-bg-surface-1 active:bg-bg-surface-1 aria-expanded:bg-bg-surface-1 focus-visible:border-ring-neutral-outline focus-visible:ring-3 focus-visible:ring-ring-neutral-shadow [&_svg]:transition-transform [&_svg]:duration-200 aria-expanded:[&_svg]:rotate-180",
        false: "",
      },
    },
    defaultVariants: {
      clickable: true,
    },
  },
);

export type CalendarPeriodIndicatorProps = Omit<React.ComponentProps<"button">, "type"> &
  VariantProps<typeof calendarPeriodIndicatorVariants>;

function CalendarPeriodIndicator({
  className,
  clickable = true,
  children,
  ...props
}: CalendarPeriodIndicatorProps) {
  const classes = cn(calendarPeriodIndicatorVariants({ clickable }), className);

  if (!clickable) {
    const { onClick: _onClick, disabled: _disabled, ...spanProps } = props;
    return (
      <span
        data-slot="calendar-period-indicator"
        className={classes}
        {...(spanProps as React.ComponentProps<"span">)}
      >
        {children}
      </span>
    );
  }

  return (
    <button
      type="button"
      data-slot="calendar-period-indicator"
      className={classes}
      {...props}
    >
      {children}
      <ChevronDown aria-hidden />
    </button>
  );
}

export type CalendarPeriodNavProps = React.ComponentProps<"div">;

function CalendarPeriodNav({
  className,
  ...props
}: CalendarPeriodNavProps) {
  return (
    <div
      data-slot="calendar-period-nav"
      className={cn("flex items-center gap-1", className)}
      {...props}
    />
  );
}

export type CalendarPeriodNavButtonProps = React.ComponentProps<typeof Button>;

function CalendarPeriodNavButton({
  className,
  ...props
}: CalendarPeriodNavButtonProps) {
  return (
    <Button
      variant="outline"
      size="icon-sm"
      data-slot="calendar-period-nav-button"
      className={cn("text-icon-strong", className)}
      {...props}
    />
  );
}

/* -------------------------------------------------------------------------- */
/*                                  Calendar                                  */
/* -------------------------------------------------------------------------- */

type CalendarConfig = {
  headerVariant: CalendarHeaderVariant;
  monthLabelClickable: boolean;
  onMonthLabelClick?: () => void;
  locale?: Partial<Locale>;
  pickerOpen: boolean;
  setPickerOpen: (open: boolean) => void;
  /** Earliest month the month & year picker may land on, inclusive. */
  pickerMinMonth?: Date;
  /** Latest month the month & year picker may land on, inclusive. */
  pickerMaxMonth?: Date;
};

const CalendarConfigContext = React.createContext<CalendarConfig>({
  headerVariant: "default",
  monthLabelClickable: true,
  pickerOpen: false,
  setPickerOpen: () => {},
});

/* -------------------------------------------------------------------------- */
/*                            Month & year picker                             */
/* -------------------------------------------------------------------------- */

/** Row height of a wheel — the same 40px module the day cell is built on. */
const CALENDAR_WHEEL_ROW = 40;
/** Five rows are on screen; the pair either side of centre fades back. */
const CALENDAR_WHEEL_PAD = CALENDAR_WHEEL_ROW * 2;
/**
 * Whether the browser says when a scroller has come to rest. Everything but
 * Safari < 18 does; without it the spring falls back to an idle timer.
 */
const CALENDAR_WHEEL_SCROLLEND =
  typeof window !== "undefined" && "onscrollend" in window;
/** Fallback idle window for browsers with no `scrollend`. */
const CALENDAR_WHEEL_SETTLE_MS = 180;
/** How far an unbounded year wheel reaches either side of the current year. */
const CALENDAR_WHEEL_YEARS_BACK = 100;
const CALENDAR_WHEEL_YEARS_FORWARD = 10;
/**
 * Carries the wheel onto the nearest row once the gesture is over. Damping is
 * kept just under critical for the stiffness (2·√k ≈ 53), so it arrives fast
 * without bouncing; `restDelta` ends it as soon as it is inside half a pixel,
 * which is a position the scroller cannot render anyway.
 */
const CALENDAR_WHEEL_SPRING = {
  type: "spring",
  stiffness: 700,
  damping: 50,
  restDelta: 0.5,
} as const;
type CalendarWheelItem = { value: number; label: string; disabled?: boolean };

/** Months as a single ordinal, so a bound check is one integer comparison. */
function monthOrdinal(year: number, month: number) {
  return year * 12 + month;
}

/**
 * The design fades a wheel row by its distance from centre — full at centre,
 * a quarter one row out, a tenth two rows out — so the falloff is a lookup on
 * that distance rather than a selected/unselected pair.
 */
function calendarWheelOpacity(distance: number) {
  if (distance === 0) return "opacity-100";
  if (distance === 1) return "opacity-25";
  return "opacity-10";
}

/**
 * The reachable span of a wheel. Bounds always disable a contiguous block at
 * one or both ends, so the span is just the first and last enabled row.
 */
function calendarWheelBounds(items: CalendarWheelItem[]) {
  let first = -1;
  let last = -1;
  for (let i = 0; i < items.length; i += 1) {
    if (items[i].disabled) continue;
    if (first === -1) first = i;
    last = i;
  }
  // Nothing reachable is not a state the picker can produce — the current month
  // is always in bounds — but a caller-supplied list could. Fall back to the
  // whole range rather than pinning the wheel to row 0.
  return first === -1 ? { first: 0, last: items.length - 1 } : { first, last };
}

/**
 * One wheel column. The row under the centre line is the value, and the
 * scrolling itself is the browser's — there is no CSS snapping, which used to
 * pin the wheel outright, and nothing writes `scrollTop` while a gesture is
 * live. A spring carries the wheel onto the nearest row afterwards.
 */
function CalendarWheelColumn({
  items,
  value,
  onValueChange,
  align,
  className,
  ...props
}: Omit<React.ComponentProps<"div">, "onChange"> & {
  items: CalendarWheelItem[];
  value: number;
  onValueChange: (value: number) => void;
  align: "start" | "end";
}) {
  const ref = React.useRef<HTMLDivElement>(null);
  const spring = React.useRef<ReturnType<typeof animate> | null>(null);
  const settle = React.useRef<ReturnType<typeof setTimeout> | null>(null);
  const index = Math.max(
    0,
    items.findIndex((item) => item.value === value),
  );

  const { first, last } = calendarWheelBounds(items);

  // The opening position is assigned outright — the popover mounts with the
  // wheel already on the current row, so there is nothing to animate from.
  React.useLayoutEffect(() => {
    const node = ref.current;
    if (node) node.scrollTop = index * CALENDAR_WHEEL_ROW;
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  /** Ease onto a row, committing it when the wheel gets there. */
  const springTo = (target: number) => {
    const node = ref.current;
    if (!node) return;
    spring.current?.stop();
    spring.current = animate(node.scrollTop, target * CALENDAR_WHEEL_ROW, {
      ...CALENDAR_WHEEL_SPRING,
      onUpdate: (top) => {
        node.scrollTop = top;
      },
      onComplete: () => {
        spring.current = null;
        const item = items[target];
        if (item && !item.disabled && item.value !== value)
          onValueChange(item.value);
      },
    });
  };

  /**
   * Give the scroller back the moment the user touches it. A spring writes
   * `scrollTop` every frame and each write cancels whatever scroll the browser
   * was running, so a spring left going during a gesture eats it frame for
   * frame. Input events are the only reliable signal — the scroll event cannot
   * tell us, because the spring's write is the last one in the frame and the
   * position it reports is therefore ours.
   */
  React.useEffect(() => {
    const node = ref.current;
    if (!node) return;
    const release = () => {
      spring.current?.stop();
      spring.current = null;
    };
    // Passive keeps the scroller on the compositor; capture means a row cannot
    // swallow the event on the way down.
    const opts = { passive: true, capture: true } as const;
    node.addEventListener("wheel", release, opts);
    node.addEventListener("touchstart", release, opts);
    node.addEventListener("keydown", release, opts);
    return () => {
      node.removeEventListener("wheel", release, opts);
      node.removeEventListener("touchstart", release, opts);
      node.removeEventListener("keydown", release, opts);
      if (settle.current !== null) clearTimeout(settle.current);
      spring.current?.stop();
    };
  }, []);

  /** Whatever row is under the centre line is the value. */
  const handleScroll = () => {
    const node = ref.current;
    if (!node) return;
    const landed = Math.round(node.scrollTop / CALENDAR_WHEEL_ROW);
    // A disabled row is passed over, never committed — the value holds at the
    // last reachable row the gesture crossed.
    const item = items[Math.min(last, Math.max(first, landed))];
    if (item && !item.disabled && item.value !== value)
      onValueChange(item.value);

    if (CALENDAR_WHEEL_SCROLLEND) return;
    if (settle.current !== null) clearTimeout(settle.current);
    settle.current = setTimeout(handleSettle, CALENDAR_WHEEL_SETTLE_MS);
  };

  /**
   * Spring onto the nearest row — but only once the scroller has genuinely
   * stopped. `scrollend` fires after momentum has run out, so the spring can
   * never interrupt a gesture in flight; an idle timer, which is what this used
   * to be, fires while the wheel is still coasting and kills it dead.
   */
  const handleSettle = () => {
    const node = ref.current;
    if (!node || spring.current) return;
    const landed = Math.round(node.scrollTop / CALENDAR_WHEEL_ROW);
    const target = Math.min(last, Math.max(first, landed));
    if (Math.abs(node.scrollTop - target * CALENDAR_WHEEL_ROW) < 1) return;
    springTo(target);
  };

  return (
    <div
      ref={ref}
      data-slot="calendar-wheel-column"
      onScroll={handleScroll}
      onScrollEnd={handleSettle}
      className={cn(
        // No CSS snapping: `mandatory` constrains every scroll offset to a snap
        // point, so a delta too small to leave the current row's basin is undone
        // as fast as it arrives and the wheel refuses to move. The spring below
        // does the settling instead, after the gesture rather than during it.
        "h-[200px] overflow-y-auto overscroll-contain [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
        className,
      )}
      {...props}
    >
      <div style={{ paddingBlock: CALENDAR_WHEEL_PAD }}>
        {items.map((item, i) => {
          return (
            <button
              key={item.value}
              type="button"
              data-slot="calendar-wheel-item"
              data-selected={i === index || undefined}
              // `aria-disabled`, not `disabled`: Chrome does not dispatch mouse
              // events over a disabled form control, so a wheel row under the
              // cursor would swallow the gesture until the pointer moved off it.
              aria-disabled={item.disabled || undefined}
              aria-current={i === index ? "true" : undefined}
              onClick={() => {
                if (item.disabled) return;
                springTo(i);
              }}
              className={cn(
                // Same type scale as the period indicator the wheel replaces, so
                // the label a user just clicked and the row they land on read as
                // the same text.
                "flex h-10 w-full items-center text-xl leading-[1.3] font-semibold text-text-strong transition-opacity outline-none select-none",
                align === "start"
                  ? "justify-start text-left"
                  : "justify-end text-right",
                calendarWheelOpacity(Math.abs(i - index)),
                // A row out of bounds can never be the centred one, so it is
                // always already dimmed by distance — the grey only has to
                // separate "far away" from "not available".
                item.disabled && "cursor-not-allowed text-text-disabled",
              )}
            >
              {item.label}
            </button>
          );
        })}
      </div>
    </div>
  );
}

export type CalendarMonthYearPickerProps = React.ComponentProps<"div">;

/**
 * The wheels stage a month and a year; nothing moves until the footer button
 * commits them. Scrolling deliberately does NOT navigate — the design pairs the
 * wheels with a Done button, and a calendar that jumped under the cursor on
 * every scroll tick would make that button meaningless. Committing navigates
 * the calendar; it never touches the selection.
 */
function CalendarMonthYearPicker({
  className,
  ...props
}: CalendarMonthYearPickerProps) {
  const { locale, setPickerOpen, pickerMinMonth, pickerMaxMonth } =
    React.useContext(CalendarConfigContext);
  const { months, goToMonth, dayPickerProps } = useDayPicker();
  const current = months[0]?.date ?? new Date();

  // The picker is mounted fresh each time the popover opens, so seeding the
  // draft from the displayed month here is enough — no effect to resync it.
  const [draftMonth, setDraftMonth] = React.useState(() => current.getMonth());
  const [draftYear, setDraftYear] = React.useState(() => current.getFullYear());

  // The panel this picker belongs to may only reach part of the calendar — in a
  // range, the "from" month cannot pass "to" and vice versa. Both ends collapse
  // to an ordinal so every row check below is one comparison.
  const minOrdinal = pickerMinMonth
    ? monthOrdinal(pickerMinMonth.getFullYear(), pickerMinMonth.getMonth())
    : Number.NEGATIVE_INFINITY;
  const maxOrdinal = pickerMaxMonth
    ? monthOrdinal(pickerMaxMonth.getFullYear(), pickerMaxMonth.getMonth())
    : Number.POSITIVE_INFINITY;

  // Which months are reachable depends on the year the other wheel is parked
  // on, so this list is rebuilt whenever the draft year moves.
  const monthItems = React.useMemo<CalendarWheelItem[]>(
    () =>
      Array.from({ length: 12 }, (_, month) => {
        const ordinal = monthOrdinal(draftYear, month);
        return {
          value: month,
          label: new Date(2000, month, 1).toLocaleString(locale?.code, {
            month: "long",
          }),
          disabled: ordinal < minOrdinal || ordinal > maxOrdinal,
        };
      }),
    [locale?.code, draftYear, minOrdinal, maxOrdinal],
  );

  // Changing the year can strand the month wheel out of bounds — December 2024
  // is fine until the year drops to 2023 and the bound sits at June. Pull it
  // back to the nearest reachable month rather than leaving an invalid draft.
  React.useEffect(() => {
    const ordinal = monthOrdinal(draftYear, draftMonth);
    if (ordinal >= minOrdinal && ordinal <= maxOrdinal) return;
    const clamped = Math.min(Math.max(ordinal, minOrdinal), maxOrdinal);
    const month = clamped - draftYear * 12;
    if (month >= 0 && month <= 11) setDraftMonth(month);
  }, [draftYear, draftMonth, minOrdinal, maxOrdinal]);

  // Both anchors are frozen on open: deriving them from `current` would slide
  // the whole year list out from under the wheel on every pick.
  const [anchorYear] = React.useState(() => current.getFullYear());
  const [thisYear] = React.useState(() => new Date().getFullYear());

  const yearItems = React.useMemo<CalendarWheelItem[]>(() => {
    // An unbounded calendar offers a century back and a decade forward — far
    // enough for a date of birth, which is the case that needs the reach. The
    // displayed year widens the window if it somehow falls outside, so the
    // month on screen is always one the wheel can show.
    const from =
      dayPickerProps.startMonth?.getFullYear() ??
      Math.min(thisYear - CALENDAR_WHEEL_YEARS_BACK, anchorYear);
    const to =
      dayPickerProps.endMonth?.getFullYear() ??
      Math.max(thisYear + CALENDAR_WHEEL_YEARS_FORWARD, anchorYear);
    return Array.from({ length: Math.max(1, to - from + 1) }, (_, i) => {
      const year = from + i;
      return {
        value: year,
        label: String(year),
        // A year is out of reach only when EVERY month in it is — otherwise it
        // stays selectable and the month wheel does the narrowing.
        disabled:
          monthOrdinal(year, 11) < minOrdinal ||
          monthOrdinal(year, 0) > maxOrdinal,
      };
    });
  }, [
    dayPickerProps.startMonth,
    dayPickerProps.endMonth,
    anchorYear,
    thisYear,
    minOrdinal,
    maxOrdinal,
  ]);

  return (
    <div
      data-slot="calendar-month-year-picker"
      className={cn("flex w-[250px] flex-col", className)}
      {...props}
    >
      <div className="px-6 py-5">
        {/* "September" is roughly half again as wide as "2024", so the columns
            split the row 3:2 rather than evenly — an even split clips the long
            month names. */}
        <div className="flex items-center gap-1 px-2">
          <CalendarWheelColumn
            aria-label="Month"
            align="start"
            className="min-w-0 flex-[3]"
            items={monthItems}
            value={draftMonth}
            onValueChange={setDraftMonth}
          />
          <CalendarWheelColumn
            aria-label="Year"
            align="end"
            className="min-w-0 flex-[2]"
            items={yearItems}
            value={draftYear}
            onValueChange={setDraftYear}
          />
        </div>
      </div>
      <div
        data-slot="calendar-month-year-picker-footer"
        className="flex items-center gap-3 border-t border-border-default p-4"
      >
        <Button
          variant="secondary"
          className="w-full"
          onClick={() => {
            goToMonth(new Date(draftYear, draftMonth, 1));
            setPickerOpen(false);
          }}
        >
          Done
        </Button>
      </div>
    </div>
  );
}

function CalendarCaption({ month }: { month: Date }) {
  const {
    headerVariant,
    monthLabelClickable,
    onMonthLabelClick,
    locale,
    pickerOpen,
    setPickerOpen,
  } = React.useContext(CalendarConfigContext);
  const { goToMonth, previousMonth, nextMonth } = useDayPicker();

  const label = month.toLocaleString(locale?.code, {
    month: "long",
    year: "numeric",
  });

  const previous = (
    <CalendarPeriodNavButton
      aria-label="Previous month"
      disabled={!previousMonth}
      onClick={() => previousMonth && goToMonth(previousMonth)}
    >
      <ChevronLeft aria-hidden />
    </CalendarPeriodNavButton>
  );

  const next = (
    <CalendarPeriodNavButton
      aria-label="Next month"
      disabled={!nextMonth}
      onClick={() => nextMonth && goToMonth(nextMonth)}
    >
      <ChevronRight aria-hidden />
    </CalendarPeriodNavButton>
  );

  // The static label has nothing to open, so it stays a bare indicator — no
  // popover machinery around a span that cannot be clicked. Base UI owns
  // `aria-expanded` on the trigger, which is what drives both the open surface
  // and the chevron flip in calendarPeriodIndicatorVariants.
  const indicator = monthLabelClickable ? (
    <PopoverPrimitive.Root
      open={pickerOpen}
      onOpenChange={(open) => {
        setPickerOpen(open);
        if (open) onMonthLabelClick?.();
      }}
    >
      <PopoverPrimitive.Trigger
        render={
          <CalendarPeriodIndicator clickable>{label}</CalendarPeriodIndicator>
        }
      />
      <PopoverPrimitive.Portal>
        <PopoverPrimitive.Positioner
          align="start"
          sideOffset={8}
          className="isolate z-50"
        >
          <PopoverPrimitive.Popup
            data-slot="calendar-period-popover"
            // Padding lives on the picker, not here — its footer is full-bleed
            // against the surface, so the panel has to clip rather than pad.
            className="z-50 origin-(--transform-origin) overflow-hidden rounded-xl border border-border-default bg-bg-main text-text-strong shadow-sm outline-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95"
          >
            <CalendarMonthYearPicker />
          </PopoverPrimitive.Popup>
        </PopoverPrimitive.Positioner>
      </PopoverPrimitive.Portal>
    </PopoverPrimitive.Root>
  ) : (
    <CalendarPeriodIndicator clickable={false}>{label}</CalendarPeriodIndicator>
  );

  return (
    <CalendarPeriodHeader variant={headerVariant}>
      {headerVariant === "center" || headerVariant === "first-in-range"
        ? previous
        : null}
      {indicator}
      {headerVariant === "default" ? (
        <CalendarPeriodNav>
          {previous}
          {next}
        </CalendarPeriodNav>
      ) : null}
      {headerVariant === "center" || headerVariant === "last-in-range"
        ? next
        : null}
    </CalendarPeriodHeader>
  );
}

export type CalendarProps = React.ComponentProps<typeof DayPicker> & {
  headerVariant?: CalendarHeaderVariant;
  monthLabelClickable?: boolean;
  onMonthLabelClick?: () => void;
  pickerMinMonth?: Date;
  pickerMaxMonth?: Date;
};

function Calendar({
  className,
  classNames,
  showOutsideDays = true,
  weekStartsOn = 1,
  headerVariant = "default",
  monthLabelClickable = true,
  onMonthLabelClick,
  locale,
  formatters,
  components,
  pickerMinMonth,
  pickerMaxMonth,
  ...props
}: CalendarProps) {
  const defaultClassNames = getDefaultClassNames();
  const [pickerOpen, setPickerOpen] = React.useState(false);

  const config = React.useMemo<CalendarConfig>(
    () => ({
      headerVariant,
      monthLabelClickable,
      onMonthLabelClick,
      locale,
      pickerOpen,
      setPickerOpen,
      pickerMinMonth,
      pickerMaxMonth,
    }),
    [
      headerVariant,
      monthLabelClickable,
      onMonthLabelClick,
      locale,
      pickerOpen,
      pickerMinMonth,
      pickerMaxMonth,
    ],
  );

  return (
    <CalendarConfigContext value={config}>
      <DayPicker
        showOutsideDays={showOutsideDays}
        weekStartsOn={weekStartsOn}
        locale={locale}
        className={cn("group/calendar w-fit bg-bg-main px-6 py-5", className)}
        formatters={{
          formatWeekdayName: (date) =>
            date.toLocaleString(locale?.code, { weekday: "short" }).slice(0, 2),
          ...formatters,
        }}
        classNames={{
          root: cn("w-fit", defaultClassNames.root),
          months: cn("flex flex-col", defaultClassNames.months),
          month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
          // The header is rendered by MonthCaption — react-day-picker's own nav
          // row would duplicate it.
          nav: cn("hidden", defaultClassNames.nav),
          month_caption: cn("w-full", defaultClassNames.month_caption),
          month_grid: cn(
            "w-full border-collapse",
            defaultClassNames.month_grid,
          ),
          weekdays: cn("flex", defaultClassNames.weekdays),
          weekday: cn(
            "flex size-10 items-center justify-center text-sm leading-[1.4] font-normal text-text-note select-none",
            defaultClassNames.weekday,
          ),
          week: cn("flex w-full", defaultClassNames.week),
          day: cn("group/day relative p-0 text-center", defaultClassNames.day),
          // The range band is painted behind the endpoint disc — half a cell on
          // the inner side of each endpoint.
          range_start: cn(
            "relative isolate z-0 after:absolute after:inset-y-0 after:right-0 after:-z-10 after:w-5 after:bg-bg-surface-1",
            defaultClassNames.range_start,
          ),
          range_end: cn(
            "relative isolate z-0 after:absolute after:inset-y-0 after:left-0 after:-z-10 after:w-5 after:bg-bg-surface-1",
            defaultClassNames.range_end,
          ),
          range_middle: cn(
            "relative isolate z-0",
            defaultClassNames.range_middle,
          ),
          hidden: cn("invisible", defaultClassNames.hidden),
          ...classNames,
        }}
        components={{
          Root: ({ className, rootRef, ...rootProps }) => (
            <div
              data-slot="calendar"
              ref={rootRef}
              className={cn(className)}
              {...rootProps}
            />
          ),
          MonthCaption: ({ calendarMonth }) => (
            <CalendarCaption month={calendarMonth.date} />
          ),
          Chevron: ({ className, orientation, ...chevronProps }) => {
            if (orientation === "left") {
              return (
                <ChevronLeft
                  className={cn("size-4", className)}
                  {...chevronProps}
                />
              );
            }
            if (orientation === "right") {
              return (
                <ChevronRight
                  className={cn("size-4", className)}
                  {...chevronProps}
                />
              );
            }
            return (
              <ChevronDown
                className={cn("size-4", className)}
                {...chevronProps}
              />
            );
          },
          DayButton: ({ className, day, modifiers, ...dayProps }) => {
            const isEndpoint = !!modifiers.selected && !modifiers.range_middle;
            return (
              <CalendarCell
                data-day={day.date.toLocaleDateString(locale?.code)}
                data-selected={isEndpoint || undefined}
                data-range-start={modifiers.range_start || undefined}
                data-range-end={modifiers.range_end || undefined}
                data-range-middle={modifiers.range_middle || undefined}
                data-today={
                  (modifiers.today && !modifiers.selected) || undefined
                }
                data-active={modifiers.focused || undefined}
                data-outside={modifiers.outside || undefined}
                className={cn(className)}
                {...dayProps}
              />
            );
          },
          ...components,
        }}
        {...props}
      />
    </CalendarConfigContext>
  );
}

/* -------------------------------------------------------------------------- */
/*                                Calendar range                              */
/* -------------------------------------------------------------------------- */

export type CalendarRangeProps = Omit<
  React.ComponentProps<"div">,
  "onSelect"
> & {
  selected?: DateRange;
  onSelect?: (range: DateRange | undefined) => void;
  /** Month the left panel opens on; the right opens a month later. */
  defaultMonth?: Date;
  startMonth?: Date;
  endMonth?: Date;
  disabled?: Matcher | Matcher[];
  locale?: Partial<Locale>;
  showOutsideDays?: boolean;
  weekStartsOn?: React.ComponentProps<typeof Calendar>["weekStartsOn"];
  monthLabelClickable?: boolean;
};

/** First of the month — the two panels compare and store months, never days. */
function startOfMonth(date: Date) {
  return new Date(date.getFullYear(), date.getMonth(), 1);
}

function addMonths(date: Date, amount: number) {
  return new Date(date.getFullYear(), date.getMonth() + amount, 1);
}

/**
 * The design's dual-date layout: two panels split by a rule, the left carrying
 * only a previous arrow and the right only a next one.
 *
 * The panels hold their months INDEPENDENTLY — this is not one DayPicker
 * showing two months, because that forces them consecutive. Independence is
 * what makes `from <= to` violable, so each panel's month & year picker is
 * bounded by the other's month: the left can never pass the right, and the
 * right can never fall behind the left.
 */
function CalendarRange({
  className,
  selected,
  onSelect,
  defaultMonth,
  startMonth,
  endMonth,
  disabled,
  locale,
  showOutsideDays,
  weekStartsOn,
  monthLabelClickable,
  ...props
}: CalendarRangeProps) {
  const [fromMonth, setFromMonth] = React.useState(() =>
    startOfMonth(defaultMonth ?? selected?.from ?? new Date()),
  );
  const [toMonth, setToMonth] = React.useState(() =>
    addMonths(defaultMonth ?? selected?.from ?? new Date(), 1),
  );

  // The bound is `from <= to`, so both panels are allowed to rest on the SAME
  // month. The arrows can only widen the pair — the left has no next and the
  // right has no previous — so they cannot cross on their own. The pushes below
  // are the backstop for anything that sets a month directly.
  const handleFromMonth = (month: Date) => {
    const next = startOfMonth(month);
    setFromMonth(next);
    setToMonth((to) => (to < next ? next : to));
  };

  const handleToMonth = (month: Date) => {
    const next = startOfMonth(month);
    setToMonth(next);
    setFromMonth((from) => (from > next ? next : from));
  };

  const shared = {
    mode: "range",
    selected,
    onSelect,
    startMonth,
    endMonth,
    disabled,
    locale,
    showOutsideDays,
    weekStartsOn,
    monthLabelClickable,
  } as const;

  return (
    <div
      data-slot="calendar-range"
      className={cn("flex divide-x divide-border-default", className)}
      {...props}
    >
      <Calendar
        {...shared}
        headerVariant="first-in-range"
        month={fromMonth}
        onMonthChange={handleFromMonth}
        pickerMaxMonth={toMonth}
      />
      <Calendar
        {...shared}
        headerVariant="last-in-range"
        month={toMonth}
        onMonthChange={handleToMonth}
        pickerMinMonth={fromMonth}
      />
    </div>
  );
}

export {
  Calendar,
  CalendarCell,
  CalendarMonthYearPicker,
  CalendarRange,
  CalendarPeriodHeader,
  CalendarPeriodIndicator,
  CalendarPeriodNav,
  CalendarPeriodNavButton,
  calendarPeriodHeaderVariants,
  calendarPeriodIndicatorVariants,
};