# Date picker

> A button that opens a calendar in a popover, or a bottom sheet on phones, for single dates and ranges.

Docs: https://hextaui.com/docs/date-picker
Markdown: https://hextaui.com/docs/date-picker.md

```tsx title="components/examples/date-picker/demo.tsx"
import { DatePicker } from "@/components/ui/date-picker"

export function DatePickerDemo() {
  return <DatePicker />
}
```

## Installation

### CLI

```bash
npx shadcn@latest add https://hextaui.com/r/date-picker.json
```

This adds the component, the HextaUI theme tokens and any HextaUI components it depends on.

### Manual

Add the HextaUI theme tokens (https://hextaui.com/docs/installation#theme) to your global CSS if you haven't yet, then install the dependencies.

```bash
pnpm add @base-ui/react @tabler/icons-react class-variance-authority cn react-day-picker
```

Copy and paste the following code into your project.

```tsx title="components/ui/date-picker.tsx"
"use client"

import * as React from "react"
import { IconCalendar } from "@tabler/icons-react"
import { cn } from "cn"
import type { DateRange } from "react-day-picker"

import { Button } from "@/components/ui/button"
import { Calendar, type CalendarProps } from "@/components/ui/calendar"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover"
import {
  Sheet,
  SheetBody,
  SheetContent,
  SheetFooter,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet"

const phoneQuery = "(max-width: 639px)"

function subscribePhone(onChange: () => void) {
  if (typeof window.matchMedia !== "function") {
    return () => {}
  }
  const media = window.matchMedia(phoneQuery)
  media.addEventListener("change", onChange)
  return () => media.removeEventListener("change", onChange)
}

function getPhone() {
  return typeof window.matchMedia === "function"
    ? window.matchMedia(phoneQuery).matches
    : false
}

function useIsPhone() {
  return React.useSyncExternalStore(subscribePhone, getPhone, () => false)
}

function toIsoDate(date: Date) {
  const month = String(date.getMonth() + 1).padStart(2, "0")
  const day = String(date.getDate()).padStart(2, "0")
  return `${date.getFullYear()}-${month}-${day}`
}

function normalizeSpaces(text: string) {
  return text.replace(/[\u2009\u202f\u00a0]/g, " ")
}

function isSameDay(a: Date, b: Date) {
  return (
    a.getFullYear() === b.getFullYear() &&
    a.getMonth() === b.getMonth() &&
    a.getDate() === b.getDate()
  )
}

type CalendarPassthrough = Omit<
  CalendarProps,
  "mode" | "selected" | "onSelect" | "required" | "numberOfMonths" | "autoFocus"
>

type TriggerProps = Omit<
  React.ComponentProps<typeof Button>,
  "value" | "defaultValue" | "onChange" | "children" | "render"
>

type DatePickerSharedProps = TriggerProps & {
  open?: boolean
  defaultOpen?: boolean
  onOpenChange?: (open: boolean) => void
  placeholder?: string
  title?: string
  locale?: string
  clearable?: boolean
  clearLabel?: string
  name?: string
  calendarProps?: CalendarPassthrough
}

type DatePickerShellProps = Omit<
  DatePickerSharedProps,
  "locale" | "calendarProps"
> & {
  label: string | undefined
  hasValue: boolean
  hiddenValue: string | undefined
  onClear: () => void
  children: (close: () => void, phone: boolean) => React.ReactNode
}

function DatePickerShell({
  open: openProp,
  defaultOpen = false,
  onOpenChange,
  placeholder,
  title,
  label,
  hasValue,
  hiddenValue,
  clearable = false,
  clearLabel = "Clear",
  name,
  onClear,
  children,
  className,
  variant = "outline",
  disabled,
  ...props
}: DatePickerShellProps) {
  const phone = useIsPhone()
  const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
  const open = openProp ?? uncontrolledOpen

  const setOpen = (next: boolean) => {
    if (openProp === undefined) {
      setUncontrolledOpen(next)
    }
    onOpenChange?.(next)
  }
  const close = () => setOpen(false)

  const trigger = (
    <Button
      variant={variant}
      disabled={disabled}
      data-slot="date-picker-trigger"
      data-empty={hasValue ? undefined : ""}
      className={cn(
        "w-60 max-w-full justify-start gap-2 px-3 text-start font-normal tabular-nums data-empty:text-muted-foreground",
        className
      )}
      {...props}
    />
  )

  const triggerContent = (
    <>
      <IconCalendar aria-hidden className="text-muted-foreground" />
      <span className="min-w-0 truncate">{label ?? placeholder}</span>
    </>
  )

  const clearButton =
    clearable && hasValue ? (
      <Button
        type="button"
        variant="ghost"
        size="sm"
        data-slot="date-picker-clear"
        onClick={() => {
          onClear()
          close()
        }}
      >
        {clearLabel}
      </Button>
    ) : null

  const hidden =
    name !== undefined ? (
      <input type="hidden" name={name} value={hiddenValue ?? ""} />
    ) : null

  if (phone) {
    return (
      <Sheet open={open} onOpenChange={setOpen}>
        <SheetTrigger render={trigger}>{triggerContent}</SheetTrigger>
        {hidden}
        <SheetContent side="bottom">
          <SheetHeader>
            <SheetTitle>{title}</SheetTitle>
          </SheetHeader>
          <SheetBody>
            <div
              data-slot="date-picker-content"
              className="flex justify-center"
            >
              {children(close, true)}
            </div>
          </SheetBody>
          {clearButton ? <SheetFooter>{clearButton}</SheetFooter> : null}
        </SheetContent>
      </Sheet>
    )
  }

  return (
    <Popover open={open} onOpenChange={setOpen}>
      <PopoverTrigger render={trigger}>{triggerContent}</PopoverTrigger>
      {hidden}
      <PopoverContent
        align="start"
        aria-label={title}
        className="w-auto gap-0 p-0"
      >
        <div data-slot="date-picker-content">{children(close, false)}</div>
        {clearButton ? (
          <div className="flex justify-end border-t px-2 py-1.5">
            {clearButton}
          </div>
        ) : null}
      </PopoverContent>
    </Popover>
  )
}

type DatePickerProps = DatePickerSharedProps & {
  value?: Date | null
  defaultValue?: Date | null
  onValueChange?: (value: Date | null) => void
}

function DatePicker({
  value: valueProp,
  defaultValue = null,
  onValueChange,
  placeholder = "Pick a date",
  title = "Select a date",
  locale = "en-US",
  calendarProps,
  ...props
}: DatePickerProps) {
  const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue)
  const value = valueProp !== undefined ? valueProp : uncontrolledValue
  const formatter = React.useMemo(
    () => new Intl.DateTimeFormat(locale, { dateStyle: "medium" }),
    [locale]
  )

  const setValue = (next: Date | null) => {
    if (valueProp === undefined) {
      setUncontrolledValue(next)
    }
    onValueChange?.(next)
  }

  return (
    <DatePickerShell
      {...props}
      placeholder={placeholder}
      title={title}
      label={value ? normalizeSpaces(formatter.format(value)) : undefined}
      hasValue={Boolean(value)}
      hiddenValue={value ? toIsoDate(value) : undefined}
      onClear={() => setValue(null)}
    >
      {(close) => (
        <Calendar
          {...calendarProps}
          mode="single"
          required
          autoFocus
          selected={value ?? undefined}
          defaultMonth={value ?? calendarProps?.defaultMonth}
          onSelect={(date) => {
            setValue(date)
            close()
          }}
        />
      )}
    </DatePickerShell>
  )
}

type DateRangePickerProps = DatePickerSharedProps & {
  value?: DateRange | null
  defaultValue?: DateRange | null
  onValueChange?: (value: DateRange | null) => void
}

function DateRangeCalendar({
  value,
  phone,
  calendarProps,
  onComplete,
}: {
  value: DateRange | null
  phone: boolean
  calendarProps?: CalendarPassthrough
  onComplete: (range: DateRange) => void
}) {
  const [draft, setDraft] = React.useState<DateRange | undefined>(undefined)
  const selected = draft ?? value ?? undefined

  return (
    <Calendar
      {...calendarProps}
      mode="range"
      autoFocus
      numberOfMonths={phone ? 1 : 2}
      defaultMonth={value?.from ?? calendarProps?.defaultMonth}
      selected={selected}
      onSelect={(_, day) => {
        if (!draft) {
          setDraft({ from: day, to: day })
          return
        }
        const start = draft.from ?? day
        const range =
          day < start ? { from: day, to: start } : { from: start, to: day }
        setDraft(undefined)
        onComplete(range)
      }}
    />
  )
}

function DateRangePicker({
  value: valueProp,
  defaultValue = null,
  onValueChange,
  placeholder = "Pick a date range",
  title = "Select dates",
  locale = "en-US",
  calendarProps,
  ...props
}: DateRangePickerProps) {
  const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue)
  const value = valueProp !== undefined ? valueProp : uncontrolledValue
  const formatter = React.useMemo(
    () => new Intl.DateTimeFormat(locale, { dateStyle: "medium" }),
    [locale]
  )

  const setValue = (next: DateRange | null) => {
    if (valueProp === undefined) {
      setUncontrolledValue(next)
    }
    onValueChange?.(next)
  }

  const from = value?.from
  const to = value?.to ?? value?.from
  const label =
    from && to
      ? isSameDay(from, to)
        ? normalizeSpaces(formatter.format(from))
        : normalizeSpaces(formatter.formatRange(from, to))
      : undefined

  return (
    <DatePickerShell
      {...props}
      placeholder={placeholder}
      title={title}
      label={label}
      hasValue={Boolean(from)}
      hiddenValue={
        from && to ? `${toIsoDate(from)}/${toIsoDate(to)}` : undefined
      }
      onClear={() => setValue(null)}
    >
      {(close, phone) => (
        <DateRangeCalendar
          value={value}
          phone={phone}
          calendarProps={calendarProps}
          onComplete={(range) => {
            setValue(range)
            close()
          }}
        />
      )}
    </DatePickerShell>
  )
}

export { DatePicker, DateRangePicker }
export type { DatePickerProps, DateRangePickerProps }
```

```tsx title="components/ui/calendar.tsx"
"use client"

import * as React from "react"
import { useDirection } from "@base-ui/react/direction-provider"
import {
  IconChevronDown,
  IconChevronLeft,
  IconChevronRight,
  IconChevronUp,
} from "@tabler/icons-react"
import { cn } from "cn"

import {
  fromDateKey,
  getTodayKey,
  subscribeToday,
  toDateKey,
  useToday,
} from "@/hooks/use-today"
import { easeOut, easeSpring } from "@/lib/motion"
import {
  addToRange,
  DayPicker,
  getDefaultClassNames,
  useDayPicker,
  type ChevronProps,
  type DateRange,
  type DayButtonProps,
  type DayEventHandler,
  type DayProps,
  type MonthCaptionProps,
  type MonthProps,
  type MonthsProps,
  type RootProps,
  type WeekNumberProps,
  type WeeksProps,
} from "react-day-picker"

import { Button, buttonVariants } from "@/components/ui/button"

type CalendarContextValue = {
  id: string
  todayKey: string | undefined
  animate: boolean
  hovered: Date | undefined
}

const CalendarContext = React.createContext<CalendarContextValue | null>(null)
const CalendarPreviewContext = React.createContext<DateRange | undefined>(
  undefined
)

function useCalendarContext(part: string) {
  const context = React.useContext(CalendarContext)

  if (!context) {
    throw new Error(`<${part}> must be used within <Calendar>.`)
  }

  return context
}

function omit<T extends object, K extends keyof T>(value: T, ...keys: K[]) {
  const result = { ...value }
  for (const key of keys) {
    delete result[key]
  }
  return result as Omit<T, K>
}

function isSameDay(a: Date, b: Date) {
  return (
    a.getFullYear() === b.getFullYear() &&
    a.getMonth() === b.getMonth() &&
    a.getDate() === b.getDate()
  )
}

function subscribeNothing() {
  return () => {}
}

function readServerToday(id: string) {
  if (typeof document === "undefined") {
    return getTodayKey()
  }

  const root = document.querySelector(`[data-calendar-id="${CSS.escape(id)}"]`)

  return root?.getAttribute("data-today") ?? getTodayKey()
}

function useCalendarToday(id: string, enabled: boolean) {
  const getServerSnapshot = React.useCallback(() => readServerToday(id), [id])
  const todayKey = React.useSyncExternalStore(
    enabled ? subscribeToday : subscribeNothing,
    getTodayKey,
    getServerSnapshot
  )
  const isHydrating = React.useSyncExternalStore(
    subscribeNothing,
    () => false,
    () => true
  )
  const [initialKey] = React.useState(todayKey)
  const [remount, setRemount] = React.useState<boolean | undefined>(undefined)

  if (enabled && !isHydrating && remount === undefined) {
    setRemount(initialKey.slice(0, 7) !== todayKey.slice(0, 7))
  }

  return { todayKey: enabled ? todayKey : undefined, remount: remount === true }
}

type CalendarProps = React.ComponentProps<typeof DayPicker> & {
  buttonVariant?: React.ComponentProps<typeof Button>["variant"]
}

function composeDayHandler<Event extends React.SyntheticEvent>(
  userHandler: DayEventHandler<Event> | undefined,
  internal: DayEventHandler<Event> | undefined
): DayEventHandler<Event> | undefined {
  if (!internal) {
    return userHandler
  }

  return (date, modifiers, event) => {
    userHandler?.(date, modifiers, event)
    if (!event.defaultPrevented) {
      internal(date, modifiers, event)
    }
  }
}

function Calendar({
  className,
  classNames,
  showOutsideDays = true,
  captionLayout = "label",
  navLayout = "around",
  buttonVariant = "ghost",
  animate = true,
  formatters,
  components,
  dir,
  today,
  onDayMouseEnter,
  onDayMouseLeave,
  onDayFocus,
  onDayBlur,
  ...props
}: CalendarProps) {
  const defaultClassNames = getDefaultClassNames()
  const id = React.useId()
  const direction = useDirection()
  const [hovered, setHovered] = React.useState<Date>()
  const managesToday = today === undefined && props.timeZone === undefined
  const { todayKey, remount } = useCalendarToday(id, managesToday)
  const isRange = props.mode === "range"

  const context = React.useMemo(
    () => ({ id, todayKey, animate, hovered }),
    [id, todayKey, animate, hovered]
  )

  const track = React.useCallback<DayEventHandler<React.SyntheticEvent>>(
    (date, modifiers) => {
      setHovered(modifiers.disabled ? undefined : date)
    },
    []
  )
  const clear = React.useCallback<DayEventHandler<React.SyntheticEvent>>(
    () => setHovered(undefined),
    []
  )

  return (
    <CalendarContext.Provider value={context}>
      <DayPicker
        key={remount ? "client" : "server"}
        dir={dir ?? (direction === "rtl" ? "rtl" : undefined)}
        today={today ?? (todayKey ? fromDateKey(todayKey) : undefined)}
        showOutsideDays={showOutsideDays}
        captionLayout={captionLayout}
        navLayout={navLayout}
        onDayMouseEnter={composeDayHandler(
          onDayMouseEnter,
          isRange ? track : undefined
        )}
        onDayMouseLeave={composeDayHandler(
          onDayMouseLeave,
          isRange ? clear : undefined
        )}
        onDayFocus={composeDayHandler(onDayFocus, isRange ? track : undefined)}
        onDayBlur={composeDayHandler(onDayBlur, isRange ? clear : undefined)}
        className={cn(
          "group/calendar bg-background p-3 [--cell-radius:var(--radius-md)] [--cell-size:--spacing(9)] in-data-[slot=alert-dialog-content]:bg-transparent in-data-[slot=card-content]:bg-transparent in-data-[slot=dialog-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent in-data-[slot=sheet-content]:bg-transparent pointer-coarse:[--cell-size:--spacing(11)]",
          className
        )}
        formatters={{
          formatMonthDropdown: (month, dateLib) =>
            dateLib ? dateLib.format(month, "LLL") : toDateKey(month),
          ...formatters,
        }}
        classNames={{
          root: cn("w-fit max-w-full", defaultClassNames.root),
          months: cn(
            "relative flex flex-col gap-4 sm:flex-row",
            defaultClassNames.months
          ),
          month: cn(
            "relative flex w-[calc(var(--cell-size)*7)] max-w-full min-w-0 flex-col gap-3 group-data-week-numbers/calendar:w-[calc(var(--cell-size)*8)]",
            defaultClassNames.month
          ),
          nav: cn(
            "pointer-events-none absolute inset-x-0 top-0 z-10 flex h-(--cell-size) items-center justify-between gap-1 *:pointer-events-auto",
            defaultClassNames.nav
          ),
          button_previous: cn(
            buttonVariants({ variant: buttonVariant, size: "icon" }),
            "size-(--cell-size) rounded-(--cell-radius) p-0 aria-disabled:pointer-events-none aria-disabled:opacity-50",
            navLayout === "around" && "absolute start-0 top-0 z-10",
            defaultClassNames.button_previous
          ),
          button_next: cn(
            buttonVariants({ variant: buttonVariant, size: "icon" }),
            "size-(--cell-size) rounded-(--cell-radius) p-0 aria-disabled:pointer-events-none aria-disabled:opacity-50",
            navLayout === "around" && "absolute end-0 top-0 z-10",
            defaultClassNames.button_next
          ),
          month_caption: cn(
            "flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)",
            defaultClassNames.month_caption
          ),
          dropdowns: cn(
            "flex h-(--cell-size) w-full items-center justify-center gap-1 text-sm font-medium",
            defaultClassNames.dropdowns
          ),
          dropdown_root: cn(
            "relative rounded-(--cell-radius) transition-[background-color,box-shadow] duration-150 ease-out-quint has-focus-visible:ring-3 has-focus-visible:ring-focus-ring [@media(hover:hover)]:has-[select:hover]:bg-muted",
            defaultClassNames.dropdown_root
          ),
          dropdown: cn(
            "absolute inset-0 cursor-pointer bg-popover text-popover-foreground opacity-0",
            defaultClassNames.dropdown
          ),
          caption_label: cn(
            "font-medium select-none",
            captionLayout === "label"
              ? "truncate text-sm"
              : "flex h-8 items-center gap-1 rounded-(--cell-radius) ps-2 pe-1 text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground",
            defaultClassNames.caption_label
          ),
          month_grid: cn(
            "w-full table-fixed border-collapse",
            defaultClassNames.month_grid
          ),
          weekdays: cn("flex", defaultClassNames.weekdays),
          weekday: cn(
            "w-(--cell-size) min-w-0 text-xs font-normal text-muted-foreground select-none",
            defaultClassNames.weekday
          ),
          week: cn("mt-1 flex w-full", defaultClassNames.week),
          week_number_header: cn(
            "w-(--cell-size) shrink-0 select-none",
            defaultClassNames.week_number_header
          ),
          week_number: cn(
            "text-xs font-normal text-muted-foreground select-none",
            defaultClassNames.week_number
          ),
          day: cn(
            "group/day relative aspect-square w-(--cell-size) min-w-0 p-0 text-center transition-[background-color] duration-150 ease-out-quint select-none data-preview:bg-muted/60 data-[preview=end]:rounded-e-(--cell-radius) data-[preview=start]:rounded-s-(--cell-radius) data-[preview=end]:data-selected:rounded-s-none data-[preview=start]:data-selected:rounded-e-none [&:is([data-preview],[data-range-middle]):first-of-type]:rounded-s-(--cell-radius) [&:is([data-preview],[data-range-middle]):has(+[data-duplicate])]:rounded-e-(--cell-radius) [&:is([data-preview],[data-range-middle]):last-of-type]:rounded-e-(--cell-radius) [[data-duplicate]+&:is([data-preview],[data-range-middle])]:rounded-s-(--cell-radius)",
            defaultClassNames.day
          ),
          range_start: cn(
            "rounded-s-(--cell-radius) bg-muted",
            defaultClassNames.range_start
          ),
          range_middle: cn("bg-muted", defaultClassNames.range_middle),
          range_end: cn(
            "rounded-e-(--cell-radius) bg-muted",
            defaultClassNames.range_end
          ),
          today: cn(defaultClassNames.today),
          outside: cn(defaultClassNames.outside),
          disabled: cn(defaultClassNames.disabled),
          hidden: cn("invisible", defaultClassNames.hidden),
          footer: cn(
            "pt-3 text-sm text-pretty text-muted-foreground",
            defaultClassNames.footer
          ),
          ...classNames,
        }}
        components={{
          Root: CalendarRoot,
          Chevron: CalendarChevron,
          Months: CalendarMonths,
          Month: CalendarMonth,
          MonthCaption: CalendarMonthCaption,
          Weeks: CalendarWeeks,
          Day: CalendarDay,
          DayButton: CalendarDayButton,
          WeekNumber: CalendarWeekNumber,
          ...components,
        }}
        {...props}
      />
    </CalendarContext.Provider>
  )
}

function CalendarRoot({
  className,
  rootRef,
  onKeyDownCapture,
  ...props
}: RootProps) {
  const { id, todayKey } = useCalendarContext("CalendarRoot")

  return (
    <div
      ref={rootRef}
      className={className}
      onKeyDownCapture={(event) => {
        event.currentTarget.dataset.keyboardAt = String(performance.now())
        onKeyDownCapture?.(event)
      }}
      {...props}
      data-slot="calendar"
      data-calendar-id={id}
      data-today={todayKey}
    />
  )
}

function CalendarChevron({ className, orientation, ...props }: ChevronProps) {
  const { dayPickerProps } = useDayPicker()
  const mirrored =
    dayPickerProps.navLayout === "around" && dayPickerProps.dir === "rtl"
  const icons = {
    left: IconChevronLeft,
    right: IconChevronRight,
    up: IconChevronUp,
    down: IconChevronDown,
  }
  const Icon = icons[orientation ?? "left"]

  return (
    <Icon
      aria-hidden
      className={cn(
        "size-4",
        !mirrored &&
          (orientation === "left" || orientation === "right") &&
          "rtl:-scale-x-100",
        className
      )}
      style={props.style}
    />
  )
}

function CalendarMonths({ className, ...props }: MonthsProps) {
  const { hovered } = useCalendarContext("CalendarMonths")
  const { selected, dayPickerProps } = useDayPicker()

  const preview = React.useMemo(() => {
    if (dayPickerProps.mode !== "range" || !hovered) {
      return undefined
    }

    const range = selected as DateRange | undefined

    if (!range?.from || (range.to && !isSameDay(range.from, range.to))) {
      return undefined
    }

    const next = addToRange(
      hovered,
      range,
      dayPickerProps.min,
      dayPickerProps.max,
      dayPickerProps.required
    )

    if (!next?.from || !next.to || isSameDay(next.from, next.to)) {
      return undefined
    }

    return next
  }, [dayPickerProps, hovered, selected])

  return (
    <CalendarPreviewContext.Provider value={preview}>
      <div className={className} {...props} data-slot="calendar-months" />
    </CalendarPreviewContext.Provider>
  )
}

const bottomClip = "inset(-1rem -1rem 0 -1rem)"

function layoutSize(element: HTMLElement) {
  const style = getComputedStyle(element)
  const width = parseFloat(style.width)
  const height = parseFloat(style.height)
  if (Number.isFinite(width) && Number.isFinite(height)) {
    return { width, height }
  }
  const rect = element.getBoundingClientRect()
  return { width: rect.width, height: rect.height }
}

function CalendarMonth({ calendarMonth, className, ...rest }: MonthProps) {
  const props = omit(rest, "displayIndex")
  const { animate } = useCalendarContext("CalendarMonth")
  const elementRef = React.useRef<HTMLDivElement>(null)
  const time = calendarMonth.date.getTime()
  const previousTimeRef = React.useRef(time)
  const animationsRef = React.useRef<Animation[]>([])
  const heightRef = React.useRef(0)

  React.useLayoutEffect(() => {
    const element = elementRef.current

    if (!element) {
      return
    }

    heightRef.current = layoutSize(element).height

    if (typeof ResizeObserver !== "function") {
      return
    }

    const observer = new ResizeObserver(() => {
      if (animationsRef.current.every((a) => a.playState !== "running")) {
        heightRef.current = layoutSize(element).height
      }
    })
    observer.observe(element)

    return () => observer.disconnect()
  }, [])

  React.useLayoutEffect(() => {
    const element = elementRef.current
    const previous = previousTimeRef.current
    previousTimeRef.current = time

    if (
      !animate ||
      !element ||
      previous === time ||
      typeof element.animate !== "function"
    ) {
      return
    }

    const reduceMotion =
      typeof window.matchMedia !== "function" ||
      window.matchMedia("(prefers-reduced-motion: reduce)").matches
    const keyboardAt = Number(
      element.closest<HTMLElement>("[data-slot=calendar]")?.dataset
        .keyboardAt ?? -Infinity
    )
    const fromKeyboard = performance.now() - keyboardAt < 100
    const rtl = getComputedStyle(element).direction === "rtl"
    const sign = (time > previous ? 1 : -1) * (rtl ? -1 : 1)
    const distance = Math.min(32, layoutSize(element).width * 0.1)
    const weeks = element.querySelector<HTMLElement>(
      "[data-slot=calendar-weeks]"
    )
    const caption = element.querySelector<HTMLElement>(
      "[data-slot=calendar-month-caption]"
    )
    const interrupted = animationsRef.current.some(
      (a) => a.playState === "running"
    )
    const fromHeight = interrupted
      ? layoutSize(element).height
      : heightRef.current
    const fromOpacity =
      interrupted && weeks ? parseFloat(getComputedStyle(weeks).opacity) / 2 : 0

    for (const running of animationsRef.current) {
      running.cancel()
    }

    const toHeight = layoutSize(element).height
    heightRef.current = toHeight

    const slide = (target: HTMLElement, offset: number, duration: number) =>
      target.animate(
        reduceMotion || fromKeyboard
          ? [{ opacity: fromOpacity }, { opacity: 1 }]
          : [
              { translate: `${sign * offset}px 0`, opacity: fromOpacity },
              { translate: "0 0", opacity: 1 },
            ],
        {
          duration: reduceMotion ? 150 : fromKeyboard ? 120 : duration,
          easing: easeOut,
        }
      )

    const next: Animation[] = []

    if (weeks) {
      next.push(slide(weeks, distance, 280))
    }

    if (caption) {
      next.push(slide(caption, distance * 0.4, 220))
    }

    if (fromHeight > 0 && Math.abs(fromHeight - toHeight) > 0.5) {
      next.push(
        element.animate(
          [
            { height: `${fromHeight}px`, clipPath: bottomClip },
            { height: `${toHeight}px`, clipPath: bottomClip },
          ],
          { duration: reduceMotion ? 0 : 300, easing: easeSpring }
        )
      )
    }

    animationsRef.current = next
  }, [animate, time])

  React.useEffect(() => {
    const animations = animationsRef
    return () => {
      for (const running of animations.current) {
        running.cancel()
      }
    }
  }, [])

  return (
    <div
      ref={elementRef}
      className={className}
      {...props}
      data-slot="calendar-month"
    />
  )
}

function CalendarMonthCaption({ className, ...rest }: MonthCaptionProps) {
  const props = omit(rest, "calendarMonth", "displayIndex")
  return (
    <div className={className} {...props} data-slot="calendar-month-caption" />
  )
}

function CalendarWeeks({ className, ...props }: WeeksProps) {
  return <tbody className={className} {...props} data-slot="calendar-weeks" />
}

function CalendarDay({
  day,
  modifiers,
  className,
  children,
  ...props
}: DayProps) {
  const preview = React.useContext(CalendarPreviewContext)
  const { months } = useDayPicker()
  const duplicate =
    day.outside &&
    months.length > 1 &&
    months.some(
      (month) =>
        month.date.getFullYear() === day.date.getFullYear() &&
        month.date.getMonth() === day.date.getMonth()
    )

  if (duplicate) {
    return (
      <td
        className={cn(className, "invisible")}
        {...props}
        aria-hidden
        data-slot="calendar-day"
        data-duplicate=""
      />
    )
  }

  let state: "start" | "middle" | "end" | undefined

  if (preview?.from && preview.to && !modifiers.hidden) {
    const date = day.date

    if (isSameDay(date, preview.from)) {
      state = "start"
    } else if (isSameDay(date, preview.to)) {
      state = "end"
    } else if (date > preview.from && date < preview.to) {
      state = "middle"
    }
  }

  return (
    <td
      className={className}
      {...props}
      data-slot="calendar-day"
      data-preview={state}
      data-range-middle={modifiers.range_middle ? "" : undefined}
    >
      {children}
    </td>
  )
}

function CalendarDayButton({
  className,
  day,
  modifiers,
  ...props
}: DayButtonProps) {
  const ref = React.useRef<HTMLButtonElement>(null)

  React.useEffect(() => {
    if (modifiers.focused) {
      ref.current?.focus()
    }
  }, [modifiers.focused])

  const rangeStart = Boolean(modifiers.range_start)
  const rangeEnd = Boolean(modifiers.range_end)
  const rangeMiddle = Boolean(modifiers.range_middle)
  const selectedSingle =
    Boolean(modifiers.selected) && !rangeStart && !rangeEnd && !rangeMiddle

  return (
    <button
      ref={ref}
      className={cn(
        buttonVariants({ variant: "ghost", size: "icon" }),
        "relative size-full rounded-(--cell-radius) p-0 font-normal tabular-nums group-data-outside/day:text-muted-foreground focus-visible:z-10 data-[range-end]:bg-primary data-[range-end]:text-primary-foreground data-[range-end]:hover:bg-primary/90 data-[range-middle]:rounded-none data-[range-middle]:hover:rounded-(--cell-radius) data-[range-middle]:hover:bg-foreground/6 data-[range-start]:bg-primary data-[range-start]:text-primary-foreground data-[range-start]:hover:bg-primary/90 data-[selected-single]:bg-primary data-[selected-single]:text-primary-foreground data-[selected-single]:hover:bg-primary/90 data-[today]:bg-info/12 data-[today]:font-semibold data-[today]:text-info data-[today]:hover:bg-info/15 data-[today]:data-[range-end]:bg-info data-[today]:data-[range-end]:text-info-foreground data-[today]:data-[range-end]:hover:bg-info/90 data-[today]:data-[range-middle]:bg-transparent data-[today]:data-[range-middle]:hover:bg-foreground/6 data-[today]:data-[range-start]:bg-info data-[today]:data-[range-start]:text-info-foreground data-[today]:data-[range-start]:hover:bg-info/90 data-[today]:data-[selected-single]:bg-info data-[today]:data-[selected-single]:text-info-foreground data-[today]:data-[selected-single]:hover:bg-info/90 forced-colors:data-[range-end]:outline-2 forced-colors:data-[range-end]:-outline-offset-2 forced-colors:data-[range-end]:outline-solid forced-colors:data-[range-start]:outline-2 forced-colors:data-[range-start]:-outline-offset-2 forced-colors:data-[range-start]:outline-solid forced-colors:data-[selected-single]:outline-2 forced-colors:data-[selected-single]:-outline-offset-2 forced-colors:data-[selected-single]:outline-solid",
        className
      )}
      {...props}
      data-slot="calendar-day-button"
      data-day={day.isoDate}
      data-today={modifiers.today && !modifiers.outside ? "" : undefined}
      data-selected-single={selectedSingle ? "" : undefined}
      data-range-start={rangeStart ? "" : undefined}
      data-range-end={rangeEnd ? "" : undefined}
      data-range-middle={rangeMiddle ? "" : undefined}
    />
  )
}

function CalendarWeekNumber({ children, className, ...rest }: WeekNumberProps) {
  const props = omit(rest, "week")
  return (
    <th className={className} {...props} data-slot="calendar-week-number">
      <div className="flex size-(--cell-size) items-center justify-center text-center">
        {children}
      </div>
    </th>
  )
}

export { Calendar, CalendarDayButton, useToday }
export type { CalendarProps }
```

```tsx title="components/ui/popover.tsx"
"use client"

import * as React from "react"
import {
  DirectionProvider,
  useDirection,
  type TextDirection,
} from "@base-ui/react/direction-provider"
import { Popover as PopoverPrimitive } from "@base-ui/react/popover"
import { cn } from "cn"

type ClassName<State> =
  string | ((state: State) => string | undefined) | undefined

function mergeClassName<State>(base: string, className: ClassName<State>) {
  return typeof className === "function"
    ? (state: State) => cn(base, className(state))
    : cn(base, className)
}

function readDirection(element: Element | null | undefined) {
  if (!element || !element.isConnected) {
    return undefined
  }
  return getComputedStyle(element).direction === "rtl" ? "rtl" : "ltr"
}

function Popover<Payload>({
  onOpenChange,
  children,
  ...props
}: PopoverPrimitive.Root.Props<Payload>) {
  const inherited = useDirection()
  const [triggerDirection, setTriggerDirection] =
    React.useState<TextDirection>()
  const direction = triggerDirection === "rtl" ? "rtl" : inherited

  return (
    <DirectionProvider direction={direction}>
      <PopoverPrimitive.Root
        onOpenChange={(open, eventDetails) => {
          onOpenChange?.(open, eventDetails)
          if (open && !eventDetails.isCanceled && eventDetails.trigger) {
            setTriggerDirection(readDirection(eventDetails.trigger))
          }
        }}
        {...props}
      >
        {children}
      </PopoverPrimitive.Root>
    </DirectionProvider>
  )
}

function PopoverTrigger<Payload>(
  props: PopoverPrimitive.Trigger.Props<Payload>
) {
  return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
}

function PopoverPortal(props: PopoverPrimitive.Portal.Props) {
  return <PopoverPrimitive.Portal data-slot="popover-portal" {...props} />
}

const tracking = 80

function useAnimatedHeight() {
  return React.useCallback((sizer: HTMLDivElement | null) => {
    const popup = sizer?.parentElement
    if (!sizer || !popup || typeof ResizeObserver === "undefined") {
      return
    }
    const positioner = popup.parentElement
    if (positioner && !positioner.hasAttribute("dir")) {
      const trigger = popup.id
        ? popup.ownerDocument.querySelector(
            `[aria-controls="${CSS.escape(popup.id)}"]`
          )
        : null
      if (readDirection(trigger) === "rtl") {
        positioner.setAttribute("dir", "rtl")
      }
    }
    let last = 0
    let settle: ReturnType<typeof setTimeout> | undefined
    let sized: ReturnType<typeof setTimeout> | undefined
    const endSizing = (event?: TransitionEvent) => {
      if (
        event &&
        (event.target !== popup || event.propertyName !== "height")
      ) {
        return
      }
      clearTimeout(sized)
      delete popup.dataset.sizing
    }
    popup.addEventListener("transitionend", endSizing)
    popup.addEventListener("transitioncancel", endSizing)
    const observer = new ResizeObserver(([entry]) => {
      const now = performance.now()
      const continuous = now - last < tracking
      if (continuous) {
        popup.dataset.resizing = ""
      }
      last = now
      clearTimeout(settle)
      settle = setTimeout(() => {
        delete popup.dataset.resizing
      }, tracking)
      if (popup.style.height && !continuous) {
        popup.dataset.sizing = ""
        clearTimeout(sized)
        sized = setTimeout(endSizing, 300)
      }
      const style = getComputedStyle(popup)
      const chrome =
        parseFloat(style.paddingTop) +
        parseFloat(style.paddingBottom) +
        parseFloat(style.borderTopWidth) +
        parseFloat(style.borderBottomWidth)
      const height =
        entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height
      popup.style.height = `${height + chrome}px`
    })
    observer.observe(sizer)
    return () => {
      observer.disconnect()
      clearTimeout(settle)
      clearTimeout(sized)
      popup.removeEventListener("transitionend", endSizing)
      popup.removeEventListener("transitioncancel", endSizing)
      delete popup.dataset.resizing
      delete popup.dataset.sizing
      popup.style.height = ""
    }
  }, [])
}

type PopoverContentProps = PopoverPrimitive.Popup.Props &
  Pick<
    PopoverPrimitive.Positioner.Props,
    | "side"
    | "align"
    | "sideOffset"
    | "alignOffset"
    | "anchor"
    | "collisionPadding"
    | "collisionAvoidance"
    | "collisionBoundary"
    | "sticky"
    | "positionMethod"
  > & {
    portalProps?: Omit<PopoverPrimitive.Portal.Props, "children">
  }

function PopoverContent({
  className,
  children,
  side = "bottom",
  align = "center",
  sideOffset = 6,
  alignOffset = 0,
  anchor,
  collisionPadding = 8,
  collisionAvoidance,
  collisionBoundary,
  sticky,
  positionMethod,
  portalProps,
  ...props
}: PopoverContentProps) {
  const sizerRef = useAnimatedHeight()
  const direction = useDirection()

  return (
    <PopoverPrimitive.Portal {...portalProps}>
      <PopoverPrimitive.Positioner
        data-slot="popover-positioner"
        dir={direction === "rtl" ? "rtl" : undefined}
        side={side}
        align={align}
        sideOffset={sideOffset}
        alignOffset={alignOffset}
        anchor={anchor}
        collisionPadding={collisionPadding}
        collisionAvoidance={collisionAvoidance}
        collisionBoundary={collisionBoundary}
        sticky={sticky}
        positionMethod={positionMethod}
        className="isolate z-50 outline-none focus-visible:outline-hidden"
      >
        <PopoverPrimitive.Popup
          data-slot="popover-content"
          className={mergeClassName(
            "relative flex max-h-(--available-height) w-72 max-w-(--available-width) origin-(--transform-origin) flex-col gap-2.5 overflow-x-hidden overflow-y-auto overscroll-none rounded-lg bg-popover p-2.5 text-sm text-popover-foreground shadow-md ring-(length:--hairline) ring-foreground/10 transition-[opacity,scale,height] duration-150 ease-out-quint outline-none focus-visible:outline-hidden data-ending-style:opacity-0 data-ending-style:duration-100 data-ending-style:data-instant:transition-none data-starting-style:opacity-0 data-[resizing]:transition-[opacity,scale] data-[sizing]:overflow-y-hidden motion-safe:data-ending-style:scale-96 motion-safe:data-starting-style:scale-96 motion-reduce:transition-opacity forced-colors:border",
            className
          )}
          {...props}
        >
          <div
            ref={sizerRef}
            data-slot="popover-content-sizer"
            className="flex min-w-0 shrink-0 flex-col gap-[inherit]"
          >
            {children}
          </div>
        </PopoverPrimitive.Popup>
      </PopoverPrimitive.Positioner>
    </PopoverPrimitive.Portal>
  )
}

function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="popover-header"
      className={cn("flex min-w-0 flex-col gap-0.5 text-sm", className)}
      {...props}
    />
  )
}

function PopoverTitle({ className, ...props }: PopoverPrimitive.Title.Props) {
  return (
    <PopoverPrimitive.Title
      data-slot="popover-title"
      className={mergeClassName(
        "text-sm font-medium text-pretty wrap-anywhere",
        className
      )}
      {...props}
    />
  )
}

function PopoverDescription({
  className,
  ...props
}: PopoverPrimitive.Description.Props) {
  return (
    <PopoverPrimitive.Description
      data-slot="popover-description"
      className={mergeClassName(
        "text-sm text-pretty wrap-anywhere text-muted-foreground",
        className
      )}
      {...props}
    />
  )
}

function PopoverClose(props: PopoverPrimitive.Close.Props) {
  return <PopoverPrimitive.Close data-slot="popover-close" {...props} />
}

const createPopoverHandle = PopoverPrimitive.createHandle

export {
  Popover,
  PopoverTrigger,
  PopoverPortal,
  PopoverContent,
  PopoverHeader,
  PopoverTitle,
  PopoverDescription,
  PopoverClose,
  createPopoverHandle,
}
export type { PopoverContentProps }
```

```tsx title="components/ui/sheet.tsx"
"use client"

import * as React from "react"
import { Drawer as SheetPrimitive } from "@base-ui/react/drawer"
import { IconX } from "@tabler/icons-react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"

import { Button } from "@/components/ui/button"

type ClassName<State> =
  string | ((state: State) => string | undefined) | undefined

function mergeClassName<State>(base: string, className: ClassName<State>) {
  return typeof className === "function"
    ? (state: State) => cn(base, className(state))
    : cn(base, className)
}

const DialogLayerContext = React.createContext(0)

function DialogLayerProvider({ children }: { children: React.ReactNode }) {
  const depth = React.useContext(DialogLayerContext)

  return (
    <DialogLayerContext.Provider value={depth + 1}>
      {children}
    </DialogLayerContext.Provider>
  )
}

function useDialogLayer() {
  return React.useContext(DialogLayerContext)
}

type SwipeDirection = "up" | "down" | "left" | "right"

const SheetRootContext = React.createContext<
  ((direction: SwipeDirection) => void) | null
>(null)

function Sheet<Payload>({
  swipeDirection,
  ...props
}: SheetPrimitive.Root.Props<Payload>) {
  const [contentDirection, setContentDirection] =
    React.useState<SwipeDirection>("right")

  return (
    <DialogLayerProvider>
      <SheetRootContext.Provider value={setContentDirection}>
        <SheetPrimitive.Root
          data-slot="sheet"
          swipeDirection={swipeDirection ?? contentDirection}
          {...props}
        />
      </SheetRootContext.Provider>
    </DialogLayerProvider>
  )
}

function SheetTrigger<Payload>(props: SheetPrimitive.Trigger.Props<Payload>) {
  return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
}

function SheetPortal(props: SheetPrimitive.Portal.Props) {
  return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
}

function SheetClose(props: SheetPrimitive.Close.Props) {
  return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
}

const sheetOverlayVariants = cva(
  "fixed inset-0 z-50 min-h-dvh opacity-[calc(1-var(--drawer-swipe-progress,0))] transition-opacity duration-300 ease-drawer data-ending-style:opacity-0 data-ending-style:duration-[calc(var(--drawer-swipe-strength,0.7)*300ms)] data-starting-style:opacity-0 supports-backdrop-filter:backdrop-blur-xs supports-[-webkit-touch-callout:none]:absolute sm:has-[~[data-slot=sheet-viewport]>[data-slot$=dialog-content]]:duration-200 sm:has-[~[data-slot=sheet-viewport]>[data-slot$=dialog-content]]:ease-out-quint sm:has-[~[data-slot=sheet-viewport]>[data-slot$=dialog-content]]:data-ending-style:duration-150 [&[data-swiping]:not([data-ending-style])]:duration-0",
  {
    variants: {
      nested: {
        false: "bg-black/40 dark:bg-black/60",
        true: "bg-black/20 dark:bg-black/30",
      },
    },
    defaultVariants: {
      nested: false,
    },
  }
)

function SheetOverlay({
  className,
  forceRender,
  ...props
}: SheetPrimitive.Backdrop.Props) {
  const nested = useDialogLayer() > 1

  return (
    <SheetPrimitive.Backdrop
      data-slot="sheet-overlay"
      data-nested={nested ? "" : undefined}
      forceRender={forceRender ?? nested}
      className={mergeClassName(sheetOverlayVariants({ nested }), className)}
      {...props}
    />
  )
}

const sheetContentVariants = cva(
  "fixed z-50 flex flex-col gap-4 overflow-y-auto overscroll-none bg-popover text-popover-foreground shadow-lg ring-(length:--hairline) ring-foreground/10 transition-[transform,translate,scale,opacity,top] duration-300 ease-drawer outline-none [--bleed:3rem] focus-visible:outline-hidden data-ending-style:duration-[calc(var(--drawer-swipe-strength,0.7)*300ms)] data-nested-dialog-open:scale-[calc(1-0.02*var(--nested-dialogs))] data-nested-drawer-open:scale-[calc(1-0.02*var(--nested-drawers))] data-swiping:cursor-grabbing data-swiping:select-none motion-reduce:transition-opacity motion-reduce:data-ending-style:opacity-0 motion-reduce:data-starting-style:opacity-0 rtl:[--sheet-dir:-1] forced-colors:border [&[data-swiping]:not([data-ending-style])]:duration-0",
  {
    variants: {
      side: {
        top: "inset-x-0 top-[calc(var(--bleed)*-1)] max-h-[calc(100dvh-2rem+var(--bleed))] [transform:translateY(var(--drawer-swipe-movement-y,0px))] rounded-b-2xl pt-(--bleed) [--sheet-close-top:calc(0.75rem+var(--bleed))] motion-safe:data-ending-style:translate-y-[calc(-100%+var(--bleed))] motion-safe:data-starting-style:translate-y-[calc(-100%+var(--bleed))]",
        right:
          "inset-y-0 end-[calc(var(--bleed)*-1)] h-full w-[calc(75%+var(--bleed))] [transform:translateX(var(--drawer-swipe-movement-x,0px))] rounded-s-2xl pe-(--bleed) [--sheet-close-end:calc(0.75rem+var(--bleed))] motion-safe:data-ending-style:translate-x-[calc(var(--sheet-dir,1)*(100%-var(--bleed)))] motion-safe:data-starting-style:translate-x-[calc(var(--sheet-dir,1)*(100%-var(--bleed)))] sm:max-w-[calc(24rem+var(--bleed))]",
        bottom:
          "inset-x-0 bottom-[calc(var(--bleed)*-1)] max-h-[calc(100dvh-2rem+var(--bleed))] [transform:translateY(var(--drawer-swipe-movement-y,0px))] rounded-t-2xl pb-[calc(var(--bleed)+env(safe-area-inset-bottom))] [--sheet-close-radius:calc(var(--radius-2xl)-0.75rem)] motion-safe:data-ending-style:translate-y-[calc(100%-var(--bleed))] motion-safe:data-starting-style:translate-y-[calc(100%-var(--bleed))]",
        left: "inset-y-0 start-[calc(var(--bleed)*-1)] h-full w-[calc(75%+var(--bleed))] [transform:translateX(var(--drawer-swipe-movement-x,0px))] rounded-e-2xl ps-(--bleed) [--sheet-close-radius:calc(var(--radius-2xl)-0.75rem)] motion-safe:data-ending-style:translate-x-[calc(var(--sheet-dir,1)*(var(--bleed)-100%))] motion-safe:data-starting-style:translate-x-[calc(var(--sheet-dir,1)*(var(--bleed)-100%))] sm:max-w-[calc(24rem+var(--bleed))]",
      },
    },
    defaultVariants: {
      side: "right",
    },
  }
)

const sheetHandleVariants = cva(
  "group/sheet-handle absolute z-10 flex cursor-grab touch-none items-center justify-center active:cursor-grabbing",
  {
    variants: {
      side: {
        top: "inset-x-0 bottom-0 h-6",
        right: "inset-y-0 start-0 w-6",
        bottom: "inset-x-0 top-0 h-6",
        left: "inset-y-0 end-0 w-6",
      },
    },
  }
)

const sheetHandleBarVariants = cva(
  "rounded-full bg-muted-foreground/30 transition-colors duration-150 group-hover/sheet-handle:bg-muted-foreground/50 group-active/sheet-handle:bg-muted-foreground/60",
  {
    variants: {
      side: {
        top: "h-1 w-10",
        right: "h-10 w-1",
        bottom: "h-1 w-10",
        left: "h-10 w-1",
      },
    },
  }
)

type SheetSide = NonNullable<VariantProps<typeof sheetContentVariants>["side"]>

const SheetContentContext = React.createContext<{
  showCloseButton: boolean
}>({ showCloseButton: false })

function resolveSwipeDirection(side: SheetSide, rtl: boolean): SwipeDirection {
  if (side === "top") {
    return "up"
  }
  if (side === "bottom") {
    return "down"
  }
  return (side === "right") !== rtl ? "right" : "left"
}

function SheetContent({
  className,
  children,
  side = "right",
  showCloseButton = true,
  dir,
  ...props
}: SheetPrimitive.Popup.Props &
  VariantProps<typeof sheetContentVariants> & {
    showCloseButton?: boolean
  }) {
  const resolvedSide = side ?? "right"
  const setSwipeDirection = React.useContext(SheetRootContext)
  const context = React.useMemo(() => ({ showCloseButton }), [showCloseButton])

  React.useEffect(() => {
    const rtl =
      (dir ?? getComputedStyle(document.documentElement).direction) === "rtl"
    setSwipeDirection?.(resolveSwipeDirection(resolvedSide, rtl))
  }, [dir, resolvedSide, setSwipeDirection])

  return (
    <SheetContentContext.Provider value={context}>
      <SheetPortal>
        <SheetOverlay />
        <SheetPrimitive.Viewport
          data-slot="sheet-viewport"
          dir={dir}
          className="fixed inset-0 z-50"
        >
          <SheetPrimitive.Popup
            data-slot="sheet-content"
            data-side={resolvedSide}
            dir={dir}
            className={mergeClassName(
              sheetContentVariants({ side: resolvedSide }),
              className
            )}
            {...props}
          >
            <div
              data-slot="sheet-handle"
              aria-hidden="true"
              className={sheetHandleVariants({ side: resolvedSide })}
            >
              <div className={sheetHandleBarVariants({ side: resolvedSide })} />
            </div>
            <SheetPrimitive.Content
              data-slot="sheet-inner"
              className="flex min-h-0 flex-1 flex-col gap-4"
            >
              {children}
            </SheetPrimitive.Content>
            {showCloseButton ? (
              <SheetPrimitive.Close
                data-slot="sheet-close-button"
                aria-label="Close"
                render={<Button variant="ghost" size="icon-sm" />}
                className="absolute end-(--sheet-close-end,0.75rem) top-(--sheet-close-top,0.75rem) z-20 rounded-(--sheet-close-radius,var(--radius-md)) text-muted-foreground hover:text-foreground"
              >
                <IconX />
              </SheetPrimitive.Close>
            ) : null}
          </SheetPrimitive.Popup>
        </SheetPrimitive.Viewport>
      </SheetPortal>
    </SheetContentContext.Provider>
  )
}

function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
  const { showCloseButton } = React.useContext(SheetContentContext)

  return (
    <div
      data-slot="sheet-header"
      className={cn(
        "flex flex-col gap-1.5 px-6 pt-6",
        showCloseButton && "pe-12",
        className
      )}
      {...props}
    />
  )
}

function SheetBody({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="sheet-body"
      className={cn(
        "min-h-0 min-w-0 flex-1 overflow-y-auto overscroll-none px-6 wrap-anywhere",
        className
      )}
      {...props}
    />
  )
}

function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="sheet-footer"
      className={cn(
        "mt-auto flex flex-col-reverse gap-2 px-6 pb-6 sm:flex-row sm:justify-end max-sm:[&>*:not([data-slot=button-status])]:h-10",
        className
      )}
      {...props}
    />
  )
}

function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) {
  return (
    <SheetPrimitive.Title
      data-slot="sheet-title"
      className={mergeClassName(
        "min-w-0 text-lg leading-snug font-semibold text-pretty wrap-anywhere",
        className
      )}
      {...props}
    />
  )
}

function SheetDescription({
  className,
  ...props
}: SheetPrimitive.Description.Props) {
  return (
    <SheetPrimitive.Description
      data-slot="sheet-description"
      className={mergeClassName(
        "min-w-0 text-sm text-pretty wrap-anywhere text-muted-foreground",
        className
      )}
      {...props}
    />
  )
}

const createSheetHandle = SheetPrimitive.createHandle

export {
  Sheet,
  SheetTrigger,
  SheetPortal,
  SheetOverlay,
  SheetContent,
  SheetHeader,
  SheetBody,
  SheetFooter,
  SheetTitle,
  SheetDescription,
  SheetClose,
  sheetContentVariants,
  sheetOverlayVariants,
  createSheetHandle,
  DialogLayerProvider,
  useDialogLayer,
}
```

```tsx title="components/ui/button.tsx"
"use client"

import * as React from "react"
import { Button as ButtonPrimitive } from "@base-ui/react/button"
import { IconAlertCircle, IconCircleCheck } from "@tabler/icons-react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"

import { Spinner } from "@/components/ui/spinner"
import {
  useButtonFeedback,
  type ButtonFeedbackOptions,
  type ButtonStatus,
} from "@/hooks/use-button-feedback"

const buttonVariants = cva(
  "group/button relative inline-flex items-center justify-center rounded-md bg-clip-padding text-sm font-medium whitespace-nowrap inset-ring-(length:--hairline) inset-ring-transparent transition-[color,background-color,box-shadow,opacity,translate,scale] duration-200 ease-out-quint outline-none select-none focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:inset-ring-ring focus-visible:outline-hidden active:duration-100 active:not-aria-[haspopup]:not-aria-busy:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-busy:cursor-progress aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:inset-ring-destructive motion-safe:active:not-aria-[haspopup]:not-aria-busy:scale-[0.97] dark:aria-invalid:ring-destructive/40 dark:aria-invalid:inset-ring-destructive/50 forced-colors:border [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/80",
        outline:
          "bg-background inset-ring-border hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:bg-input/30 dark:inset-ring-input dark:hover:bg-input/50",
        secondary:
          "bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
        ghost:
          "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50 forced-colors:border-0",
        destructive:
          "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:ring-destructive/70 focus-visible:inset-ring-destructive/40 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/70",
        link: "text-primary underline-offset-4 hover:underline forced-colors:border-0",
        success:
          "bg-success text-success-foreground hover:bg-success/90 focus-visible:ring-success/70 focus-visible:inset-ring-success/40",
        error:
          "bg-destructive text-destructive-foreground hover:bg-destructive/90 focus-visible:ring-destructive/70 focus-visible:inset-ring-destructive/40 motion-safe:animate-button-shake",
      },
      size: {
        default:
          "h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2",
        xs: "h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 [&_svg:not([class*='size-'])]:size-3",
        sm: "h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5",
        lg: "h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2",
        icon: "size-9 shrink-0",
        "icon-xs":
          "size-6 shrink-0 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md pointer-coarse:after:absolute pointer-coarse:after:-inset-2.5 pointer-coarse:in-data-[slot=button-group]:after:hidden [&_svg:not([class*='size-'])]:size-3",
        "icon-sm":
          "size-8 shrink-0 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md pointer-coarse:after:absolute pointer-coarse:after:-inset-1.5 pointer-coarse:in-data-[slot=button-group]:after:hidden",
        "icon-lg": "size-10 shrink-0",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

type ButtonVariant = Exclude<
  NonNullable<VariantProps<typeof buttonVariants>["variant"]>,
  "success" | "error"
>

type ButtonClickEvent = Parameters<
  NonNullable<ButtonPrimitive.Props["onClick"]>
>[0]

type ButtonProps = Omit<ButtonPrimitive.Props, "onClick"> &
  Omit<VariantProps<typeof buttonVariants>, "variant"> &
  ButtonFeedbackOptions & {
    variant?: ButtonVariant | null
    onClick?: (event: ButtonClickEvent) => unknown
    feedback?: boolean
    loading?: boolean
    status?: ButtonStatus
    loadingLabel?: React.ReactNode
    successLabel?: React.ReactNode
    errorLabel?: React.ReactNode | ((error: unknown) => React.ReactNode)
  }

const stateTransitionTime = 220
const statuses: ButtonStatus[] = ["idle", "loading", "success", "error"]
const quietVariants: ButtonVariant[] = ["ghost", "link"]
const quietStatusClasses: Partial<Record<ButtonStatus, string>> = {
  success: "text-success hover:text-success",
  error:
    "text-destructive hover:text-destructive motion-safe:animate-button-shake",
}

function isThenable(value: unknown): value is PromiseLike<unknown> {
  return (
    typeof value === "object" &&
    value !== null &&
    typeof (value as PromiseLike<unknown>).then === "function"
  )
}

function useStatusTransition(status: ButtonStatus) {
  const [shown, setShown] = React.useState(status)
  const [leaving, setLeaving] = React.useState<ButtonStatus | null>(null)
  const [transitionKey, setTransitionKey] = React.useState(0)

  if (status !== shown) {
    setLeaving(shown)
    setShown(status)
    setTransitionKey((key) => key + 1)
  }

  React.useEffect(() => {
    if (leaving === null) {
      return
    }
    const timer = setTimeout(() => setLeaving(null), stateTransitionTime)
    return () => clearTimeout(timer)
  }, [leaving, transitionKey])

  return { leaving, transitionKey }
}

function useLayerWidth(status: ButtonStatus, active: boolean) {
  const contentRef = React.useRef<HTMLSpanElement>(null)
  const layersRef = React.useRef(new Map<ButtonStatus, HTMLElement>())
  const observerRef = React.useRef<ResizeObserver | null>(null)
  const statusRef = React.useRef(status)

  const applyWidth = React.useCallback(() => {
    const content = contentRef.current
    const layer = layersRef.current.get(statusRef.current)
    if (!content || !layer) {
      return
    }
    const natural = parseFloat(getComputedStyle(layer).width)
    if (!Number.isFinite(natural)) {
      return
    }
    content.style.width = `${natural}px`
    if (!content.hasAttribute("data-measured")) {
      requestAnimationFrame(() => content.setAttribute("data-measured", ""))
    }
  }, [])

  React.useLayoutEffect(() => {
    statusRef.current = status
    if (active) {
      applyWidth()
    }
  }, [active, applyWidth, status])

  React.useLayoutEffect(() => {
    if (!active || typeof ResizeObserver === "undefined") {
      return
    }
    const observer = new ResizeObserver(applyWidth)
    observerRef.current = observer
    layersRef.current.forEach((layer) => observer.observe(layer))
    document.fonts?.addEventListener?.("loadingdone", applyWidth)
    return () => {
      observer.disconnect()
      observerRef.current = null
      document.fonts?.removeEventListener?.("loadingdone", applyWidth)
    }
  }, [active, applyWidth])

  const layerRef = React.useCallback((node: HTMLElement | null) => {
    if (!node) {
      return
    }
    const layer = node.dataset.layer as ButtonStatus
    layersRef.current.set(layer, node)
    observerRef.current?.observe(node)
    return () => {
      observerRef.current?.unobserve(node)
      if (layersRef.current.get(layer) === node) {
        layersRef.current.delete(layer)
      }
    }
  }, [])

  return { contentRef, layerRef }
}

function composeHandlers<Event>(
  first: ((event: Event) => void) | undefined,
  second: (event: Event) => void
) {
  return (event: Event) => {
    first?.(event)
    second(event)
  }
}

function Button({
  className,
  variant = "default",
  size = "default",
  children,
  onClick,
  disabled,
  focusableWhenDisabled,
  feedback,
  loading,
  status: statusProp,
  onStatusChange,
  onError,
  resetAfter,
  loadingLabel,
  successLabel = "Done",
  errorLabel = "Failed",
  onPointerEnter,
  onPointerLeave,
  onFocus,
  onBlur,
  ...props
}: ButtonProps) {
  const internal = useButtonFeedback({ resetAfter, onStatusChange, onError })
  const status = statusProp ?? (loading ? "loading" : internal.status)
  const requested =
    Boolean(feedback) || loading !== undefined || statusProp !== undefined
  const [enabled, setEnabled] = React.useState(requested)
  if (requested && !enabled) {
    setEnabled(true)
  }

  const { leaving, transitionKey } = useStatusTransition(status)
  const iconOnly = size?.startsWith("icon") ?? false
  const widthStatus =
    status === "loading" && loadingLabel === undefined ? "idle" : status
  const { contentRef, layerRef } = useLayerWidth(
    widthStatus,
    enabled && !iconOnly
  )
  const isLoading = status === "loading"
  const quiet = quietVariants.includes(variant ?? "default")
  const resolvedVariant =
    !quiet && status === "success"
      ? "success"
      : !quiet && status === "error"
        ? "error"
        : variant
  const resolvedErrorLabel =
    typeof errorLabel !== "function"
      ? errorLabel
      : internal.error !== undefined
        ? errorLabel(internal.error)
        : "Failed"

  const handleClick = (event: ButtonClickEvent) => {
    if (feedback && internal.isPending()) {
      return
    }
    const result = onClick?.(event)
    if (feedback && isThenable(result)) {
      internal.track(result)
    }
  }

  const layerContent = (layer: ButtonStatus, animated: boolean) => {
    if (layer === "idle") {
      return children
    }
    if (layer === "loading") {
      return (
        <>
          <Spinner aria-hidden size={null} animated={animated} />
          {iconOnly ? null : loadingLabel}
        </>
      )
    }
    if (layer === "success") {
      return (
        <>
          <IconCircleCheck
            className={cn(
              animated &&
                "motion-safe:[&>path:first-child]:animate-button-draw-circle motion-safe:[&>path:first-child]:[stroke-dasharray:57] motion-safe:[&>path:first-child]:[stroke-dashoffset:57] motion-safe:[&>path:last-child]:animate-button-draw-check motion-safe:[&>path:last-child]:[stroke-dasharray:9] motion-safe:[&>path:last-child]:[stroke-dashoffset:9]"
            )}
          />
          {iconOnly ? null : successLabel}
        </>
      )
    }
    return (
      <>
        <IconAlertCircle />
        {iconOnly ? null : resolvedErrorLabel}
      </>
    )
  }

  const button = (
    <ButtonPrimitive
      data-slot="button"
      data-status={enabled ? status : undefined}
      aria-busy={isLoading || undefined}
      disabled={disabled || isLoading}
      focusableWhenDisabled={focusableWhenDisabled ?? (isLoading || undefined)}
      className={cn(
        buttonVariants({ variant: resolvedVariant, size }),
        quiet && quietStatusClasses[status],
        className
      )}
      onClick={handleClick}
      onPointerEnter={composeHandlers(
        onPointerEnter,
        internal.buttonProps.onPointerEnter
      )}
      onPointerLeave={composeHandlers(
        onPointerLeave,
        internal.buttonProps.onPointerLeave
      )}
      onFocus={composeHandlers(onFocus, internal.buttonProps.onFocus)}
      onBlur={composeHandlers(onBlur, internal.buttonProps.onBlur)}
      {...props}
    >
      {enabled ? (
        <span
          ref={contentRef}
          data-slot="button-content"
          className="relative inline-grid grid-cols-[minmax(0,1fr)] place-items-center gap-[inherit] overflow-x-clip *:col-start-1 *:row-start-1 motion-safe:data-measured:transition-[width] motion-safe:data-measured:duration-220 motion-safe:data-measured:ease-[cubic-bezier(0.2,0,0,1)]"
        >
          {statuses.map((layer) => {
            const active = layer === status
            const exiting = layer === leaving
            const namesButton =
              layer === "idle" && isLoading && loadingLabel === undefined

            return (
              <span
                key={active ? `${layer}-${transitionKey}` : layer}
                ref={layerRef}
                data-slot="button-layer"
                data-layer={layer}
                aria-hidden={active || namesButton ? undefined : true}
                className={cn(
                  "inline-flex items-center justify-center gap-[inherit] backface-hidden",
                  !active &&
                    !exiting &&
                    "pointer-events-none absolute opacity-0",
                  active &&
                    transitionKey > 0 &&
                    "origin-bottom motion-safe:animate-button-state-in motion-reduce:animate-in motion-reduce:animation-duration-150 motion-reduce:fade-in-0",
                  exiting &&
                    "pointer-events-none origin-top opacity-0 motion-safe:animate-button-state-out"
                )}
              >
                {layerContent(layer, active || exiting)}
              </span>
            )
          })}
        </span>
      ) : (
        children
      )}
    </ButtonPrimitive>
  )

  if (!enabled) {
    return button
  }

  const announcement =
    status === "loading"
      ? typeof loadingLabel === "string"
        ? loadingLabel
        : "Loading"
      : status === "success"
        ? typeof successLabel === "string"
          ? successLabel
          : "Done"
        : status === "error"
          ? typeof resolvedErrorLabel === "string"
            ? resolvedErrorLabel
            : "Failed"
          : ""

  return (
    <>
      {button}
      <span data-slot="button-status" role="status" className="sr-only">
        {announcement}
      </span>
    </>
  )
}

export { Button, buttonVariants, useButtonFeedback }
export type { ButtonProps, ButtonStatus, ButtonFeedbackOptions }
```

```ts title="lib/motion.ts"
import * as React from "react"

const easeOut = "cubic-bezier(0.23, 1, 0.32, 1)"
const easeInOut = "cubic-bezier(0.77, 0, 0.175, 1)"
const easeSpring =
  "linear(0, 0.015 2%, 0.0532 4%, 0.1065 6%, 0.1686 8%, 0.2351 10%, 0.3363 13%, 0.4332 16%, 0.5495 20%, 0.648 24%, 0.7287 28%, 0.807 33%, 0.8646 38%, 0.9128 44%, 0.9447 50%, 0.968 57%, 0.9834 65%, 0.9924 74%, 0.9975 85%, 1)"

const duration = {
  press: 100,
  release: 200,
  hover: 150,
  enter: 200,
  exit: 150,
  morph: 300,
} as const

function prefersReducedMotion() {
  return (
    typeof window === "undefined" ||
    typeof window.matchMedia !== "function" ||
    window.matchMedia("(prefers-reduced-motion: reduce)").matches
  )
}

type SizeAxis = "width" | "height"

type SizeMorphOptions = {
  axis: SizeAxis
  enabled?: boolean
  duration?: number
  easing?: string
}

function readSize(element: HTMLElement, axis: SizeAxis) {
  const value = parseFloat(getComputedStyle(element)[axis])
  if (Number.isFinite(value)) {
    return value
  }
  const rect = element.getBoundingClientRect()
  return axis === "width" ? rect.width : rect.height
}

function attachSizeMorph(
  element: HTMLElement,
  axis: SizeAxis,
  time: number,
  easing: string
) {
  if (
    typeof MutationObserver === "undefined" ||
    typeof element.animate !== "function"
  ) {
    return undefined
  }

  let settled = readSize(element, axis)
  let animation: Animation | null = null

  const morph = () => {
    const from = animation ? readSize(element, axis) : settled
    animation?.cancel()
    animation = null
    const to = readSize(element, axis)
    settled = to

    if (
      Math.abs(from - to) < 0.5 ||
      prefersReducedMotion() ||
      !element.isConnected ||
      element.getClientRects().length === 0
    ) {
      element.removeAttribute("data-morphing")
      return
    }

    element.setAttribute("data-morphing", "")
    const running = element.animate(
      [{ [axis]: `${from}px` }, { [axis]: `${to}px` }],
      { duration: time, easing }
    )
    animation = running
    running.onfinish = () => {
      if (animation === running) {
        animation = null
        element.removeAttribute("data-morphing")
        settled = readSize(element, axis)
      }
    }
  }

  const mutations = new MutationObserver(morph)
  mutations.observe(element, {
    childList: true,
    subtree: true,
    characterData: true,
  })

  const resize =
    typeof ResizeObserver === "undefined"
      ? null
      : new ResizeObserver(() => {
          if (!animation) {
            settled = readSize(element, axis)
          }
        })
  resize?.observe(element)

  return () => {
    mutations.disconnect()
    resize?.disconnect()
    animation?.cancel()
    element.removeAttribute("data-morphing")
  }
}

function useSizeMorph<T extends HTMLElement>({
  axis,
  enabled = true,
  duration: time = duration.morph,
  easing = easeOut,
}: SizeMorphOptions): React.RefCallback<T> {
  return React.useCallback(
    (element: T | null) => {
      if (!element || !enabled) {
        return undefined
      }
      return attachSizeMorph(element, axis, time, easing)
    },
    [axis, enabled, time, easing]
  )
}

function useSlidingHighlight(
  barRef: React.RefObject<HTMLElement | null>,
  highlightRef: React.RefObject<HTMLElement | null>,
  selector: string,
  attribute = "data-popup-open"
) {
  React.useLayoutEffect(() => {
    const bar = barRef.current
    const highlight = highlightRef.current
    if (!bar || !highlight) {
      return
    }

    let current: HTMLElement | null = null

    const place = (trigger: HTMLElement, instant: boolean) => {
      if (instant || prefersReducedMotion()) {
        highlight.setAttribute("data-instant", "")
      } else {
        highlight.removeAttribute("data-instant")
      }
      const frame = bar.getBoundingClientRect()
      const box = trigger.getBoundingClientRect()
      const scale = bar.offsetWidth > 0 ? frame.width / bar.offsetWidth : 1
      highlight.style.left = "0px"
      highlight.style.width = `${box.width / scale}px`
      highlight.style.height = `${box.height / scale}px`
      highlight.style.transform = `translate(${(box.left - frame.left) / scale - bar.clientLeft}px, ${(box.top - frame.top) / scale - bar.clientTop}px)`
    }

    const sync = () => {
      const trigger = bar.querySelector<HTMLElement>(selector)
      if (trigger === current) {
        if (trigger) {
          place(trigger, true)
        }
        return
      }
      const appearing = current === null
      current = trigger
      if (!trigger) {
        highlight.removeAttribute("data-visible")
        return
      }
      place(trigger, appearing)
      if (appearing) {
        void highlight.offsetWidth
      }
      highlight.setAttribute("data-visible", "")
    }

    sync()
    const mutations = new MutationObserver(sync)
    mutations.observe(bar, {
      subtree: true,
      childList: true,
      attributes: true,
      attributeFilter: [attribute],
    })
    const resize =
      typeof ResizeObserver === "undefined"
        ? null
        : new ResizeObserver(() => {
            if (current) {
              place(current, true)
            }
          })
    resize?.observe(bar)
    const onScroll = () => {
      if (current) {
        place(current, true)
      }
    }
    bar.addEventListener("scroll", onScroll, { capture: true, passive: true })

    return () => {
      mutations.disconnect()
      resize?.disconnect()
      bar.removeEventListener("scroll", onScroll, { capture: true })
    }
  }, [barRef, highlightRef, selector, attribute])
}

export {
  duration,
  easeInOut,
  easeOut,
  easeSpring,
  prefersReducedMotion,
  useSizeMorph,
  useSlidingHighlight,
}
export type { SizeMorphOptions }
```

Update the import paths to match your project setup.

## Usage

```tsx
import { DatePicker, DateRangePicker } from "@/components/ui/date-picker"
```

```tsx
const [date, setDate] = React.useState<Date | null>(null)

<DatePicker value={date} onValueChange={setDate} />
```

On screens narrower than 640px the calendar opens in a bottom sheet instead of a popover, so days stay large enough to tap.

## Examples

### Controlled

Pass `value` and `onValueChange`. Cleared values are `null`. The calendar always opens on the month of the selected date, and picking a day closes it and returns focus to the button.

```tsx title="components/examples/date-picker/controlled.tsx"
"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { DatePicker } from "@/components/ui/date-picker"

function daysFromToday(days: number) {
  const date = new Date()
  date.setHours(0, 0, 0, 0)
  date.setDate(date.getDate() + days)
  return date
}

const isoDay = new Intl.DateTimeFormat("en-CA", {
  year: "numeric",
  month: "2-digit",
  day: "2-digit",
})

export function DatePickerControlled() {
  const [date, setDate] = React.useState<Date | null>(null)

  return (
    <div className="flex flex-wrap items-center justify-center gap-2">
      <DatePicker value={date} onValueChange={setDate} clearable />
      <Button variant="ghost" onClick={() => setDate(daysFromToday(0))}>
        Today
      </Button>
      <Button variant="ghost" onClick={() => setDate(daysFromToday(7))}>
        In a week
      </Button>
      <output className="text-sm text-muted-foreground tabular-nums">
        {date ? isoDay.format(date) : "null"}
      </output>
    </div>
  )
}
```

### Range

`<DateRangePicker />` shows two months on larger screens. The first click always starts a new range, even when one is set, and the second click finishes it in either order and closes the picker.

```tsx title="components/examples/date-picker/range.tsx"
"use client"

import * as React from "react"
import type { DateRange } from "react-day-picker"

import { DateRangePicker } from "@/components/ui/date-picker"

const isoDay = new Intl.DateTimeFormat("en-CA", {
  year: "numeric",
  month: "2-digit",
  day: "2-digit",
})

export function DatePickerRange() {
  const [range, setRange] = React.useState<DateRange | null>(null)

  return (
    <div className="flex flex-col items-center gap-2">
      <DateRangePicker value={range} onValueChange={setRange} clearable />
      <output className="text-sm text-muted-foreground tabular-nums">
        {range?.from && range.to
          ? `${isoDay.format(range.from)} → ${isoDay.format(range.to)}`
          : "null"}
      </output>
    </div>
  )
}
```

### Disabled days

Pass any react-day-picker matcher as `calendarProps.disabled`. Here only future weekdays can be picked.

```tsx title="components/examples/date-picker/disabled-days.tsx"
"use client"

import * as React from "react"

import { DatePicker } from "@/components/ui/date-picker"

function tomorrow() {
  const date = new Date()
  date.setHours(0, 0, 0, 0)
  date.setDate(date.getDate() + 1)
  return date
}

export function DatePickerDisabledDays() {
  const [firstDay] = React.useState(tomorrow)

  return (
    <DatePicker
      placeholder="Delivery date"
      calendarProps={{
        disabled: [{ before: firstDay }, { dayOfWeek: [0, 6] }],
      }}
    />
  )
}
```

### Month and year dropdowns

For far-away dates like a birthday, set `captionLayout: "dropdown"` with a `startMonth` and `endMonth`. Label the trigger with a `<label />` pointing at its `id`.

```tsx title="components/examples/date-picker/dropdowns.tsx"
import { DatePicker } from "@/components/ui/date-picker"

export function DatePickerDropdowns() {
  return (
    <div className="flex flex-col gap-1.5">
      <label htmlFor="birthday" className="text-sm font-medium">
        Date of birth
      </label>
      <DatePicker
        id="birthday"
        placeholder="Select your birthday"
        calendarProps={{
          captionLayout: "dropdown",
          startMonth: new Date(1920, 0),
          endMonth: new Date(),
          disabled: { after: new Date() },
        }}
      />
    </div>
  )
}
```

### In a form

With `name`, a single date is submitted as `2026-10-14` and a range as `2026-10-20/2026-10-24`. Empty pickers submit an empty string.

```tsx title="components/examples/date-picker/form.tsx"
"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { DatePicker, DateRangePicker } from "@/components/ui/date-picker"

export function DatePickerForm() {
  const [submitted, setSubmitted] = React.useState<string>()

  return (
    <form
      className="flex flex-col items-start gap-3"
      onSubmit={(event) => {
        event.preventDefault()
        const data = new FormData(event.currentTarget)
        setSubmitted(JSON.stringify(Object.fromEntries(data)))
      }}
    >
      <DatePicker name="due" defaultValue={new Date(2026, 9, 14)} />
      <DateRangePicker
        name="stay"
        defaultValue={{
          from: new Date(2026, 9, 20),
          to: new Date(2026, 9, 24),
        }}
      />
      <Button type="submit">Submit</Button>
      <output className="text-sm break-all text-muted-foreground">
        {submitted ?? "Not submitted"}
      </output>
    </form>
  )
}
```

### Disabled

A disabled trigger can still show a value.

```tsx title="components/examples/date-picker/disabled.tsx"
import { DatePicker } from "@/components/ui/date-picker"

export function DatePickerDisabled() {
  return (
    <div className="flex flex-wrap justify-center gap-2">
      <DatePicker disabled />
      <DatePicker disabled defaultValue={new Date(2026, 0, 1)} />
    </div>
  )
}
```

### Narrow container

The trigger never grows past its container. Long labels truncate instead of wrapping.

```tsx title="components/examples/date-picker/narrow.tsx"
import { DateRangePicker } from "@/components/ui/date-picker"

export function DatePickerNarrow() {
  return (
    <div className="w-40">
      <DateRangePicker
        defaultValue={{
          from: new Date(2026, 0, 28),
          to: new Date(2026, 11, 3),
        }}
      />
    </div>
  )
}
```

### Inside a sheet

The picker layers above the sheet, and Escape closes only the picker.

```tsx title="components/examples/date-picker/in-sheet.tsx"
import { Button } from "@/components/ui/button"
import { DateRangePicker } from "@/components/ui/date-picker"
import {
  Sheet,
  SheetBody,
  SheetContent,
  SheetDescription,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet"

export function DatePickerInSheet() {
  return (
    <Sheet>
      <SheetTrigger render={<Button variant="outline" />}>
        Edit booking
      </SheetTrigger>
      <SheetContent>
        <SheetHeader>
          <SheetTitle>Edit booking</SheetTitle>
          <SheetDescription>
            The picker opens above the sheet and Escape closes only the picker.
          </SheetDescription>
        </SheetHeader>
        <SheetBody>
          <DateRangePicker />
        </SheetBody>
      </SheetContent>
    </Sheet>
  )
}
```

### Right to left

Pass `locale` for the trigger label and a react-day-picker locale with `dir: "rtl"` in `calendarProps` for the calendar. Arrow keys follow the direction.

```tsx title="components/examples/date-picker/rtl.tsx"
"use client"

import { arSA } from "react-day-picker/locale"

import { DatePicker, DateRangePicker } from "@/components/ui/date-picker"

export function DatePickerRtl() {
  return (
    <div dir="rtl" className="flex flex-wrap justify-center gap-2">
      <DatePicker
        locale="ar"
        placeholder="اختر تاريخًا"
        title="اختر تاريخًا"
        clearLabel="مسح"
        clearable
        defaultValue={new Date(2026, 9, 2)}
        calendarProps={{ locale: arSA, dir: "rtl" }}
      />
      <DateRangePicker
        locale="ar"
        placeholder="اختر فترة"
        title="اختر فترة"
        calendarProps={{ locale: arSA, dir: "rtl" }}
      />
    </div>
  )
}
```

## Keyboard

| Key | Action |
| --- | --- |
| `Enter` `Space` | On the trigger, opens the picker and focuses the selected day, or today. On a day, selects it. |
| `←` `→` | Moves to the previous or next day. |
| `↑` `↓` | Moves to the same day in the previous or next week. |
| `Page Up` `Page Down` | Moves to the previous or next month. |
| `Shift` `Page Up` | Moves to the previous year. Shift Page Down moves to the next. |
| `Home` `End` | Moves to the start or end of the week. |
| `Esc` | Closes the picker and returns focus to the trigger. |

## Accessibility

- The popover is labelled with `title`, and the bottom sheet shows it as a heading. Translate it along with `placeholder`.
- The trigger has no visible label of its own. Pair it with a `<label />` or pass `aria-label`.
- Disabled days are skipped by the keyboard and announced as unavailable.

## API reference

Both pickers share the props below and render a `<Button />` trigger with a `<Calendar />` in a popover or bottom sheet.

### DatePicker

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `Date \| null` | – |  |
| `defaultValue` | `Date \| null` | `null` |  |
| `onValueChange` | `(value: Date \| null) => void` | – |  |
| `placeholder` | `string` | `"Pick a date"` |  |
| `title` | `string` | `"Select a date"` | Accessible name of the popover, heading of the sheet. |
| `open` | `boolean` | – |  |
| `defaultOpen` | `boolean` | `false` |  |
| `onOpenChange` | `(open: boolean) => void` | – |  |
| `locale` | `string` | `"en-US"` | Formats the trigger label with Intl.DateTimeFormat. Pass a react-day-picker locale in calendarProps to translate the calendar too. |
| `clearable` | `boolean` | `false` | Adds a Clear button below the calendar once there is a value. |
| `clearLabel` | `string` | `"Clear"` |  |
| `name` | `string` | – | Renders a hidden input so the value is submitted with its form. |
| `calendarProps` | `CalendarProps` | – | Passed to the calendar, except mode, selected, onSelect, required, numberOfMonths and autoFocus. Use it for disabled days, captionLayout, startMonth, endMonth, locale and dir. |
| `variant` | `Button variant` | `"outline"` | The trigger is a Button, and accepts its other props too. |
| `disabled` | `boolean` | `false` |  |
| `className` | `string` | – | Applied to the trigger, which is w-60 by default. |

### DateRangePicker

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `{ from: Date; to?: Date } \| null` | – |  |
| `defaultValue` | `{ from: Date; to?: Date } \| null` | `null` |  |
| `onValueChange` | `(value: DateRange \| null) => void` | – | Called once the second day is picked. |
| `placeholder` | `string` | `"Pick a date range"` |  |
| `title` | `string` | `"Select dates"` |  |
| `open` | `boolean` | – |  |
| `defaultOpen` | `boolean` | `false` |  |
| `onOpenChange` | `(open: boolean) => void` | – |  |
| `locale` | `string` | `"en-US"` | Formats the trigger label with Intl.DateTimeFormat. Pass a react-day-picker locale in calendarProps to translate the calendar too. |
| `clearable` | `boolean` | `false` | Adds a Clear button below the calendar once there is a value. |
| `clearLabel` | `string` | `"Clear"` |  |
| `name` | `string` | – | Renders a hidden input so the value is submitted with its form. |
| `calendarProps` | `CalendarProps` | – | Passed to the calendar, except mode, selected, onSelect, required, numberOfMonths and autoFocus. Use it for disabled days, captionLayout, startMonth, endMonth, locale and dir. |
| `variant` | `Button variant` | `"outline"` | The trigger is a Button, and accepts its other props too. |
| `disabled` | `boolean` | `false` |  |
| `className` | `string` | – | Applied to the trigger, which is w-60 by default. |

### Data attributes

| Attribute | Description |
| --- | --- |
| `data-slot="date-picker-trigger"` | The trigger button. |
| `data-empty` | Present on the trigger while there is no value. Shows the placeholder in muted text. |
| `data-popup-open` | Present on the trigger while the picker is open. |
| `data-slot="date-picker-content"` | Wraps the calendar in the popover or sheet. |
| `data-slot="date-picker-clear"` | The Clear button. |

## Notes for AI assistants

- Install a component with the shadcn CLI: `npx shadcn@latest add https://hextaui.com/r/<name>.json`. It adds the source, the HextaUI theme tokens and any HextaUI components it depends on. `https://hextaui.com/r/all.json` installs every component.
- The code is then owned by the project, like shadcn/ui. There is no HextaUI npm package. HextaUI is MIT licensed and free for personal and commercial use.
- Behavior and accessibility come from Base UI (`@base-ui/react`). Compose with the `render` prop, not `asChild`.
- Styling uses Tailwind CSS v4 with theme tokens. Merge classes with `cn` from the `cn` package.
- Icons come from `@tabler/icons-react`.
- Import components from `@/components/ui/<name>`, hooks from `@/hooks/<name>` and utilities from `@/lib/<name>`.

Every HextaUI doc: https://hextaui.com/llms.txt
