# Calendar

A month-grid date picker for selecting a single date, several dates or a date range.

Calendar is an open-source component from suiss/ui for React, React Native, SwiftUI and Jetpack Compose. The @esuiss/ui CLI copies its source into your project, so you own the code.

Source: https://ui.suiss.app/docs/components/calendar

**React**

**Example**

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

function SingleCalendar(props: React.ComponentProps<typeof Calendar>) {
  const today = new Date()
  const [date, setDate] = React.useState<Date | undefined>(
    new Date(today.getFullYear(), today.getMonth(), today.getDate() + 3)
  )
  return (
    <Calendar
      {...props}
      mode="single"
      selected={date}
      onSelect={setDate}
      className="rounded-2xl border"
    />
  )
}

export default function CalendarDemo() {
  return <SingleCalendar buttonVariant="plain" />
}
```

`Calendar` is a styled wrapper around `DayPicker` from `react-day-picker`, so it accepts the full `DayPicker` API, including `mode`, `selected`, `onSelect`, `numberOfMonths`, `captionLayout`, `disabled` and `showWeekNumber`. It replaces the day cells with `CalendarDayButton`, which renders each day as a `Button` and styles today, single selections and range start, middle and end.

Use a calendar when people need to see dates in context, such as availability or a stay range. Place it inside a Popover to build a compact date picker; the calendar's background becomes transparent inside popover and card content.

## Installation

### CLI

```bash title="Terminal"
npx shadcn@latest add @esuiss/calendar
```

Works in any project with a `components.json`. See [Installation](https://ui.suiss.app/docs/installation#add-components) for the one-line registry setup. Or use the suiss/ui CLI:

```bash title="Terminal"
npx @esuiss/ui@latest add calendar
```

### Manual

1. Install the dependencies and add the [`cn` helper](https://ui.suiss.app/docs/installation#the-cn-helper):

```bash title="Terminal"
npm install @phosphor-icons/react react-day-picker
```

2. Add the pieces it builds on: [Button](https://ui.suiss.app/docs/components/button).

3. Copy the source into `components/ui/calendar.tsx`:

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

import {
  CaretDownIcon,
  CaretLeftIcon,
  CaretRightIcon,
} from "@phosphor-icons/react"
import * as React from "react"
import {
  type ChevronProps,
  type DayButtonProps,
  DayPicker,
  getDefaultClassNames,
  type RootProps,
  useDayPicker,
  type WeekNumberProps,
} from "react-day-picker"
import { enUS } from "react-day-picker/locale"
import { Button, buttonVariants } from "@/components/ui/button"
import { cn } from "@/lib/utils"

function Calendar({
  className,
  classNames,
  showOutsideDays = false,
  captionLayout = "label",
  buttonVariant = "plain",
  size = "default",
  locale = enUS,
  formatters,
  components,
  ...props
}: React.ComponentProps<typeof DayPicker> & {
  buttonVariant?: React.ComponentProps<typeof Button>["variant"]
  size?: "sm" | "default" | "lg"
}) {
  const defaultClassNames = getDefaultClassNames()

  return (
    <DayPicker
      data-size={size}
      showOutsideDays={showOutsideDays}
      className={cn(
        "group/calendar bg-background p-3 [--cell-radius:calc(infinity*1px)] [--cell-size:--spacing(8)] data-[size=lg]:p-4 data-[size=lg]:[--cell-size:--spacing(10)] data-[size=sm]:p-2 data-[size=sm]:[--cell-size:--spacing(6)] in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent",
        String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
        String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
        className
      )}
      captionLayout={captionLayout}
      locale={locale}
      formatters={{
        formatMonthDropdown: (date) =>
          date.toLocaleString(locale.code, { month: "short" }),
        ...formatters,
      }}
      classNames={{
        root: cn("w-fit", defaultClassNames.root),
        months: cn(
          "relative flex flex-col gap-4 md:flex-row",
          defaultClassNames.months
        ),
        month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
        nav: cn(
          "absolute end-0 top-0 flex items-center gap-0.5",
          defaultClassNames.nav
        ),
        button_previous: cn(
          buttonVariants({ variant: buttonVariant }),
          "size-(--cell-size) p-0 select-none aria-disabled:opacity-50",
          defaultClassNames.button_previous
        ),
        button_next: cn(
          buttonVariants({ variant: buttonVariant }),
          "size-(--cell-size) p-0 select-none aria-disabled:opacity-50",
          defaultClassNames.button_next
        ),
        month_caption: cn(
          "flex h-(--cell-size) w-full items-center justify-start ps-1.5 pe-[calc(var(--cell-size)*2)]",
          defaultClassNames.month_caption
        ),
        dropdowns: cn(
          "flex h-(--cell-size) w-full items-center justify-start gap-1.5 text-sm font-semibold group-data-[size=lg]/calendar:text-base group-data-[size=sm]/calendar:text-xs",
          defaultClassNames.dropdowns
        ),
        dropdown_root: cn(
          "relative rounded-(--cell-radius)",
          defaultClassNames.dropdown_root
        ),
        dropdown: cn(
          "absolute inset-0 bg-card opacity-0",
          defaultClassNames.dropdown
        ),
        caption_label: cn(
          "font-semibold select-none",
          "text-sm group-data-[size=lg]/calendar:text-base group-data-[size=sm]/calendar:text-xs",
          captionLayout === "label"
            ? undefined
            : "flex items-center gap-1 rounded-(--cell-radius) [&>svg]:size-3.5 [&>svg]:text-muted-foreground",
          defaultClassNames.caption_label
        ),
        month_grid: cn("w-full border-collapse", defaultClassNames.month_grid),
        weekdays: cn("flex", defaultClassNames.weekdays),
        weekday: cn(
          "flex-1 rounded-(--cell-radius) text-xs font-semibold text-muted-foreground uppercase select-none group-data-[size=lg]/calendar:text-xs group-data-[size=sm]/calendar:normal-case",
          defaultClassNames.weekday
        ),
        week: cn("mt-2 flex w-full", defaultClassNames.week),
        week_number_header: cn(
          "w-(--cell-size) select-none",
          defaultClassNames.week_number_header
        ),
        week_number: cn(
          "text-xs text-muted-foreground select-none",
          defaultClassNames.week_number
        ),
        day: cn(
          "relative aspect-square h-full w-full rounded-(--cell-radius) p-0 text-center select-none [&:last-child[data-selected=true]_button]:rounded-e-(--cell-radius)",
          props.showWeekNumber
            ? "[&:nth-child(2)[data-selected=true]_button]:rounded-s-(--cell-radius)"
            : "[&:first-child[data-selected=true]_button]:rounded-s-(--cell-radius)",
          defaultClassNames.day
        ),
        range_start: cn(
          "relative isolate z-0 rounded-s-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:end-0 after:w-4 after:bg-muted last:after:hidden",
          defaultClassNames.range_start
        ),
        range_middle: cn("rounded-none", defaultClassNames.range_middle),
        range_end: cn(
          "relative isolate z-0 rounded-e-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:start-0 after:w-4 after:bg-muted first:after:hidden",
          defaultClassNames.range_end
        ),
        today: cn("rounded-(--cell-radius)", defaultClassNames.today),
        outside: cn(
          "text-muted-foreground aria-selected:text-muted-foreground",
          defaultClassNames.outside
        ),
        disabled: cn("text-muted-foreground/50", defaultClassNames.disabled),
        hidden: cn("invisible", defaultClassNames.hidden),
        ...classNames,
      }}
      components={{
        Root: CalendarRoot,
        Chevron: CalendarChevron,
        DayButton: CalendarDayButton,
        WeekNumber: CalendarWeekNumber,
        ...components,
      }}
      {...props}
    />
  )
}

function CalendarRoot({ className, rootRef, ...props }: RootProps) {
  return (
    <div
      data-slot="calendar"
      ref={rootRef}
      className={cn(className)}
      {...props}
    />
  )
}

function CalendarChevron({ className, orientation, ...props }: ChevronProps) {
  if (orientation === "left") {
    return (
      <CaretLeftIcon
        className={cn("rtl:rotate-180 size-4", className)}
        {...props}
      />
    )
  }

  if (orientation === "right") {
    return (
      <CaretRightIcon
        className={cn("rtl:rotate-180 size-4", className)}
        {...props}
      />
    )
  }

  return (
    <CaretDownIcon className={cn("size-4", className)} {...props} />
  )
}

function CalendarWeekNumber({ children, week, ...props }: WeekNumberProps) {
  return (
    <td {...props}>
      <div className="flex size-(--cell-size) items-center justify-center text-center">
        {children}
      </div>
    </td>
  )
}

function CalendarDayButton({
  className,
  day,
  modifiers,
  ...props
}: DayButtonProps) {
  const defaultClassNames = getDefaultClassNames()
  const { locale } = useDayPicker().dayPickerProps

  const ref = React.useRef<HTMLButtonElement>(null)
  React.useEffect(() => {
    if (modifiers.focused) ref.current?.focus()
  }, [modifiers.focused])

  return (
    <Button
      ref={ref}
      variant="ghost"
      size="icon"
      data-day={day.date.toLocaleDateString(locale?.code)}
      data-selected-single={
        modifiers.selected &&
        !modifiers.range_start &&
        !modifiers.range_end &&
        !modifiers.range_middle
      }
      data-today={modifiers.today}
      data-range-start={modifiers.range_start}
      data-range-end={modifiers.range_end}
      data-range-middle={modifiers.range_middle}
      className={cn(
        "relative isolate z-10 flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 border-0 leading-none font-normal group-data-[size=lg]/calendar:text-lg group-data-[size=sm]/calendar:text-xs data-[today=true]:font-semibold data-[today=true]:text-primary data-[selected-single=true]:bg-primary/10 dark:data-[selected-single=true]:bg-primary/20 data-[selected-single=true]:font-semibold data-[selected-single=true]:text-primary hover:data-[selected-single=true]:bg-primary/20 dark:hover:data-[selected-single=true]:bg-primary/30 data-[today=true]:data-[selected-single=true]:bg-primary data-[today=true]:data-[selected-single=true]:text-primary-foreground hover:data-[today=true]:data-[selected-single=true]:bg-primary/80 data-[range-end=true]:rounded-(--cell-radius) data-[range-end=true]:rounded-e-(--cell-radius) data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-muted data-[range-middle=true]:text-foreground data-[range-start=true]:rounded-(--cell-radius) data-[range-start=true]:rounded-s-(--cell-radius) data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[today=true]:data-[range-start=true]:text-primary-foreground data-[today=true]:data-[range-end=true]:text-primary-foreground [&>span]:text-xs [&>span]:text-muted-foreground",
        defaultClassNames.day,
        className
      )}
      {...props}
    />
  )
}

export { Calendar, CalendarDayButton }
```

4. Update the import paths to match your project.

## Usage

```tsx
import { Calendar, CalendarDayButton } from "@/components/ui/calendar"
```

```tsx
<SingleCalendar buttonVariant="plain" />
```

Set `mode` to `single`, `multiple` or `range`, and control the selection with `selected` and `onSelect`. For ranges, `numberOfMonths={2}` shows two months side by side on wider screens.

Set `captionLayout` to `dropdown`, `dropdown-months` or `dropdown-years` to let people jump to a month or year directly; the default is `label`.

Unlike the `DayPicker` default, outside days are hidden unless you pass `showOutsideDays`. The default `locale` is `enUS`. Pass `disabled` with a matcher, such as `{ dayOfWeek: [0, 6] }`, to block days.

Use `CalendarDayButton` when you supply your own `components` and still want the library's day styling.

## Examples

### Sizes

Compares the `sm`, `default` and `lg` sizes.

**Example**

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

function SingleCalendar(props: React.ComponentProps<typeof Calendar>) {
  const today = new Date()
  const [date, setDate] = React.useState<Date | undefined>(
    new Date(today.getFullYear(), today.getMonth(), today.getDate() + 3)
  )
  return (
    <Calendar
      {...props}
      mode="single"
      selected={date}
      onSelect={setDate}
      className="rounded-2xl border"
    />
  )
}

export default function CalendarSizes() {
  return (
    <div className="flex flex-wrap items-start gap-6">
      <SingleCalendar buttonVariant="plain" size="sm" />
      <SingleCalendar buttonVariant="plain" size="default" />
      <SingleCalendar buttonVariant="plain" size="lg" />
    </div>
  )
}
```

### Range

Selects a seven-day range across two months with `mode="range"`.

**Example**

```tsx
import * as React from "react"
import type { DateRange } from "react-day-picker"
import { Calendar } from "@/components/ui/calendar"

function RangeCalendar(props: React.ComponentProps<typeof Calendar>) {
  const today = new Date()
  const [range, setRange] = React.useState<DateRange | undefined>({
    from: today,
    to: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 6),
  })
  return (
    <Calendar
      {...props}
      mode="range"
      selected={range}
      onSelect={setRange}
      numberOfMonths={2}
      className="rounded-2xl border"
    />
  )
}

export default function CalendarRange() {
  return <RangeCalendar buttonVariant="plain" />
}
```

### Dropdown caption

Replaces the month label with month and year dropdowns.

**Example**

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

function SingleCalendar(props: React.ComponentProps<typeof Calendar>) {
  const today = new Date()
  const [date, setDate] = React.useState<Date | undefined>(
    new Date(today.getFullYear(), today.getMonth(), today.getDate() + 3)
  )
  return (
    <Calendar
      {...props}
      mode="single"
      selected={date}
      onSelect={setDate}
      className="rounded-2xl border"
    />
  )
}

export default function CalendarDropdownCaption() {
  return <SingleCalendar buttonVariant="plain" captionLayout="dropdown" />
}
```

### Week numbers

Shows the week number at the start of each row.

**Example**

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

function SingleCalendar(props: React.ComponentProps<typeof Calendar>) {
  const today = new Date()
  const [date, setDate] = React.useState<Date | undefined>(
    new Date(today.getFullYear(), today.getMonth(), today.getDate() + 3)
  )
  return (
    <Calendar
      {...props}
      mode="single"
      selected={date}
      onSelect={setDate}
      className="rounded-2xl border"
    />
  )
}

export default function CalendarWeekNumbers() {
  return <SingleCalendar buttonVariant="plain" showWeekNumber />
}
```

### Disabled days

Disables weekends with a `dayOfWeek` matcher.

**Example**

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

function SingleCalendar(props: React.ComponentProps<typeof Calendar>) {
  const today = new Date()
  const [date, setDate] = React.useState<Date | undefined>(
    new Date(today.getFullYear(), today.getMonth(), today.getDate() + 3)
  )
  return (
    <Calendar
      {...props}
      mode="single"
      selected={date}
      onSelect={setDate}
      className="rounded-2xl border"
    />
  )
}

export default function CalendarDisabledDays() {
  return (
    <SingleCalendar buttonVariant="plain" disabled={{ dayOfWeek: [0, 6] }} />
  )
}
```

## API Reference

### Calendar

The date grid, with month navigation and caption.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `buttonVariant` | `React.ComponentProps<typeof Button>["variant"]` | `"plain"` | `Button` variant used for the previous and next month buttons. Defaults to `plain`. |
| `size` | `"sm" \| "default" \| "lg"` | `"default"` | Cell size and type scale: `sm`, `default` or `lg`. |

Also accepts the props of `React.ComponentProps<typeof DayPicker>`.

### CalendarDayButton

The button rendered for each day, styled by its selection state.

Also accepts the props of `DayButtonProps`.

## Accessibility

- Each month renders as a `grid` labelled with its month and year; selected days set `aria-selected` on their cell.
- Arrow keys move focus between days, and clicking or pressing a day selects it.
- The navigation buttons are labelled for the next and previous month.
- Disabled days render disabled buttons.
- Caption dropdowns are native `select` elements labelled for month and year.

## Notes

- Styling hooks: `data-slot="calendar"` and `data-size` on the root; day buttons expose `data-day`, `data-today`, `data-selected-single`, `data-range-start`, `data-range-middle` and `data-range-end`.
- Cell size and radius come from the `--cell-size` and `--cell-radius` custom properties, which `size` adjusts.
- Pass `classNames` to override any `react-day-picker` class key; your values replace the defaults for that key.
- Navigation chevrons flip in right-to-left layouts.

**React Native, SwiftUI, Jetpack Compose**

![Preview](https://ui.suiss.app/previews/components-calendar--default.light.webp)

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

type CalendarProps = React.ComponentProps<typeof Calendar>

function SingleCalendar(
  props: Omit<CalendarProps, "mode" | "selected" | "onSelect">
) {
  const today = new Date()
  const [date, setDate] = React.useState<Date | undefined>(
    new Date(today.getFullYear(), today.getMonth(), today.getDate() + 3)
  )
  return (
    <Calendar
      {...props}
      mode="single"
      selected={date}
      onSelect={setDate}
      className="rounded-2xl border border-border"
    />
  )
}

export function CalendarExample() {
  return (
    <SingleCalendar />
  )
}
```

```swift
import EsuissUI
import SwiftUI

enum CalendarDates {
  static func day(_ offset: Int) -> Date {
    let calendar = Foundation.Calendar.current
    let start = calendar.startOfDay(for: Date())
    return calendar.date(byAdding: .day, value: offset, to: start) ?? start
  }

  static func dayOfMonth(_ day: Int) -> Date {
    let calendar = Foundation.Calendar.current
    var components = calendar.dateComponents([.year, .month], from: Date())
    components.day = day
    return calendar.date(from: components) ?? Date()
  }
}

struct CalendarBordered: ViewModifier {
  func body(content: Content) -> some View {
    content
      .clipShape(RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous))
      .overlay {
        RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous).strokeBorder(EsuissColor.border)
      }
  }
}

struct CalendarSingleDemo: View {
  var size: DateCalendar.Size = .default
  var buttonVariant: VariantButtonStyle.Variant = .plain
  var captionLayout: DateCalendar.CaptionLayout = .label
  var showOutsideDays = false
  var showWeekNumber = false
  var weekendsDisabled = false

  @State private var date: Date? = CalendarDates.day(3)

  var body: some View {
    DateCalendar(
      selection: $date,
      size: size,
      buttonVariant: buttonVariant,
      captionLayout: captionLayout,
      showOutsideDays: showOutsideDays,
      showWeekNumber: showWeekNumber,
      isDisabled: { day in
        guard weekendsDisabled else { return false }
        let weekday = Foundation.Calendar.current.component(.weekday, from: day)
        return weekday == 1 || weekday == 7
      }
    )
    .modifier(CalendarBordered())
  }
}

func day(_ offset: Int) -> Date {
  let calendar = Foundation.Calendar.current
  let start = calendar.startOfDay(for: Date())
  return calendar.date(byAdding: .day, value: offset, to: start) ?? start
}

func dayOfMonth(_ day: Int) -> Date {
  let calendar = Foundation.Calendar.current
  var components = calendar.dateComponents([.year, .month], from: Date())
  components.day = day
  return calendar.date(from: components) ?? Date()
}
```

```kotlin
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Calendar
import com.esuiss.ui.components.CalendarCaptionLayout
import com.esuiss.ui.components.CalendarSize
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Radius
import java.util.Date
import java.util.GregorianCalendar
import java.util.Locale

fun calendarDay(offsetDays: Int = 0, dayOfMonth: Int? = null): Date {
    val now = GregorianCalendar()
    val result = GregorianCalendar(now.get(GregorianCalendar.YEAR), now.get(GregorianCalendar.MONTH), now.get(GregorianCalendar.DAY_OF_MONTH))
    if (dayOfMonth != null) result.set(GregorianCalendar.DAY_OF_MONTH, dayOfMonth)
    result.add(GregorianCalendar.DAY_OF_MONTH, offsetDays)
    return result.time
}

fun calendarIsWeekend(date: Date): Boolean {
    val day = GregorianCalendar().apply { time = date }.get(GregorianCalendar.DAY_OF_WEEK)
    return day == GregorianCalendar.SATURDAY || day == GregorianCalendar.SUNDAY
}

@Composable
fun CalendarFrame(content: @Composable () -> Unit) {
    Box(
        Modifier
            .border(1.dp, EsuissTheme.colors.border, RoundedCornerShape(Radius.xl2))
            .padding(1.dp),
    ) {
        content()
    }
}

@Composable
fun CalendarSingle(
    size: CalendarSize = CalendarSize.Default,
    captionLayout: CalendarCaptionLayout = CalendarCaptionLayout.Label,
    showWeekNumber: Boolean = false,
    showOutsideDays: Boolean = false,
    weekendsDisabled: Boolean = false,
    buttonVariant: ButtonVariant = ButtonVariant.Plain,
    locale: Locale = Locale.getDefault(),
    key: String = "",
) {
    var date by rememberSaveable(key) { mutableStateOf<Date?>(calendarDay(offsetDays = 3)) }
    CalendarFrame {
        Calendar(
            selected = date,
            onSelect = { date = it },
            size = size,
            captionLayout = captionLayout,
            showWeekNumber = showWeekNumber,
            showOutsideDays = showOutsideDays,
            disabled = { weekendsDisabled && calendarIsWeekend(it) },
            buttonVariant = buttonVariant,
            locale = locale,
        )
    }
}
```

`Calendar` renders one or more month grids with a caption, previous and next buttons, weekday headers and pressable day cells. Selection mode is set with `mode`: `single`, `multiple` or `range`. Ranges are drawn as a continuous shaded band with filled start and end days.

Three sizes are available: `sm`, `default` and `lg`. The caption can be a plain label or month and year dropdowns built on Select, which suits birth dates and other dates far from today.

## Installation

**React Native**

```bash title="Terminal"
npx shadcn@latest add @esuiss-rn/calendar
```

See [Installation](https://ui.suiss.app/docs/installation) for the `components.json` setup. Or use the suiss/ui CLI:

```bash title="Terminal"
npx @esuiss/ui@latest add calendar
```

**SwiftUI**

```bash title="Terminal"
npx @esuiss/ui@latest add calendar --platform swift
```

**Jetpack Compose**

```bash title="Terminal"
npx @esuiss/ui@latest add calendar --platform kotlin
```

The CLI also adds what it builds on: [Button](https://ui.suiss.app/docs/components/button), [Icon](https://ui.suiss.app/docs/components/icon), [Select](https://ui.suiss.app/docs/components/select), [Text](https://ui.suiss.app/docs/components/text).

### Manual

**React Native**

**Uniwind**

1. Install the dependencies:

```bash title="Terminal"
npx expo install cn phosphor-react-native react-native-svg
```

2. Add the pieces it builds on: [Button](https://ui.suiss.app/docs/components/button), [Icon](https://ui.suiss.app/docs/components/icon), [Select](https://ui.suiss.app/docs/components/select), [Text](https://ui.suiss.app/docs/components/text), and the primitives `components/primitives/calendar.tsx`, `components/primitives/use-render.tsx`.

3. Copy the source into `components/ui/calendar.tsx`:

```tsx title="uniwind/components/ui/calendar.tsx"
import { cn } from "cn"
import { CaretLeftIcon, CaretRightIcon } from "phosphor-react-native"
import * as React from "react"
import { I18nManager, ScrollView, View } from "react-native"
import * as CalendarPrimitive from "@/components/primitives/calendar"
import { resolveClassName } from "@/components/primitives/use-render"
import { Button, type ButtonProps } from "@/components/ui/button"
import { Icon } from "@/components/ui/icon"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
import { Text, TextClassContext } from "@/components/ui/text"

type CalendarSize = "sm" | "default" | "lg"

type CaptionLayout = "label" | "dropdown" | "dropdown-months" | "dropdown-years"

const CELL: Record<CalendarSize, string> = {
  sm: "size-6",
  default: "size-8",
  lg: "size-10",
}

const CELL_WIDTH: Record<CalendarSize, string> = {
  sm: "w-6",
  default: "w-8",
  lg: "w-10",
}

const CELL_HEIGHT: Record<CalendarSize, string> = {
  sm: "h-6",
  default: "h-8",
  lg: "h-10",
}

const PADDING: Record<CalendarSize, string> = {
  sm: "p-2",
  default: "p-3",
  lg: "p-4",
}

const CAPTION_TEXT: Record<CalendarSize, string> = {
  sm: "text-xs",
  default: "text-sm",
  lg: "text-base",
}

const DAY_TEXT: Record<CalendarSize, string> = {
  sm: "text-xs",
  default: "text-sm",
  lg: "text-lg",
}

const WEEKDAY_TEXT: Record<CalendarSize, string> = {
  sm: "text-xs",
  default: "text-xs uppercase",
  lg: "text-xs uppercase",
}

const WEEKDAY_FORMAT: Record<CalendarSize, "narrow" | "short"> = {
  sm: "narrow",
  default: "short",
  lg: "short",
}

const CalendarContext = React.createContext<{ size: CalendarSize }>({
  size: "default",
})

type DropdownOption = {
  value: string
  label: string
  disabled?: boolean
}

function CalendarDropdown({
  value,
  options,
  label,
  onChange,
}: {
  value: string
  options: DropdownOption[]
  label: string
  onChange: (value: string) => void
}) {
  const { size } = React.useContext(CalendarContext)
  return (
    <Select
      items={options}
      value={value}
      onValueChange={(next) => {
        if (typeof next === "string") onChange(next)
      }}
    >
      <SelectTrigger
        size="sm"
        accessibilityLabel={label}
        className="h-auto gap-1 rounded-full bg-transparent px-1.5 py-0.5"
      >
        <SelectValue
          className={cn("flex-none font-semibold", CAPTION_TEXT[size])}
        />
      </SelectTrigger>
      <SelectContent>
        <ScrollView className="max-h-72">
          {options.map((option) => (
            <SelectItem
              key={option.value}
              value={option.value}
              label={option.label}
              disabled={option.disabled}
            >
              {option.label}
            </SelectItem>
          ))}
        </ScrollView>
      </SelectContent>
    </Select>
  )
}

function CalendarCaption({
  month,
  captionLayout,
}: {
  month: Date
  captionLayout: CaptionLayout
}) {
  const { size } = React.useContext(CalendarContext)
  const calendar = CalendarPrimitive.useRootContext()
  const year = month.getFullYear()

  if (captionLayout === "label") {
    return (
      <Text className={cn("font-semibold text-foreground", CAPTION_TEXT[size])}>
        {calendar.formatCaption(month)}
      </Text>
    )
  }

  const firstYear = calendar.startMonth?.getFullYear() ?? year
  const lastYear = calendar.endMonth?.getFullYear() ?? year
  const outOfRange = (target: Date) =>
    (calendar.startMonth !== undefined &&
      target.getTime() < calendar.startMonth.getTime()) ||
    (calendar.endMonth !== undefined &&
      target.getTime() > calendar.endMonth.getTime())

  const months = Array.from({ length: 12 }, (_, index) => {
    const date = new Date(year, index, 1)
    return {
      value: String(index),
      label: calendar.formatMonth(date, "short"),
      disabled: outOfRange(date),
    }
  })
  const years = Array.from(
    { length: Math.max(lastYear - firstYear + 1, 1) },
    (_, index) => ({
      value: String(firstYear + index),
      label: String(firstYear + index),
    })
  )
  const captionText = cn("font-semibold text-foreground", CAPTION_TEXT[size])

  return (
    <View className="flex-row items-center gap-1.5">
      {captionLayout === "dropdown-years" ? (
        <Text className={captionText}>
          {calendar.formatMonth(month, "long")}
        </Text>
      ) : (
        <CalendarDropdown
          label="Choose the Month"
          value={String(month.getMonth())}
          options={months}
          onChange={(next) =>
            calendar.goToMonth(
              new Date(year, Number(next), 1),
              CalendarPrimitive.CALENDAR_REASONS.monthSelect
            )
          }
        />
      )}
      {captionLayout === "dropdown-months" ? (
        <Text className={captionText}>{year}</Text>
      ) : (
        <CalendarDropdown
          label="Choose the Year"
          value={String(year)}
          options={years}
          onChange={(next) =>
            calendar.goToMonth(
              new Date(Number(next), month.getMonth(), 1),
              CalendarPrimitive.CALENDAR_REASONS.yearSelect
            )
          }
        />
      )}
    </View>
  )
}

function CalendarNav({
  buttonVariant,
}: {
  buttonVariant: ButtonProps["variant"]
}) {
  const { size } = React.useContext(CalendarContext)
  const calendar = CalendarPrimitive.useRootContext()
  const rtl = I18nManager.isRTL
  return (
    <View className="flex-row items-center gap-0.5">
      <CalendarPrimitive.Previous
        render={
          <Button
            variant={buttonVariant}
            size="icon"
            className={cn("p-0", CELL[size])}
          />
        }
      >
        <Icon
          as={rtl ? CaretRightIcon : CaretLeftIcon}
          className={cn("size-4", !calendar.previousMonth && "opacity-50")}
        />
      </CalendarPrimitive.Previous>
      <CalendarPrimitive.Next
        render={
          <Button
            variant={buttonVariant}
            size="icon"
            className={cn("p-0", CELL[size])}
          />
        }
      >
        <Icon
          as={rtl ? CaretLeftIcon : CaretRightIcon}
          className={cn("size-4", !calendar.nextMonth && "opacity-50")}
        />
      </CalendarPrimitive.Next>
    </View>
  )
}

function CalendarDayButton({
  className,
  day,
  children,
  ...props
}: React.ComponentProps<typeof CalendarPrimitive.DayButton>) {
  const { size } = React.useContext(CalendarContext)
  const modifiers = day.modifiers
  const single =
    modifiers.selected &&
    !modifiers.rangeStart &&
    !modifiers.rangeEnd &&
    !modifiers.rangeMiddle
  const filled =
    (single && modifiers.today) || modifiers.rangeStart || modifiers.rangeEnd
  return (
    <TextClassContext.Provider
      value={cn(
        "text-foreground",
        DAY_TEXT[size],
        modifiers.outside && "text-muted-foreground",
        (modifiers.today || single) && "font-semibold text-primary",
        modifiers.rangeMiddle && "text-foreground",
        filled && "text-primary-foreground",
        modifiers.disabled && "opacity-50"
      )}
    >
      <CalendarPrimitive.DayButton
        day={day}
        className={(state) =>
          cn(
            "items-center justify-center rounded-full active:opacity-80",
            CELL[size],
            single && "bg-primary/10 dark:bg-primary/20 active:opacity-80",
            filled && "bg-primary active:opacity-80",
            modifiers.rangeMiddle && "rounded-none",
            resolveClassName(className, state)
          )
        }
        {...props}
      >
        {children ?? <Text>{day.date.getDate()}</Text>}
      </CalendarPrimitive.DayButton>
    </TextClassContext.Provider>
  )
}

function CalendarMonth({
  month,
  index,
  buttonVariant,
  captionLayout,
  showWeekNumber,
}: {
  month: Date
  index: number
  buttonVariant: ButtonProps["variant"]
  captionLayout: CaptionLayout
  showWeekNumber: boolean
}) {
  const { size } = React.useContext(CalendarContext)
  const calendar = CalendarPrimitive.useRootContext()
  return (
    <View className="gap-4">
      <View className={cn("flex-row items-center", CELL_HEIGHT[size])}>
        <View className="flex-1 flex-row items-center ps-1.5">
          <CalendarCaption month={month} captionLayout={captionLayout} />
        </View>
        {index === 0 ? <CalendarNav buttonVariant={buttonVariant} /> : null}
      </View>
      <View role="grid" accessibilityLabel={calendar.formatCaption(month)}>
        <View className="flex-row">
          {showWeekNumber ? <View className={CELL_WIDTH[size]} /> : null}
          {calendar.weekdays.map((weekday) => (
            <View
              key={weekday.getDay()}
              accessibilityLabel={calendar.formatWeekday(weekday, "long")}
              className={cn("items-center", CELL_WIDTH[size])}
            >
              <Text
                numberOfLines={1}
                className={cn(
                  "font-semibold text-muted-foreground",
                  WEEKDAY_TEXT[size]
                )}
              >
                {calendar.formatWeekday(weekday, WEEKDAY_FORMAT[size])}
              </Text>
            </View>
          ))}
        </View>
        {calendar.getWeeks(month).map((week) => (
          <View key={week.days[0]?.date.getTime()} className="mt-2 flex-row">
            {showWeekNumber ? (
              <View className={cn("items-center justify-center", CELL[size])}>
                <Text className="text-xs text-muted-foreground">
                  {week.number}
                </Text>
              </View>
            ) : null}
            {week.days.map((day, dayIndex) => {
              const modifiers = day.modifiers
              const shaded =
                modifiers.rangeMiddle ||
                modifiers.rangeStart !== modifiers.rangeEnd
              return (
                <View
                  key={day.date.getTime()}
                  className={cn(
                    CELL[size],
                    !day.hidden && shaded && "bg-muted",
                    modifiers.rangeStart && "rounded-l-full",
                    modifiers.rangeEnd && "rounded-r-full",
                    modifiers.rangeMiddle && dayIndex === 0 && "rounded-l-full",
                    modifiers.rangeMiddle && dayIndex === 6 && "rounded-r-full"
                  )}
                >
                  {day.hidden ? null : <CalendarDayButton day={day} />}
                </View>
              )
            })}
          </View>
        ))}
      </View>
    </View>
  )
}

function CalendarMonths({
  buttonVariant,
  captionLayout,
  showWeekNumber,
}: {
  buttonVariant: ButtonProps["variant"]
  captionLayout: CaptionLayout
  showWeekNumber: boolean
}) {
  const calendar = CalendarPrimitive.useRootContext()
  return (
    <View className="gap-4">
      {calendar.months.map((month, index) => (
        <CalendarMonth
          key={month.getTime()}
          month={month}
          index={index}
          buttonVariant={buttonVariant}
          captionLayout={captionLayout}
          showWeekNumber={showWeekNumber}
        />
      ))}
    </View>
  )
}

function Calendar({
  className,
  size = "default",
  buttonVariant = "plain",
  captionLayout = "label",
  showWeekNumber = false,
  startMonth,
  endMonth,
  ...props
}: React.ComponentProps<typeof CalendarPrimitive.Root> & {
  buttonVariant?: ButtonProps["variant"]
  size?: CalendarSize
  captionLayout?: CaptionLayout
  showWeekNumber?: boolean
}) {
  const yearDropdown =
    captionLayout === "dropdown" || captionLayout === "dropdown-years"
  const thisYear = new Date().getFullYear()
  const context = React.useMemo(() => ({ size }), [size])
  return (
    <CalendarContext.Provider value={context}>
      <CalendarPrimitive.Root
        startMonth={
          startMonth ?? (yearDropdown ? new Date(thisYear - 100, 0) : undefined)
        }
        endMonth={
          endMonth ?? (yearDropdown ? new Date(thisYear, 11) : undefined)
        }
        className={(state) =>
          cn(
            "self-start bg-background",
            PADDING[size],
            resolveClassName(className, state)
          )
        }
        {...props}
      >
        <CalendarMonths
          buttonVariant={buttonVariant}
          captionLayout={captionLayout}
          showWeekNumber={showWeekNumber}
        />
      </CalendarPrimitive.Root>
    </CalendarContext.Provider>
  )
}

export { Calendar, CalendarDayButton }
```

4. Update the import paths to match your project.

**NativeWind**

1. Install the dependencies:

```bash title="Terminal"
npx expo install cn phosphor-react-native react-native-svg
```

2. Add the pieces it builds on: [Button](https://ui.suiss.app/docs/components/button), [Icon](https://ui.suiss.app/docs/components/icon), [Select](https://ui.suiss.app/docs/components/select), [Text](https://ui.suiss.app/docs/components/text), and the primitives `components/primitives/calendar.tsx`, `components/primitives/use-render.tsx`.

3. Copy the source into `components/ui/calendar.tsx`:

```tsx title="nativewind/components/ui/calendar.tsx"
import { cn } from "cn"
import { CaretLeftIcon, CaretRightIcon } from "phosphor-react-native"
import * as React from "react"
import { I18nManager, ScrollView, View } from "react-native"
import * as CalendarPrimitive from "@/components/primitives/calendar"
import { resolveClassName } from "@/components/primitives/use-render"
import { Button, type ButtonProps } from "@/components/ui/button"
import { Icon } from "@/components/ui/icon"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
import { Text, TextClassContext } from "@/components/ui/text"

type CalendarSize = "sm" | "default" | "lg"

type CaptionLayout = "label" | "dropdown" | "dropdown-months" | "dropdown-years"

const CELL: Record<CalendarSize, string> = {
  sm: "size-6",
  default: "size-8",
  lg: "size-10",
}

const CELL_WIDTH: Record<CalendarSize, string> = {
  sm: "w-6",
  default: "w-8",
  lg: "w-10",
}

const CELL_HEIGHT: Record<CalendarSize, string> = {
  sm: "h-6",
  default: "h-8",
  lg: "h-10",
}

const PADDING: Record<CalendarSize, string> = {
  sm: "p-2",
  default: "p-3",
  lg: "p-4",
}

const CAPTION_TEXT: Record<CalendarSize, string> = {
  sm: "text-xs",
  default: "text-sm",
  lg: "text-base",
}

const DAY_TEXT: Record<CalendarSize, string> = {
  sm: "text-xs",
  default: "text-sm",
  lg: "text-lg",
}

const WEEKDAY_TEXT: Record<CalendarSize, string> = {
  sm: "text-xs",
  default: "text-xs uppercase",
  lg: "text-xs uppercase",
}

const WEEKDAY_FORMAT: Record<CalendarSize, "narrow" | "short"> = {
  sm: "narrow",
  default: "short",
  lg: "short",
}

const CalendarContext = React.createContext<{ size: CalendarSize }>({
  size: "default",
})

type DropdownOption = {
  value: string
  label: string
  disabled?: boolean
}

function CalendarDropdown({
  value,
  options,
  label,
  onChange,
}: {
  value: string
  options: DropdownOption[]
  label: string
  onChange: (value: string) => void
}) {
  const { size } = React.useContext(CalendarContext)
  return (
    <Select
      items={options}
      value={value}
      onValueChange={(next) => {
        if (typeof next === "string") onChange(next)
      }}
    >
      <SelectTrigger
        size="sm"
        accessibilityLabel={label}
        className="h-auto gap-1 rounded-full bg-transparent px-1.5 py-0.5"
      >
        <SelectValue
          className={cn("flex-none font-semibold", CAPTION_TEXT[size])}
        />
      </SelectTrigger>
      <SelectContent>
        <ScrollView className="max-h-72">
          {options.map((option) => (
            <SelectItem
              key={option.value}
              value={option.value}
              label={option.label}
              disabled={option.disabled}
            >
              {option.label}
            </SelectItem>
          ))}
        </ScrollView>
      </SelectContent>
    </Select>
  )
}

function CalendarCaption({
  month,
  captionLayout,
}: {
  month: Date
  captionLayout: CaptionLayout
}) {
  const { size } = React.useContext(CalendarContext)
  const calendar = CalendarPrimitive.useRootContext()
  const year = month.getFullYear()

  if (captionLayout === "label") {
    return (
      <Text className={cn("font-semibold text-foreground", CAPTION_TEXT[size])}>
        {calendar.formatCaption(month)}
      </Text>
    )
  }

  const firstYear = calendar.startMonth?.getFullYear() ?? year
  const lastYear = calendar.endMonth?.getFullYear() ?? year
  const outOfRange = (target: Date) =>
    (calendar.startMonth !== undefined &&
      target.getTime() < calendar.startMonth.getTime()) ||
    (calendar.endMonth !== undefined &&
      target.getTime() > calendar.endMonth.getTime())

  const months = Array.from({ length: 12 }, (_, index) => {
    const date = new Date(year, index, 1)
    return {
      value: String(index),
      label: calendar.formatMonth(date, "short"),
      disabled: outOfRange(date),
    }
  })
  const years = Array.from(
    { length: Math.max(lastYear - firstYear + 1, 1) },
    (_, index) => ({
      value: String(firstYear + index),
      label: String(firstYear + index),
    })
  )
  const captionText = cn("font-semibold text-foreground", CAPTION_TEXT[size])

  return (
    <View className="flex-row items-center gap-1.5">
      {captionLayout === "dropdown-years" ? (
        <Text className={captionText}>
          {calendar.formatMonth(month, "long")}
        </Text>
      ) : (
        <CalendarDropdown
          label="Choose the Month"
          value={String(month.getMonth())}
          options={months}
          onChange={(next) =>
            calendar.goToMonth(
              new Date(year, Number(next), 1),
              CalendarPrimitive.CALENDAR_REASONS.monthSelect
            )
          }
        />
      )}
      {captionLayout === "dropdown-months" ? (
        <Text className={captionText}>{year}</Text>
      ) : (
        <CalendarDropdown
          label="Choose the Year"
          value={String(year)}
          options={years}
          onChange={(next) =>
            calendar.goToMonth(
              new Date(Number(next), month.getMonth(), 1),
              CalendarPrimitive.CALENDAR_REASONS.yearSelect
            )
          }
        />
      )}
    </View>
  )
}

function CalendarNav({
  buttonVariant,
}: {
  buttonVariant: ButtonProps["variant"]
}) {
  const { size } = React.useContext(CalendarContext)
  const calendar = CalendarPrimitive.useRootContext()
  const rtl = I18nManager.isRTL
  return (
    <View className="flex-row items-center gap-0.5">
      <CalendarPrimitive.Previous
        render={
          <Button
            variant={buttonVariant}
            size="icon"
            className={cn("p-0", CELL[size])}
          />
        }
      >
        <Icon
          as={rtl ? CaretRightIcon : CaretLeftIcon}
          className={cn("size-4", !calendar.previousMonth && "opacity-50")}
        />
      </CalendarPrimitive.Previous>
      <CalendarPrimitive.Next
        render={
          <Button
            variant={buttonVariant}
            size="icon"
            className={cn("p-0", CELL[size])}
          />
        }
      >
        <Icon
          as={rtl ? CaretLeftIcon : CaretRightIcon}
          className={cn("size-4", !calendar.nextMonth && "opacity-50")}
        />
      </CalendarPrimitive.Next>
    </View>
  )
}

function CalendarDayButton({
  className,
  day,
  children,
  ...props
}: React.ComponentProps<typeof CalendarPrimitive.DayButton>) {
  const { size } = React.useContext(CalendarContext)
  const modifiers = day.modifiers
  const single =
    modifiers.selected &&
    !modifiers.rangeStart &&
    !modifiers.rangeEnd &&
    !modifiers.rangeMiddle
  const filled =
    (single && modifiers.today) || modifiers.rangeStart || modifiers.rangeEnd
  return (
    <TextClassContext.Provider
      value={cn(
        "text-foreground",
        DAY_TEXT[size],
        modifiers.outside && "text-muted-foreground",
        (modifiers.today || single) && "font-semibold text-primary",
        modifiers.rangeMiddle && "text-foreground",
        filled && "text-primary-foreground",
        modifiers.disabled && "opacity-50"
      )}
    >
      <CalendarPrimitive.DayButton
        day={day}
        className={(state) =>
          cn(
            "items-center justify-center rounded-full active:opacity-80",
            CELL[size],
            single && "bg-primary-10 dark:bg-primary-20 active:opacity-80",
            filled && "bg-primary active:opacity-80",
            modifiers.rangeMiddle && "rounded-none",
            resolveClassName(className, state)
          )
        }
        {...props}
      >
        {children ?? <Text>{day.date.getDate()}</Text>}
      </CalendarPrimitive.DayButton>
    </TextClassContext.Provider>
  )
}

function CalendarMonth({
  month,
  index,
  buttonVariant,
  captionLayout,
  showWeekNumber,
}: {
  month: Date
  index: number
  buttonVariant: ButtonProps["variant"]
  captionLayout: CaptionLayout
  showWeekNumber: boolean
}) {
  const { size } = React.useContext(CalendarContext)
  const calendar = CalendarPrimitive.useRootContext()
  return (
    <View className="gap-4">
      <View className={cn("flex-row items-center", CELL_HEIGHT[size])}>
        <View className="flex-1 flex-row items-center ps-1.5">
          <CalendarCaption month={month} captionLayout={captionLayout} />
        </View>
        {index === 0 ? <CalendarNav buttonVariant={buttonVariant} /> : null}
      </View>
      <View role="grid" accessibilityLabel={calendar.formatCaption(month)}>
        <View className="flex-row">
          {showWeekNumber ? <View className={CELL_WIDTH[size]} /> : null}
          {calendar.weekdays.map((weekday) => (
            <View
              key={weekday.getDay()}
              accessibilityLabel={calendar.formatWeekday(weekday, "long")}
              className={cn("items-center", CELL_WIDTH[size])}
            >
              <Text
                numberOfLines={1}
                className={cn(
                  "font-semibold text-muted-foreground",
                  WEEKDAY_TEXT[size]
                )}
              >
                {calendar.formatWeekday(weekday, WEEKDAY_FORMAT[size])}
              </Text>
            </View>
          ))}
        </View>
        {calendar.getWeeks(month).map((week) => (
          <View key={week.days[0]?.date.getTime()} className="mt-2 flex-row">
            {showWeekNumber ? (
              <View className={cn("items-center justify-center", CELL[size])}>
                <Text className="text-xs text-muted-foreground">
                  {week.number}
                </Text>
              </View>
            ) : null}
            {week.days.map((day, dayIndex) => {
              const modifiers = day.modifiers
              const shaded =
                modifiers.rangeMiddle ||
                modifiers.rangeStart !== modifiers.rangeEnd
              return (
                <View
                  key={day.date.getTime()}
                  className={cn(
                    CELL[size],
                    !day.hidden && shaded && "bg-muted",
                    modifiers.rangeStart && "rounded-l-full",
                    modifiers.rangeEnd && "rounded-r-full",
                    modifiers.rangeMiddle && dayIndex === 0 && "rounded-l-full",
                    modifiers.rangeMiddle && dayIndex === 6 && "rounded-r-full"
                  )}
                >
                  {day.hidden ? null : <CalendarDayButton day={day} />}
                </View>
              )
            })}
          </View>
        ))}
      </View>
    </View>
  )
}

function CalendarMonths({
  buttonVariant,
  captionLayout,
  showWeekNumber,
}: {
  buttonVariant: ButtonProps["variant"]
  captionLayout: CaptionLayout
  showWeekNumber: boolean
}) {
  const calendar = CalendarPrimitive.useRootContext()
  return (
    <View className="gap-4">
      {calendar.months.map((month, index) => (
        <CalendarMonth
          key={month.getTime()}
          month={month}
          index={index}
          buttonVariant={buttonVariant}
          captionLayout={captionLayout}
          showWeekNumber={showWeekNumber}
        />
      ))}
    </View>
  )
}

function Calendar({
  className,
  size = "default",
  buttonVariant = "plain",
  captionLayout = "label",
  showWeekNumber = false,
  startMonth,
  endMonth,
  ...props
}: React.ComponentProps<typeof CalendarPrimitive.Root> & {
  buttonVariant?: ButtonProps["variant"]
  size?: CalendarSize
  captionLayout?: CaptionLayout
  showWeekNumber?: boolean
}) {
  const yearDropdown =
    captionLayout === "dropdown" || captionLayout === "dropdown-years"
  const thisYear = new Date().getFullYear()
  const context = React.useMemo(() => ({ size }), [size])
  return (
    <CalendarContext.Provider value={context}>
      <CalendarPrimitive.Root
        startMonth={
          startMonth ?? (yearDropdown ? new Date(thisYear - 100, 0) : undefined)
        }
        endMonth={
          endMonth ?? (yearDropdown ? new Date(thisYear, 11) : undefined)
        }
        className={(state) =>
          cn(
            "self-start bg-background",
            PADDING[size],
            resolveClassName(className, state)
          )
        }
        {...props}
      >
        <CalendarMonths
          buttonVariant={buttonVariant}
          captionLayout={captionLayout}
          showWeekNumber={showWeekNumber}
        />
      </CalendarPrimitive.Root>
    </CalendarContext.Provider>
  )
}

export { Calendar, CalendarDayButton }
```

4. Update the import paths to match your project.

**StyleSheet**

1. Install the dependencies:

```bash title="Terminal"
npx expo install phosphor-react-native react-native-svg
```

2. Add the pieces it builds on: [Button](https://ui.suiss.app/docs/components/button), [Icon](https://ui.suiss.app/docs/components/icon), [Select](https://ui.suiss.app/docs/components/select), [Text](https://ui.suiss.app/docs/components/text), and the primitives `components/primitives/calendar.tsx`, `components/primitives/use-render.tsx`.

3. Copy the source into `components/ui/calendar.tsx`:

```tsx title="stylesheet/components/ui/calendar.tsx"
import { CaretLeftIcon, CaretRightIcon } from "phosphor-react-native"
import * as React from "react"
import {
  I18nManager,
  Pressable,
  ScrollView,
  type StyleProp,
  StyleSheet,
  type TextStyle,
  View,
  type ViewStyle,
} from "react-native"
import * as CalendarPrimitive from "@/components/primitives/calendar"
import { resolveStyle } from "@/components/primitives/use-render"
import { Button, type ButtonProps } from "@/components/ui/button"
import { Icon } from "@/components/ui/icon"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
import { Text, TextStyleContext } from "@/components/ui/text"
import {
  fontSize,
  radius,
  type ThemeColors,
  useTheme,
} from "@/components/ui/theme"

function withAlpha(color: string, alpha: number) {
  if (!color.startsWith("#") || color.length !== 7) return color
  const r = Number.parseInt(color.slice(1, 3), 16)
  const g = Number.parseInt(color.slice(3, 5), 16)
  const b = Number.parseInt(color.slice(5, 7), 16)
  return `rgba(${r}, ${g}, ${b}, ${alpha})`
}

type CalendarSize = "sm" | "default" | "lg"

type CaptionLayout = "label" | "dropdown" | "dropdown-months" | "dropdown-years"

const CELL: Record<CalendarSize, number> = { sm: 24, default: 32, lg: 40 }

const PADDING: Record<CalendarSize, number> = { sm: 8, default: 12, lg: 16 }

const CAPTION_TEXT: Record<CalendarSize, TextStyle> = {
  sm: fontSize.xs,
  default: fontSize.sm,
  lg: fontSize.base,
}

const DAY_TEXT: Record<CalendarSize, TextStyle> = {
  sm: fontSize.xs,
  default: fontSize.sm,
  lg: fontSize.lg,
}

const WEEKDAY_TEXT: Record<CalendarSize, TextStyle> = {
  sm: fontSize.xs,
  default: { ...fontSize.xs, textTransform: "uppercase" },
  lg: { ...fontSize.xs, textTransform: "uppercase" },
}

const WEEKDAY_FORMAT: Record<CalendarSize, "narrow" | "short"> = {
  sm: "narrow",
  default: "short",
  lg: "short",
}

const styles = StyleSheet.create({
  root: { alignSelf: "flex-start" },
  months: { gap: 16 },
  month: { gap: 16 },
  header: { flexDirection: "row", alignItems: "center" },
  caption: {
    flex: 1,
    flexDirection: "row",
    alignItems: "center",
    paddingStart: 6,
  },
  dropdowns: { flexDirection: "row", alignItems: "center", gap: 6 },
  dropdownTrigger: {
    height: "auto",
    gap: 4,
    borderRadius: radius.full,
    backgroundColor: "transparent",
    paddingHorizontal: 6,
    paddingVertical: 2,
  },
  dropdownValue: { flex: 0, fontWeight: "600" },
  dropdownList: { maxHeight: 288 },
  captionText: { fontWeight: "600" },
  nav: { flexDirection: "row", alignItems: "center", gap: 2 },
  navButton: { padding: 0 },
  row: { flexDirection: "row" },
  week: { marginTop: 8, flexDirection: "row" },
  weekday: { alignItems: "center" },
  weekdayText: { fontWeight: "600" },
  center: { alignItems: "center", justifyContent: "center" },
  day: {
    alignItems: "center",
    justifyContent: "center",
    borderRadius: radius.full,
  },
})

const CalendarContext = React.createContext<{ size: CalendarSize }>({
  size: "default",
})

type DropdownOption = {
  value: string
  label: string
  disabled?: boolean
}

function CalendarDropdown({
  value,
  options,
  label,
  onChange,
}: {
  value: string
  options: DropdownOption[]
  label: string
  onChange: (value: string) => void
}) {
  const { size } = React.useContext(CalendarContext)
  return (
    <Select
      items={options}
      value={value}
      onValueChange={(next) => {
        if (typeof next === "string") onChange(next)
      }}
    >
      <SelectTrigger
        size="sm"
        accessibilityLabel={label}
        style={styles.dropdownTrigger}
      >
        <SelectValue style={[styles.dropdownValue, CAPTION_TEXT[size]]} />
      </SelectTrigger>
      <SelectContent>
        <ScrollView style={styles.dropdownList}>
          {options.map((option) => (
            <SelectItem
              key={option.value}
              value={option.value}
              label={option.label}
              disabled={option.disabled}
            >
              {option.label}
            </SelectItem>
          ))}
        </ScrollView>
      </SelectContent>
    </Select>
  )
}

function CalendarCaption({
  month,
  captionLayout,
}: {
  month: Date
  captionLayout: CaptionLayout
}) {
  const { size } = React.useContext(CalendarContext)
  const { colors } = useTheme()
  const calendar = CalendarPrimitive.useRootContext()
  const year = month.getFullYear()
  const captionText = [
    styles.captionText,
    CAPTION_TEXT[size],
    { color: colors.foreground },
  ]

  if (captionLayout === "label") {
    return <Text style={captionText}>{calendar.formatCaption(month)}</Text>
  }

  const firstYear = calendar.startMonth?.getFullYear() ?? year
  const lastYear = calendar.endMonth?.getFullYear() ?? year
  const outOfRange = (target: Date) =>
    (calendar.startMonth !== undefined &&
      target.getTime() < calendar.startMonth.getTime()) ||
    (calendar.endMonth !== undefined &&
      target.getTime() > calendar.endMonth.getTime())

  const months = Array.from({ length: 12 }, (_, index) => {
    const date = new Date(year, index, 1)
    return {
      value: String(index),
      label: calendar.formatMonth(date, "short"),
      disabled: outOfRange(date),
    }
  })
  const years = Array.from(
    { length: Math.max(lastYear - firstYear + 1, 1) },
    (_, index) => ({
      value: String(firstYear + index),
      label: String(firstYear + index),
    })
  )

  return (
    <View style={styles.dropdowns}>
      {captionLayout === "dropdown-years" ? (
        <Text style={captionText}>{calendar.formatMonth(month, "long")}</Text>
      ) : (
        <CalendarDropdown
          label="Choose the Month"
          value={String(month.getMonth())}
          options={months}
          onChange={(next) =>
            calendar.goToMonth(
              new Date(year, Number(next), 1),
              CalendarPrimitive.CALENDAR_REASONS.monthSelect
            )
          }
        />
      )}
      {captionLayout === "dropdown-months" ? (
        <Text style={captionText}>{year}</Text>
      ) : (
        <CalendarDropdown
          label="Choose the Year"
          value={String(year)}
          options={years}
          onChange={(next) =>
            calendar.goToMonth(
              new Date(Number(next), month.getMonth(), 1),
              CalendarPrimitive.CALENDAR_REASONS.yearSelect
            )
          }
        />
      )}
    </View>
  )
}

function CalendarNav({
  buttonVariant,
}: {
  buttonVariant: ButtonProps["variant"]
}) {
  const { size } = React.useContext(CalendarContext)
  const rtl = I18nManager.isRTL
  const buttonStyle = [
    styles.navButton,
    { width: CELL[size], height: CELL[size] },
  ]
  return (
    <View style={styles.nav}>
      <CalendarPrimitive.Previous
        render={
          <Button variant={buttonVariant} size="icon" style={buttonStyle} />
        }
      >
        <Icon as={rtl ? CaretRightIcon : CaretLeftIcon} size={16} />
      </CalendarPrimitive.Previous>
      <CalendarPrimitive.Next
        render={
          <Button variant={buttonVariant} size="icon" style={buttonStyle} />
        }
      >
        <Icon as={rtl ? CaretLeftIcon : CaretRightIcon} size={16} />
      </CalendarPrimitive.Next>
    </View>
  )
}

function dayColors(
  modifiers: CalendarPrimitive.DayModifiers,
  colors: ThemeColors,
  isDark: boolean
) {
  const single =
    modifiers.selected &&
    !modifiers.rangeStart &&
    !modifiers.rangeEnd &&
    !modifiers.rangeMiddle
  const filled =
    (single && modifiers.today) || modifiers.rangeStart || modifiers.rangeEnd
  let text = colors.foreground
  if (modifiers.outside) text = colors.mutedForeground
  if (modifiers.today || single) text = colors.primary
  if (modifiers.rangeMiddle) text = colors.foreground
  if (filled) text = colors.primaryForeground
  let background = "transparent"
  if (single) background = withAlpha(colors.primary, isDark ? 0.2 : 0.1)
  if (filled) background = colors.primary
  return {
    text,
    background,
    bold: modifiers.today || single,
  }
}

function CalendarDayButton({
  style,
  day,
  children,
  ...props
}: React.ComponentProps<typeof CalendarPrimitive.DayButton>) {
  const { size } = React.useContext(CalendarContext)
  const { colors, isDark } = useTheme()
  const modifiers = day.modifiers
  const idle = dayColors(modifiers, colors, isDark)
  return (
    <TextStyleContext.Provider
      value={{
        ...DAY_TEXT[size],
        color: idle.text,
        fontWeight: idle.bold ? "600" : "400",
      }}
    >
      <CalendarPrimitive.DayButton
        day={day}
        render={(renderProps, state) => (
          <Pressable
            {...renderProps}
            style={({ pressed }) => [
              styles.day,
              { width: CELL[size], height: CELL[size] },
              { backgroundColor: idle.background },
              modifiers.rangeMiddle && { borderRadius: 0 },
              pressed && { opacity: 0.8 },
              modifiers.disabled && { opacity: 0.5 },
              resolveStyle(style, state) as StyleProp<ViewStyle>,
            ]}
          />
        )}
        {...props}
      >
        {children ?? <Text>{day.date.getDate()}</Text>}
      </CalendarPrimitive.DayButton>
    </TextStyleContext.Provider>
  )
}

function CalendarMonth({
  month,
  index,
  buttonVariant,
  captionLayout,
  showWeekNumber,
}: {
  month: Date
  index: number
  buttonVariant: ButtonProps["variant"]
  captionLayout: CaptionLayout
  showWeekNumber: boolean
}) {
  const { size } = React.useContext(CalendarContext)
  const { colors } = useTheme()
  const calendar = CalendarPrimitive.useRootContext()
  const cell = CELL[size]
  return (
    <View style={styles.month}>
      <View style={[styles.header, { height: cell }]}>
        <View style={styles.caption}>
          <CalendarCaption month={month} captionLayout={captionLayout} />
        </View>
        {index === 0 ? <CalendarNav buttonVariant={buttonVariant} /> : null}
      </View>
      <View role="grid" accessibilityLabel={calendar.formatCaption(month)}>
        <View style={styles.row}>
          {showWeekNumber ? <View style={{ width: cell }} /> : null}
          {calendar.weekdays.map((weekday) => (
            <View
              key={weekday.getDay()}
              accessibilityLabel={calendar.formatWeekday(weekday, "long")}
              style={[styles.weekday, { width: cell }]}
            >
              <Text
                numberOfLines={1}
                style={[
                  styles.weekdayText,
                  WEEKDAY_TEXT[size],
                  { color: colors.mutedForeground },
                ]}
              >
                {calendar.formatWeekday(weekday, WEEKDAY_FORMAT[size])}
              </Text>
            </View>
          ))}
        </View>
        {calendar.getWeeks(month).map((week) => (
          <View key={week.days[0]?.date.getTime()} style={styles.week}>
            {showWeekNumber ? (
              <View style={[styles.center, { width: cell, height: cell }]}>
                <Text style={[fontSize.xs, { color: colors.mutedForeground }]}>
                  {week.number}
                </Text>
              </View>
            ) : null}
            {week.days.map((day, dayIndex) => {
              const modifiers = day.modifiers
              const shaded =
                modifiers.rangeMiddle ||
                modifiers.rangeStart !== modifiers.rangeEnd
              const rounded = radius.full
              return (
                <View
                  key={day.date.getTime()}
                  style={[
                    { width: cell, height: cell },
                    !day.hidden && shaded && { backgroundColor: colors.muted },
                    (modifiers.rangeStart ||
                      (modifiers.rangeMiddle && dayIndex === 0)) && {
                      borderTopLeftRadius: rounded,
                      borderBottomLeftRadius: rounded,
                    },
                    (modifiers.rangeEnd ||
                      (modifiers.rangeMiddle && dayIndex === 6)) && {
                      borderTopRightRadius: rounded,
                      borderBottomRightRadius: rounded,
                    },
                  ]}
                >
                  {day.hidden ? null : <CalendarDayButton day={day} />}
                </View>
              )
            })}
          </View>
        ))}
      </View>
    </View>
  )
}

function CalendarMonths({
  buttonVariant,
  captionLayout,
  showWeekNumber,
}: {
  buttonVariant: ButtonProps["variant"]
  captionLayout: CaptionLayout
  showWeekNumber: boolean
}) {
  const calendar = CalendarPrimitive.useRootContext()
  return (
    <View style={styles.months}>
      {calendar.months.map((month, index) => (
        <CalendarMonth
          key={month.getTime()}
          month={month}
          index={index}
          buttonVariant={buttonVariant}
          captionLayout={captionLayout}
          showWeekNumber={showWeekNumber}
        />
      ))}
    </View>
  )
}

function Calendar({
  style,
  size = "default",
  buttonVariant = "plain",
  captionLayout = "label",
  showWeekNumber = false,
  startMonth,
  endMonth,
  ...props
}: React.ComponentProps<typeof CalendarPrimitive.Root> & {
  buttonVariant?: ButtonProps["variant"]
  size?: CalendarSize
  captionLayout?: CaptionLayout
  showWeekNumber?: boolean
}) {
  const { colors } = useTheme()
  const yearDropdown =
    captionLayout === "dropdown" || captionLayout === "dropdown-years"
  const thisYear = new Date().getFullYear()
  const context = React.useMemo(() => ({ size }), [size])
  return (
    <CalendarContext.Provider value={context}>
      <CalendarPrimitive.Root
        startMonth={
          startMonth ?? (yearDropdown ? new Date(thisYear - 100, 0) : undefined)
        }
        endMonth={
          endMonth ?? (yearDropdown ? new Date(thisYear, 11) : undefined)
        }
        style={(state) => [
          styles.root,
          { backgroundColor: colors.background, padding: PADDING[size] },
          resolveStyle(style, state) as StyleProp<ViewStyle>,
        ]}
        {...props}
      >
        <CalendarMonths
          buttonVariant={buttonVariant}
          captionLayout={captionLayout}
          showWeekNumber={showWeekNumber}
        />
      </CalendarPrimitive.Root>
    </CalendarContext.Provider>
  )
}

export { Calendar, CalendarDayButton }
```

4. Update the import paths to match your project.

**SwiftUI**

1. Target iOS 17 or later.

2. Add the Swift package to your app:

```swift title="Package.swift"
.package(url: "https://github.com/phosphor-icons/swift", from: "2.1.0")
```

3. Copy the theme files every view uses:

- `swift/Theme/Theme.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Theme/Theme.swift)

- `swift/Theme/Tokens.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Theme/Tokens.swift)

4. Copy the source and every file it uses:

```swift title="swift/Components/Calendar.swift"
import PhosphorSwift
import SwiftUI

public struct DateRange: Hashable, Sendable {
  public var from: Date
  public var to: Date?

  public init(from: Date, to: Date? = nil) {
    self.from = from
    self.to = to
  }

  public static func adding(_ day: Date, to range: DateRange?, calendar: Foundation.Calendar) -> DateRange? {
    guard let range else { return DateRange(from: day) }
    let from = range.from
    guard let to = range.to else {
      if calendar.isDate(day, inSameDayAs: from) { return DateRange(from: from, to: day) }
      return day < from ? DateRange(from: day, to: from) : DateRange(from: from, to: day)
    }
    let sameFrom = calendar.isDate(day, inSameDayAs: from)
    let sameTo = calendar.isDate(day, inSameDayAs: to)
    if sameFrom && sameTo { return nil }
    if sameFrom { return DateRange(from: to) }
    if sameTo { return DateRange(from: from) }
    if day < from { return DateRange(from: day, to: to) }
    return DateRange(from: from, to: day)
  }
}

public struct CalendarDayModifiers: Hashable, Sendable {
  public var today = false
  public var selected = false
  public var rangeStart = false
  public var rangeMiddle = false
  public var rangeEnd = false
  public var outside = false
  public var disabled = false

  public init(
    today: Bool = false,
    selected: Bool = false,
    rangeStart: Bool = false,
    rangeMiddle: Bool = false,
    rangeEnd: Bool = false,
    outside: Bool = false,
    disabled: Bool = false
  ) {
    self.today = today
    self.selected = selected
    self.rangeStart = rangeStart
    self.rangeMiddle = rangeMiddle
    self.rangeEnd = rangeEnd
    self.outside = outside
    self.disabled = disabled
  }

  var selectedSingle: Bool { selected && !rangeStart && !rangeEnd && !rangeMiddle }
}

enum CalendarMonthGrid {
  static func startOfMonth(_ date: Date, calendar: Foundation.Calendar) -> Date {
    calendar.date(from: calendar.dateComponents([.year, .month], from: date)) ?? date
  }

  static func weeks(for month: Date, calendar: Foundation.Calendar) -> [[Date]] {
    let first = startOfMonth(month, calendar: calendar)
    let days = calendar.range(of: .day, in: .month, for: first)?.count ?? 30
    let lead = (calendar.component(.weekday, from: first) - calendar.firstWeekday + 7) % 7
    let count = Int((Double(lead + days) / 7).rounded(.up))
    guard let start = calendar.date(byAdding: .day, value: -lead, to: first) else { return [] }
    return (0..<count).map { week in
      (0..<7).compactMap { day in
        calendar.date(byAdding: .day, value: week * 7 + day, to: start)
      }
    }
  }

  static func weekdaySymbols(calendar: Foundation.Calendar, locale: Locale, format: String) -> [String] {
    let formatter = DateFormatter()
    formatter.locale = locale
    formatter.calendar = calendar
    formatter.dateFormat = format
    let reference = weeks(for: Date(timeIntervalSince1970: 0), calendar: calendar).first ?? []
    return reference.map { formatter.string(from: $0) }
  }
}

enum CalendarSelection {
  case single(Binding<Date?>)
  case range(Binding<DateRange?>)
  case multiple(Binding<Set<Date>>)
}

extension EnvironmentValues {
  @Entry var calendarTransparent = false
}

public struct DateCalendar: View {
  public enum Size: CaseIterable, Sendable {
    case sm, `default`, lg

    var cell: CGFloat {
      switch self {
      case .sm: 24
      case .default: 32
      case .lg: 40
      }
    }

    var padding: CGFloat {
      switch self {
      case .sm: 8
      case .default: 12
      case .lg: 16
      }
    }

    var caption: TextStyle {
      switch self {
      case .sm: .xs
      case .default: .sm
      case .lg: .base
      }
    }

    var day: TextStyle {
      switch self {
      case .sm: .xs
      case .default: .sm
      case .lg: .lg
      }
    }

    var weekday: TextStyle {
      .xs
    }
  }

  public enum CaptionLayout: CaseIterable, Sendable {
    case label, dropdown, dropdownMonths, dropdownYears
  }

  let selection: CalendarSelection
  let externalMonth: Binding<Date>?
  let size: Size
  let buttonVariant: VariantButtonStyle.Variant
  let captionLayout: CaptionLayout
  let numberOfMonths: Int
  let showOutsideDays: Bool
  let showWeekNumber: Bool
  let startMonth: Date?
  let endMonth: Date?
  let isDisabled: (Date) -> Bool

  @State private var internalMonth: Date
  @Environment(\.calendar) private var environmentCalendar
  @Environment(\.locale) private var locale
  @Environment(\.calendarTransparent) private var transparent
  @Environment(\.accessibilityReduceMotion) private var reduceMotion

  init(
    selection: CalendarSelection,
    initial: Date?,
    month: Binding<Date>?,
    size: Size,
    buttonVariant: VariantButtonStyle.Variant,
    captionLayout: CaptionLayout,
    numberOfMonths: Int,
    showOutsideDays: Bool,
    showWeekNumber: Bool,
    startMonth: Date?,
    endMonth: Date?,
    isDisabled: @escaping (Date) -> Bool
  ) {
    self.selection = selection
    externalMonth = month
    self.size = size
    self.buttonVariant = buttonVariant
    self.captionLayout = captionLayout
    self.numberOfMonths = max(1, numberOfMonths)
    self.showOutsideDays = showOutsideDays
    self.showWeekNumber = showWeekNumber
    self.startMonth = startMonth
    self.endMonth = endMonth
    self.isDisabled = isDisabled
    _internalMonth = State(initialValue: initial ?? Date())
  }

  public init(
    selection: Binding<Date?>,
    month: Binding<Date>? = nil,
    size: Size = .default,
    buttonVariant: VariantButtonStyle.Variant = .plain,
    captionLayout: CaptionLayout = .label,
    numberOfMonths: Int = 1,
    showOutsideDays: Bool = false,
    showWeekNumber: Bool = false,
    startMonth: Date? = nil,
    endMonth: Date? = nil,
    isDisabled: @escaping (Date) -> Bool = { _ in false }
  ) {
    self.init(
      selection: .single(selection),
      initial: selection.wrappedValue,
      month: month,
      size: size,
      buttonVariant: buttonVariant,
      captionLayout: captionLayout,
      numberOfMonths: numberOfMonths,
      showOutsideDays: showOutsideDays,
      showWeekNumber: showWeekNumber,
      startMonth: startMonth,
      endMonth: endMonth,
      isDisabled: isDisabled
    )
  }

  public init(
    selection: Binding<DateRange?>,
    month: Binding<Date>? = nil,
    size: Size = .default,
    buttonVariant: VariantButtonStyle.Variant = .plain,
    captionLayout: CaptionLayout = .label,
    numberOfMonths: Int = 1,
    showOutsideDays: Bool = false,
    showWeekNumber: Bool = false,
    startMonth: Date? = nil,
    endMonth: Date? = nil,
    isDisabled: @escaping (Date) -> Bool = { _ in false }
  ) {
    self.init(
      selection: .range(selection),
      initial: selection.wrappedValue?.from,
      month: month,
      size: size,
      buttonVariant: buttonVariant,
      captionLayout: captionLayout,
      numberOfMonths: numberOfMonths,
      showOutsideDays: showOutsideDays,
      showWeekNumber: showWeekNumber,
      startMonth: startMonth,
      endMonth: endMonth,
      isDisabled: isDisabled
    )
  }

  public init(
    selection: Binding<Set<Date>>,
    month: Binding<Date>? = nil,
    size: Size = .default,
    buttonVariant: VariantButtonStyle.Variant = .plain,
    captionLayout: CaptionLayout = .label,
    numberOfMonths: Int = 1,
    showOutsideDays: Bool = false,
    showWeekNumber: Bool = false,
    startMonth: Date? = nil,
    endMonth: Date? = nil,
    isDisabled: @escaping (Date) -> Bool = { _ in false }
  ) {
    self.init(
      selection: .multiple(selection),
      initial: selection.wrappedValue.min(),
      month: month,
      size: size,
      buttonVariant: buttonVariant,
      captionLayout: captionLayout,
      numberOfMonths: numberOfMonths,
      showOutsideDays: showOutsideDays,
      showWeekNumber: showWeekNumber,
      startMonth: startMonth,
      endMonth: endMonth,
      isDisabled: isDisabled
    )
  }

  private var calendar: Foundation.Calendar {
    var resolved = Foundation.Calendar(identifier: environmentCalendar.identifier)
    resolved.locale = locale
    resolved.timeZone = environmentCalendar.timeZone
    return resolved
  }

  private var displayed: Date {
    CalendarMonthGrid.startOfMonth(externalMonth?.wrappedValue ?? internalMonth, calendar: calendar)
  }

  private func setDisplayed(_ month: Date) {
    let start = CalendarMonthGrid.startOfMonth(month, calendar: calendar)
    if let externalMonth {
      externalMonth.wrappedValue = start
    } else {
      internalMonth = start
    }
  }

  private func shift(_ months: Int) -> Date {
    calendar.date(byAdding: .month, value: months, to: displayed) ?? displayed
  }

  private var canGoBack: Bool {
    guard let startMonth else { return true }
    return displayed > CalendarMonthGrid.startOfMonth(startMonth, calendar: calendar)
  }

  private var canGoForward: Bool {
    guard let endMonth else { return true }
    return shift(numberOfMonths - 1) < CalendarMonthGrid.startOfMonth(endMonth, calendar: calendar)
  }

  private func navigate(_ step: Int) {
    guard step < 0 ? canGoBack : canGoForward else { return }
    if reduceMotion {
      setDisplayed(shift(step))
    } else {
      withAnimation(.timingCurve(0.32, 0.72, 0, 1, duration: 0.3)) { setDisplayed(shift(step)) }
    }
  }

  public var body: some View {
    VStack(alignment: .leading, spacing: 16) {
      ForEach(0..<numberOfMonths, id: \.self) { index in
        month(shift(index), index: index)
      }
    }
    .padding(size.padding)
    .fixedSize()
    .background(transparent ? Color.clear : EsuissColor.background)
    .contentShape(Rectangle())
    .gesture(
      DragGesture(minimumDistance: 24)
        .onEnded { value in
          let dx = value.translation.width
          guard abs(dx) > 48, abs(dx) > abs(value.translation.height) else { return }
          navigate(dx < 0 ? 1 : -1)
        }
    )
  }

  private func month(_ month: Date, index: Int) -> some View {
    let weeks = CalendarMonthGrid.weeks(for: month, calendar: calendar)
    return VStack(alignment: .leading, spacing: 16) {
      HStack(spacing: 2) {
        caption(month)
          .padding(.leading, 6)
          .frame(maxWidth: .infinity, alignment: .leading)
        if index == 0 {
          navButton(icon: Ph.caretLeft.regular, enabled: canGoBack, label: Text("Previous month")) { navigate(-1) }
          navButton(icon: Ph.caretRight.regular, enabled: canGoForward, label: Text("Next month")) { navigate(1) }
        }
      }
      .frame(height: size.cell)
      VStack(spacing: 8) {
        weekdayRow
        ForEach(weeks, id: \.self) { week in
          weekRow(week, month: month)
        }
      }
      .accessibilityElement(children: .contain)
      .accessibilityLabel(Text(verbatim: captionText(month)))
    }
    .frame(width: size.cell * CGFloat(showWeekNumber ? 8 : 7))
  }

  private func captionText(_ month: Date) -> String {
    let formatter = DateFormatter()
    formatter.locale = locale
    formatter.calendar = calendar
    formatter.setLocalizedDateFormatFromTemplate("LLLLy")
    return formatter.string(from: month)
  }

  @ViewBuilder private func caption(_ month: Date) -> some View {
    switch captionLayout {
    case .label:
      Text(verbatim: captionText(month))
        .scaledFont(size.caption, weight: .semibold)
        .foregroundStyle(EsuissColor.foreground)
        .lineLimit(1)
    case .dropdown:
      HStack(spacing: 6) {
        monthMenu(month)
        yearMenu(month)
      }
    case .dropdownMonths:
      HStack(spacing: 6) {
        monthMenu(month)
        Text(verbatim: String(calendar.component(.year, from: month)))
          .scaledFont(size.caption, weight: .semibold)
          .foregroundStyle(EsuissColor.foreground)
      }
    case .dropdownYears:
      HStack(spacing: 6) {
        Text(verbatim: monthSymbol(month, short: false))
          .scaledFont(size.caption, weight: .semibold)
          .foregroundStyle(EsuissColor.foreground)
        yearMenu(month)
      }
    }
  }

  private func monthSymbol(_ month: Date, short: Bool) -> String {
    let symbols = short ? calendar.shortStandaloneMonthSymbols : calendar.standaloneMonthSymbols
    return symbols[calendar.component(.month, from: month) - 1]
  }

  private var yearBounds: ClosedRange<Int> {
    let current = calendar.component(.year, from: Date())
    let lower = startMonth.map { calendar.component(.year, from: $0) } ?? current - 100
    let upper = endMonth.map { calendar.component(.year, from: $0) } ?? current + 10
    return lower...max(lower, upper)
  }

  private func monthMenu(_ month: Date) -> some View {
    let selected = Binding<Int>(
      get: { calendar.component(.month, from: displayed) },
      set: { value in
        var components = calendar.dateComponents([.year], from: displayed)
        components.month = value
        if let date = calendar.date(from: components) { setDisplayed(date) }
      }
    )
    return Menu {
      Picker(selection: selected) {
        ForEach(1...12, id: \.self) { value in
          Text(verbatim: calendar.standaloneMonthSymbols[value - 1]).tag(value)
        }
      } label: {
        EmptyView()
      }
      .pickerStyle(.inline)
    } label: {
      dropdownLabel(monthSymbol(month, short: true))
    }
  }

  private func yearMenu(_ month: Date) -> some View {
    let selected = Binding<Int>(
      get: { calendar.component(.year, from: displayed) },
      set: { value in
        var components = calendar.dateComponents([.month], from: displayed)
        components.year = value
        if let date = calendar.date(from: components) { setDisplayed(date) }
      }
    )
    return Menu {
      Picker(selection: selected) {
        ForEach(Array(yearBounds), id: \.self) { value in
          Text(verbatim: String(value)).tag(value)
        }
      } label: {
        EmptyView()
      }
      .pickerStyle(.inline)
    } label: {
      dropdownLabel(String(calendar.component(.year, from: month)))
    }
  }

  private func dropdownLabel(_ text: String) -> some View {
    HStack(spacing: 4) {
      Text(verbatim: text)
        .scaledFont(size.caption, weight: .semibold)
        .foregroundStyle(EsuissColor.foreground)
      Ph.caretDown.regular
        .frame(width: 14, height: 14)
        .foregroundStyle(EsuissColor.mutedForeground)
    }
  }

  private func navButton(icon: some View, enabled: Bool, label: Text, action: @escaping () -> Void) -> some View {
    Button(action: action) {
      icon.frame(width: 16, height: 16)
    }
    .buttonStyle(CalendarNavButtonStyle(variant: buttonVariant, size: size.cell))
    .disabled(!enabled)
    .accessibilityLabel(label)
  }

  private var weekdayRow: some View {
    let symbols = CalendarMonthGrid.weekdaySymbols(calendar: calendar, locale: locale, format: "cccccc")
    return HStack(spacing: 0) {
      if showWeekNumber {
        Color.clear.frame(width: size.cell, height: 1)
      }
      ForEach(Array(symbols.enumerated()), id: \.offset) { _, symbol in
        Text(verbatim: size == .sm ? symbol : symbol.uppercased(with: locale))
          .scaledFont(size.weekday, weight: .semibold)
          .foregroundStyle(EsuissColor.mutedForeground)
          .lineLimit(1)
          .frame(maxWidth: .infinity)
      }
    }
    .accessibilityHidden(true)
  }

  private func weekRow(_ week: [Date], month: Date) -> some View {
    HStack(spacing: 0) {
      if showWeekNumber, let first = week.first {
        Text(verbatim: String(calendar.component(.weekOfYear, from: first)))
          .scaledFont(TextStyle.xs)
          .foregroundStyle(EsuissColor.mutedForeground)
          .frame(width: size.cell, height: size.cell)
      }
      ForEach(Array(week.enumerated()), id: \.offset) { position, day in
        let modifiers = modifiers(for: day, month: month)
        let hidden = modifiers.outside && !showOutsideDays
        ZStack {
          if !hidden {
            band(modifiers, first: position == 0, last: position == week.count - 1)
            CalendarDayButton(date: day, modifiers: modifiers, size: size) {
              select(day)
            }
          }
        }
        .frame(width: size.cell, height: size.cell)
      }
    }
  }

  @ViewBuilder private func band(_ modifiers: CalendarDayModifiers, first: Bool, last: Bool) -> some View {
    let inBand = modifiers.rangeMiddle || ((modifiers.rangeStart || modifiers.rangeEnd) && !(modifiers.rangeStart && modifiers.rangeEnd))
    if inBand {
      let radius = size.cell / 2
      let leading = modifiers.rangeStart || first ? radius : 0
      let trailing = modifiers.rangeEnd || last ? radius : 0
      UnevenRoundedRectangle(
        topLeadingRadius: leading,
        bottomLeadingRadius: leading,
        bottomTrailingRadius: trailing,
        topTrailingRadius: trailing,
        style: .continuous
      )
      .fill(EsuissColor.muted)
    }
  }

  private func modifiers(for day: Date, month: Date) -> CalendarDayModifiers {
    var result = CalendarDayModifiers()
    result.today = calendar.isDateInToday(day)
    result.outside = !calendar.isDate(day, equalTo: month, toGranularity: .month)
    result.disabled = isDisabled(day)
    switch selection {
    case .single(let binding):
      result.selected = binding.wrappedValue.map { calendar.isDate($0, inSameDayAs: day) } ?? false
    case .multiple(let binding):
      result.selected = binding.wrappedValue.contains { calendar.isDate($0, inSameDayAs: day) }
    case .range(let binding):
      guard let range = binding.wrappedValue else { break }
      let start = calendar.isDate(range.from, inSameDayAs: day)
      let end = range.to.map { calendar.isDate($0, inSameDayAs: day) } ?? false
      let middle = range.to.map { day > range.from && day < $0 && !end } ?? false
      result.rangeStart = start
      result.rangeEnd = end || (start && range.to == nil)
      result.rangeMiddle = middle && !start
      result.selected = start || end || result.rangeMiddle
    }
    if result.outside && !showOutsideDays { result.selected = false }
    return result
  }

  private func select(_ day: Date) {
    let normalized = calendar.startOfDay(for: day)
    switch selection {
    case .single(let binding):
      if let current = binding.wrappedValue, calendar.isDate(current, inSameDayAs: normalized) {
        binding.wrappedValue = nil
      } else {
        binding.wrappedValue = normalized
      }
    case .multiple(let binding):
      if let existing = binding.wrappedValue.first(where: { calendar.isDate($0, inSameDayAs: normalized) }) {
        binding.wrappedValue.remove(existing)
      } else {
        binding.wrappedValue.insert(normalized)
      }
    case .range(let binding):
      binding.wrappedValue = DateRange.adding(normalized, to: binding.wrappedValue, calendar: calendar)
    }
    if !calendar.isDate(normalized, equalTo: displayed, toGranularity: .month), numberOfMonths == 1 {
      setDisplayed(normalized)
    }
  }
}

struct CalendarNavButtonStyle: ButtonStyle {
  let variant: VariantButtonStyle.Variant
  let size: CGFloat

  @Environment(\.isEnabled) private var isEnabled

  func makeBody(configuration: Configuration) -> some View {
    let pressed = configuration.isPressed && isEnabled
    configuration.label
      .foregroundStyle(foreground)
      .frame(width: size, height: size)
      .background(background, in: Circle())
      .opacity(isEnabled ? (pressed ? 0.8 : 1) : 0.5)
      .contentShape(Circle())
  }

  private var foreground: Color {
    switch variant {
    case .plain, .tinted, .link: return EsuissColor.primary
    case .default: return EsuissColor.primaryForeground
    case .destructive: return EsuissColor.destructive
    case .secondary: return EsuissColor.secondaryForeground
    case .outline, .ghost: return EsuissColor.foreground
    }
  }

  private var background: Color {
    switch variant {
    case .default: return EsuissColor.primary
    case .secondary: return EsuissColor.secondary
    case .tinted: return Color.adaptive(light: EsuissColor.primary.opacity(0.1), dark: EsuissColor.primary.opacity(0.2))
    case .destructive: return Color.adaptive(light: EsuissColor.destructive.opacity(0.1), dark: EsuissColor.destructive.opacity(0.2))
    case .outline, .ghost, .plain, .link: return .clear
    }
  }
}

public struct CalendarDayButton: View {
  let date: Date
  let modifiers: CalendarDayModifiers
  let size: DateCalendar.Size
  let action: () -> Void

  @Environment(\.locale) private var locale
  @Environment(\.calendar) private var calendar

  public init(date: Date, modifiers: CalendarDayModifiers, size: DateCalendar.Size = .default, action: @escaping () -> Void) {
    self.date = date
    self.modifiers = modifiers
    self.size = size
    self.action = action
  }

  public var body: some View {
    Button(action: action) {
      Text(verbatim: String(calendar.component(.day, from: date)))
    }
    .buttonStyle(PressReader { pressed in
      label(pressed: pressed)
    })
    .disabled(modifiers.disabled)
    .accessibilityLabel(Text(verbatim: accessibilityText))
    .accessibilityAddTraits(modifiers.selected ? .isSelected : [])
  }

  private var accessibilityText: String {
    let formatter = DateFormatter()
    formatter.locale = locale
    formatter.dateStyle = .full
    let full = formatter.string(from: date)
    guard modifiers.today else { return full }
    let relative = RelativeDateTimeFormatter()
    relative.locale = locale
    relative.dateTimeStyle = .named
    let today = relative.localizedString(from: DateComponents(day: 0))
    return "\(today.prefix(1).uppercased())\(today.dropFirst()), \(full)"
  }

  private var accent: Bool {
    modifiers.rangeStart || modifiers.rangeEnd || (modifiers.today && modifiers.selectedSingle)
  }

  private func label(pressed: Bool) -> some View {
    let bold = modifiers.today || modifiers.selectedSingle
    let style = size.day
    let cell = size.cell
    return Text(verbatim: String(calendar.component(.day, from: date)))
      .scaledFont(style, weight: bold ? .semibold : .regular)
      .monospacedDigit()
      .foregroundStyle(foreground)
      .frame(width: cell, height: cell)
      .background(background, in: Circle())
      .opacity(modifiers.disabled ? 0.5 : (pressed ? 0.8 : 1))
      .contentShape(Circle())
  }

  private var foreground: Color {
    if accent { return EsuissColor.primaryForeground }
    if modifiers.disabled { return EsuissColor.mutedForeground }
    if modifiers.rangeMiddle { return EsuissColor.accentForeground }
    if modifiers.selectedSingle || modifiers.today { return EsuissColor.primary }
    if modifiers.outside { return EsuissColor.mutedForeground }
    return EsuissColor.foreground
  }

  private var background: Color {
    if accent { return EsuissColor.primary }
    if modifiers.selectedSingle { return Color.adaptive(light: EsuissColor.primary.opacity(0.1), dark: EsuissColor.primary.opacity(0.2)) }
    return .clear
  }
}
```

- `swift/Components/Button.swift`: see [Button](https://ui.suiss.app/docs/components/button.md)

- `swift/Components/ButtonGroup.swift`: see [Button Group](https://ui.suiss.app/docs/components/button-group.md)

- `swift/Components/Checkbox.swift`: see [Checkbox](https://ui.suiss.app/docs/components/checkbox.md)

- `swift/Components/Field.swift`: see [Field](https://ui.suiss.app/docs/components/field.md)

- `swift/Components/Haptics.swift`: see [Haptics](https://ui.suiss.app/docs/components/haptics.md)

- `swift/Components/Separator.swift`: see [Separator](https://ui.suiss.app/docs/components/separator.md)

- `swift/Components/TextVariant.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Components/TextVariant.swift)

5. Add the files to your app target.

**Jetpack Compose**

1. Set `minSdk` to 24 or higher and add the dependencies:

```kotlin title="build.gradle.kts"
implementation(platform("androidx.compose:compose-bom:2026.09.00"))
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.ui:ui")
implementation("com.adamglin:phosphor-icon-android:1.0.0")
```

2. Copy the theme files every view uses:

- `kotlin/theme/Theme.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/theme/Theme.kt)

- `kotlin/theme/Tokens.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/theme/Tokens.kt)

3. Copy the source and every file it uses:

```kt title="kotlin/components/Calendar.kt"
package com.esuiss.ui.components

import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.semantics.LiveRegionMode
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.liveRegion
import androidx.compose.ui.semantics.selected
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.CaretDown
import com.adamglin.phosphoricons.regular.CaretLeft
import com.adamglin.phosphoricons.regular.CaretRight
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Typography
import java.util.Calendar as JavaCalendar
import java.util.Date
import java.util.Locale

enum class CalendarSize(internal val cell: Dp, internal val padding: Dp) {
    Sm(24.dp, 8.dp),
    Default(32.dp, 12.dp),
    Lg(40.dp, 16.dp),
}

enum class CalendarCaptionLayout { Label, Dropdown, DropdownMonths, DropdownYears }

@Immutable
data class DateRange(val from: Date?, val to: Date? = null)

@Immutable
data class CalendarDayState(
    val selected: Boolean = false,
    val today: Boolean = false,
    val rangeStart: Boolean = false,
    val rangeMiddle: Boolean = false,
    val rangeEnd: Boolean = false,
    val outside: Boolean = false,
    val disabled: Boolean = false,
)

typealias CalendarDayButtonSlot = @Composable (date: Date, state: CalendarDayState, onClick: () -> Unit) -> Unit

@Composable
fun Calendar(
    selected: Date?,
    onSelect: (Date?) -> Unit,
    modifier: Modifier = Modifier,
    defaultMonth: Date? = null,
    numberOfMonths: Int = 1,
    startMonth: Date? = null,
    endMonth: Date? = null,
    showOutsideDays: Boolean = false,
    showWeekNumber: Boolean = false,
    weekStartsOn: Int? = null,
    disabled: (Date) -> Boolean = { false },
    today: Date = remember { Date() },
    locale: Locale = Locale.getDefault(),
    buttonVariant: ButtonVariant = ButtonVariant.Plain,
    size: CalendarSize = CalendarSize.Default,
    captionLayout: CalendarCaptionLayout = CalendarCaptionLayout.Label,
    dayButton: CalendarDayButtonSlot? = null,
) {
    val selectedKey = selected?.let(::dayKey)
    CalendarGrid(
        modifier = modifier,
        initialMonth = defaultMonth ?: selected ?: today,
        numberOfMonths = numberOfMonths,
        startMonth = startMonth,
        endMonth = endMonth,
        showOutsideDays = showOutsideDays,
        showWeekNumber = showWeekNumber,
        weekStartsOn = weekStartsOn,
        disabled = disabled,
        today = today,
        locale = locale,
        buttonVariant = buttonVariant,
        size = size,
        captionLayout = captionLayout,
        dayButton = dayButton,
        selection = { key -> if (key == selectedKey) DaySelection.Single else DaySelection.None },
        onDayClick = { date -> onSelect(if (dayKey(date) == selectedKey) null else date) },
    )
}

@Composable
fun Calendar(
    selected: DateRange?,
    onSelect: (DateRange?) -> Unit,
    modifier: Modifier = Modifier,
    defaultMonth: Date? = null,
    numberOfMonths: Int = 1,
    startMonth: Date? = null,
    endMonth: Date? = null,
    showOutsideDays: Boolean = false,
    showWeekNumber: Boolean = false,
    weekStartsOn: Int? = null,
    disabled: (Date) -> Boolean = { false },
    today: Date = remember { Date() },
    locale: Locale = Locale.getDefault(),
    buttonVariant: ButtonVariant = ButtonVariant.Plain,
    size: CalendarSize = CalendarSize.Default,
    captionLayout: CalendarCaptionLayout = CalendarCaptionLayout.Label,
    dayButton: CalendarDayButtonSlot? = null,
) {
    val from = selected?.from?.let(::dayKey)
    val to = selected?.to?.let(::dayKey)
    CalendarGrid(
        modifier = modifier,
        initialMonth = defaultMonth ?: selected?.from ?: today,
        numberOfMonths = numberOfMonths,
        startMonth = startMonth,
        endMonth = endMonth,
        showOutsideDays = showOutsideDays,
        showWeekNumber = showWeekNumber,
        weekStartsOn = weekStartsOn,
        disabled = disabled,
        today = today,
        locale = locale,
        buttonVariant = buttonVariant,
        size = size,
        captionLayout = captionLayout,
        dayButton = dayButton,
        selection = { key ->
            when {
                from == null -> DaySelection.None
                to == null -> if (key == from) DaySelection.RangeBoth else DaySelection.None
                key == from && key == to -> DaySelection.RangeBoth
                key == from -> DaySelection.RangeStart
                key == to -> DaySelection.RangeEnd
                key in from..to -> DaySelection.RangeMiddle
                else -> DaySelection.None
            }
        },
        onDayClick = { date -> onSelect(addToRange(date, selected)) },
    )
}

@Composable
fun Calendar(
    selected: List<Date>,
    onSelect: (List<Date>) -> Unit,
    modifier: Modifier = Modifier,
    defaultMonth: Date? = null,
    numberOfMonths: Int = 1,
    startMonth: Date? = null,
    endMonth: Date? = null,
    showOutsideDays: Boolean = false,
    showWeekNumber: Boolean = false,
    weekStartsOn: Int? = null,
    disabled: (Date) -> Boolean = { false },
    today: Date = remember { Date() },
    locale: Locale = Locale.getDefault(),
    buttonVariant: ButtonVariant = ButtonVariant.Plain,
    size: CalendarSize = CalendarSize.Default,
    captionLayout: CalendarCaptionLayout = CalendarCaptionLayout.Label,
    dayButton: CalendarDayButtonSlot? = null,
) {
    val keys = selected.map(::dayKey).toSet()
    CalendarGrid(
        modifier = modifier,
        initialMonth = defaultMonth ?: selected.firstOrNull() ?: today,
        numberOfMonths = numberOfMonths,
        startMonth = startMonth,
        endMonth = endMonth,
        showOutsideDays = showOutsideDays,
        showWeekNumber = showWeekNumber,
        weekStartsOn = weekStartsOn,
        disabled = disabled,
        today = today,
        locale = locale,
        buttonVariant = buttonVariant,
        size = size,
        captionLayout = captionLayout,
        dayButton = dayButton,
        selection = { key -> if (key in keys) DaySelection.Single else DaySelection.None },
        onDayClick = { date ->
            val key = dayKey(date)
            onSelect(if (key in keys) selected.filter { dayKey(it) != key } else selected + date)
        },
    )
}

@Composable
fun CalendarDayButton(
    date: Date,
    state: CalendarDayState,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    size: CalendarSize = CalendarSize.Default,
    locale: Locale = Locale.getDefault(),
) {
    val colors = EsuissTheme.colors
    val interaction = remember { MutableInteractionSource() }
    val pressed by interaction.collectIsPressedAsState()
    val range = state.rangeStart || state.rangeEnd
    val single = state.selected && !range && !state.rangeMiddle
    val background = when {
        range -> colors.primary
        single && state.today -> colors.primary
        single -> colors.primary.copy(alpha = if (colors.isDark) 0.2f else 0.1f)
        else -> Color.Transparent
    }
    val foreground = when {
        range -> colors.primaryForeground
        single && state.today -> colors.primaryForeground
        state.rangeMiddle -> colors.foreground
        single -> colors.primary
        state.outside -> colors.mutedForeground
        state.today -> colors.primary
        else -> colors.foreground
    }
    val bold = state.today || single
    val style = dayTextStyle(size).copy(
        color = foreground,
        fontWeight = if (bold) FontWeight.SemiBold else FontWeight.Normal,
        fontFeatureSettings = "tnum",
    )
    val description = remember(date, locale, state.today) {
        val full = android.icu.text.DateFormat.getDateInstance(android.icu.text.DateFormat.FULL, locale).format(date)
        if (state.today) "${todayLabel(locale)}, $full" else full
    }
    val number = remember(date) { JavaCalendar.getInstance().apply { time = date }.get(JavaCalendar.DAY_OF_MONTH) }
    Box(
        modifier = modifier
            .fillMaxSize()
            .alpha(
                when {
                    state.disabled -> 0.5f
                    pressed -> 0.8f
                    else -> 1f
                },
            )
            .clip(CircleShape)
            .background(background, CircleShape)
            .clickable(
                interactionSource = interaction,
                indication = null,
                enabled = !state.disabled,
                role = Role.Button,
                onClick = onClick,
            )
            .semantics {
                contentDescription = description
                selected = state.selected || state.rangeStart || state.rangeEnd || state.rangeMiddle
            },
        contentAlignment = Alignment.Center,
    ) {
        BasicText(number.toString(), style = style, maxLines = 1)
    }
}

private enum class DaySelection { None, Single, RangeStart, RangeMiddle, RangeEnd, RangeBoth }

private fun dayTextStyle(size: CalendarSize): TextStyle = when (size) {
    CalendarSize.Sm -> Typography.xs
    CalendarSize.Default -> Typography.sm
    CalendarSize.Lg -> Typography.lg
}

private fun captionStyle(size: CalendarSize): TextStyle = when (size) {
    CalendarSize.Sm -> Typography.xs
    CalendarSize.Default -> Typography.sm
    CalendarSize.Lg -> Typography.base
}

private fun todayLabel(locale: Locale): String =
    android.icu.text.RelativeDateTimeFormatter.getInstance(locale).format(
        android.icu.text.RelativeDateTimeFormatter.Direction.THIS,
        android.icu.text.RelativeDateTimeFormatter.AbsoluteUnit.DAY,
    ).replaceFirstChar { it.titlecase(locale) }

private fun monthLabel(locale: Locale, direction: android.icu.text.RelativeDateTimeFormatter.Direction): String =
    android.icu.text.RelativeDateTimeFormatter.getInstance(locale).format(
        direction,
        android.icu.text.RelativeDateTimeFormatter.AbsoluteUnit.MONTH,
    ).replaceFirstChar { it.titlecase(locale) }

private fun dayKey(date: Date): Int {
    val calendar = JavaCalendar.getInstance().apply { time = date }
    return calendar.get(JavaCalendar.YEAR) * 10000 + calendar.get(JavaCalendar.MONTH) * 100 + calendar.get(JavaCalendar.DAY_OF_MONTH)
}

private fun monthIndex(date: Date): Int {
    val calendar = JavaCalendar.getInstance().apply { time = date }
    return calendar.get(JavaCalendar.YEAR) * 12 + calendar.get(JavaCalendar.MONTH)
}

private fun dateOf(year: Int, month: Int, day: Int): Date =
    JavaCalendar.getInstance().apply {
        clear()
        set(year, month, day)
    }.time

private fun addToRange(date: Date, range: DateRange?): DateRange? {
    val key = dayKey(date)
    val from = range?.from
    val to = range?.to
    return when {
        from == null -> DateRange(date, null)
        to == null -> when {
            key == dayKey(from) -> DateRange(from, from)
            key < dayKey(from) -> DateRange(date, from)
            else -> DateRange(from, date)
        }
        key == dayKey(from) && key == dayKey(to) -> null
        key == dayKey(from) || key == dayKey(to) -> DateRange(date, null)
        key < dayKey(from) -> DateRange(date, to)
        else -> DateRange(from, date)
    }
}

@Composable
private fun CalendarGrid(
    modifier: Modifier,
    initialMonth: Date,
    numberOfMonths: Int,
    startMonth: Date?,
    endMonth: Date?,
    showOutsideDays: Boolean,
    showWeekNumber: Boolean,
    weekStartsOn: Int?,
    disabled: (Date) -> Boolean,
    today: Date,
    locale: Locale,
    buttonVariant: ButtonVariant,
    size: CalendarSize,
    captionLayout: CalendarCaptionLayout,
    dayButton: CalendarDayButtonSlot?,
    selection: (Int) -> DaySelection,
    onDayClick: (Date) -> Unit,
) {
    val colors = EsuissTheme.colors
    val first = startMonth?.let(::monthIndex)
    val last = endMonth?.let(::monthIndex)?.minus(numberOfMonths - 1)
    var month by remember { mutableIntStateOf(monthIndex(initialMonth)) }
    val canPrevious = first == null || month > first
    val canNext = last == null || month < last
    val currentMonth by rememberUpdatedState(month)
    val currentCanPrevious by rememberUpdatedState(canPrevious)
    val currentCanNext by rememberUpdatedState(canNext)
    val rtl = LocalLayoutDirection.current == LayoutDirection.Rtl
    val weekStart = weekStartsOn ?: (JavaCalendar.getInstance(locale).firstDayOfWeek - 1)
    val todayKey = dayKey(today)
    var direction by remember { mutableIntStateOf(1) }
    val go: (Int) -> Unit = { step ->
        direction = step
        month = currentMonth + step
    }

    Column(
        modifier = modifier
            .width(IntrinsicSize.Max)
            .background(colors.background)
            .padding(size.padding)
            .pointerInput(rtl) {
                var total = 0f
                detectHorizontalDragGestures(
                    onDragStart = { total = 0f },
                    onDragEnd = {
                        val threshold = 48.dp.toPx()
                        val forward = if (rtl) total > threshold else total < -threshold
                        val back = if (rtl) total < -threshold else total > threshold
                        if (forward && currentCanNext) go(1)
                        if (back && currentCanPrevious) go(-1)
                    },
                ) { change, amount ->
                    change.consume()
                    total += amount
                }
            },
    ) {
        AnimatedContent(
            targetState = month,
            transitionSpec = {
                val sign = if (direction >= 0) 1 else -1
                (slideInHorizontally(tween(250)) { it / 3 * sign } + fadeIn(tween(250))) togetherWith
                    (slideOutHorizontally(tween(250)) { -it / 3 * sign } + fadeOut(tween(150)))
            },
            label = "calendar-month",
        ) { shown ->
            Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
                repeat(numberOfMonths) { offset ->
                    CalendarMonth(
                        monthIndex = shown + offset,
                        showNav = offset == 0,
                        canPrevious = canPrevious,
                        canNext = canNext,
                        onPrevious = { go(-1) },
                        onNext = { go(1) },
                        onMonthSelect = { target -> go(target - offset - currentMonth) },
                        captionLayout = captionLayout,
                        firstMonth = first,
                        lastMonth = endMonth?.let(::monthIndex),
                        showOutsideDays = showOutsideDays,
                        showWeekNumber = showWeekNumber,
                        weekStart = weekStart,
                        disabled = disabled,
                        todayKey = todayKey,
                        locale = locale,
                        buttonVariant = buttonVariant,
                        size = size,
                        dayButton = dayButton,
                        selection = selection,
                        onDayClick = onDayClick,
                    )
                }
            }
        }
    }
}

@Composable
private fun CalendarMonth(
    monthIndex: Int,
    showNav: Boolean,
    canPrevious: Boolean,
    canNext: Boolean,
    onPrevious: () -> Unit,
    onNext: () -> Unit,
    onMonthSelect: (Int) -> Unit,
    captionLayout: CalendarCaptionLayout,
    firstMonth: Int?,
    lastMonth: Int?,
    showOutsideDays: Boolean,
    showWeekNumber: Boolean,
    weekStart: Int,
    disabled: (Date) -> Boolean,
    todayKey: Int,
    locale: Locale,
    buttonVariant: ButtonVariant,
    size: CalendarSize,
    dayButton: CalendarDayButtonSlot?,
    selection: (Int) -> DaySelection,
    onDayClick: (Date) -> Unit,
) {
    val colors = EsuissTheme.colors
    val year = Math.floorDiv(monthIndex, 12)
    val month = Math.floorMod(monthIndex, 12)
    val firstDay = dateOf(year, month, 1)
    val caption = remember(monthIndex, locale) {
        android.icu.text.DateFormat.getInstanceForSkeleton("yMMMM", locale).format(firstDay)
    }
    val calendar = JavaCalendar.getInstance().apply { time = firstDay }
    val daysInMonth = calendar.getActualMaximum(JavaCalendar.DAY_OF_MONTH)
    val leading = Math.floorMod(calendar.get(JavaCalendar.DAY_OF_WEEK) - 1 - weekStart, 7)
    val rows = (leading + daysInMonth + 6) / 7
    val weekdayFormat = remember(locale) { android.icu.text.SimpleDateFormat("EEEEEE", locale) }
    val weekdayFull = remember(locale) { android.icu.text.SimpleDateFormat("EEEE", locale) }
    val weekdayStyle = when (size) {
        CalendarSize.Lg -> Typography.xs
        else -> Typography.xs
    }.copy(color = colors.mutedForeground, fontWeight = FontWeight.SemiBold)
    val cell = size.cell
    val previousLabel = remember(locale) { monthLabel(locale, android.icu.text.RelativeDateTimeFormatter.Direction.LAST) }
    val nextLabel = remember(locale) { monthLabel(locale, android.icu.text.RelativeDateTimeFormatter.Direction.NEXT) }

    Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
        Box(modifier = Modifier.fillMaxWidth().height(cell)) {
            CalendarCaption(
                caption = caption,
                monthIndex = monthIndex,
                captionLayout = captionLayout,
                firstMonth = firstMonth,
                lastMonth = lastMonth,
                locale = locale,
                size = size,
                onMonthSelect = onMonthSelect,
                modifier = Modifier
                    .align(Alignment.CenterStart)
                    .padding(start = if (captionLayout == CalendarCaptionLayout.Label) 6.dp else 0.dp, end = cell * 2),
            )
            if (showNav) {
                Row(
                    modifier = Modifier.align(Alignment.CenterEnd),
                    horizontalArrangement = Arrangement.spacedBy(2.dp),
                ) {
                    CalendarNavButton(PhosphorIcons.Regular.CaretLeft, previousLabel, canPrevious, buttonVariant, cell, onPrevious)
                    CalendarNavButton(PhosphorIcons.Regular.CaretRight, nextLabel, canNext, buttonVariant, cell, onNext)
                }
            }
        }
        Column {
            Row(modifier = Modifier.fillMaxWidth()) {
                if (showWeekNumber) Box(Modifier.width(cell))
                repeat(7) { column ->
                    val weekday = dateOf(2000, 0, 2 + Math.floorMod(weekStart + column, 7))
                    val short = weekdayFormat.format(weekday)
                    BasicText(
                        if (size == CalendarSize.Sm) short else short.uppercase(locale),
                        modifier = Modifier
                            .weight(1f)
                            .widthIn(min = cell)
                            .clearAndSetSemantics { contentDescription = weekdayFull.format(weekday) },
                        style = weekdayStyle.copy(textAlign = TextAlign.Center),
                        maxLines = 1,
                    )
                }
            }
            repeat(rows) { row ->
                Row(modifier = Modifier.fillMaxWidth().padding(top = 8.dp)) {
                    if (showWeekNumber) {
                        val rowStart = dateOf(year, month, 1 - leading + row * 7)
                        val week = JavaCalendar.getInstance(locale).apply {
                            firstDayOfWeek = weekStart + 1
                            time = rowStart
                        }.get(JavaCalendar.WEEK_OF_YEAR)
                        Box(Modifier.size(cell), contentAlignment = Alignment.Center) {
                            BasicText(week.toString(), style = Typography.xs.copy(color = colors.mutedForeground))
                        }
                    }
                    repeat(7) { column ->
                        val dayNumber = row * 7 + column - leading + 1
                        val date = dateOf(year, month, dayNumber)
                        val outside = dayNumber < 1 || dayNumber > daysInMonth
                        DayCell(
                            date = date,
                            column = column,
                            visible = !outside || showOutsideDays,
                            outside = outside,
                            disabled = disabled(date),
                            todayKey = todayKey,
                            cell = cell,
                            size = size,
                            locale = locale,
                            dayButton = dayButton,
                            selection = if (outside) DaySelection.None else selection(dayKey(date)),
                            onClick = { onDayClick(date) },
                        )
                    }
                }
            }
        }
    }
}

@Composable
private fun CalendarCaption(
    caption: String,
    monthIndex: Int,
    captionLayout: CalendarCaptionLayout,
    firstMonth: Int?,
    lastMonth: Int?,
    locale: Locale,
    size: CalendarSize,
    onMonthSelect: (Int) -> Unit,
    modifier: Modifier,
) {
    val colors = EsuissTheme.colors
    val style = captionStyle(size).copy(color = colors.foreground, fontWeight = FontWeight.SemiBold)
    val semantics = Modifier.semantics {
        heading()
        liveRegion = LiveRegionMode.Polite
    }
    if (captionLayout == CalendarCaptionLayout.Label) {
        BasicText(caption, modifier = modifier.then(semantics), style = style, maxLines = 1, softWrap = false)
        return
    }
    val year = Math.floorDiv(monthIndex, 12)
    val month = Math.floorMod(monthIndex, 12)
    val firstYear = firstMonth?.let { Math.floorDiv(it, 12) } ?: year
    val lastYear = lastMonth?.let { Math.floorDiv(it, 12) } ?: year
    val shortMonth = remember(locale) { android.icu.text.SimpleDateFormat("LLL", locale) }
    val longMonth = remember(locale) { android.icu.text.SimpleDateFormat("LLLL", locale) }
    val outOfRange = { target: Int -> (firstMonth != null && target < firstMonth) || (lastMonth != null && target > lastMonth) }
    Row(
        modifier = modifier.then(semantics),
        horizontalArrangement = Arrangement.spacedBy(6.dp),
        verticalAlignment = Alignment.CenterVertically,
    ) {
        if (captionLayout == CalendarCaptionLayout.DropdownYears) {
            BasicText(longMonth.format(dateOf(year, month, 1)), style = style, maxLines = 1, softWrap = false)
        } else {
            CalendarDropdown(
                label = "Choose the Month",
                text = shortMonth.format(dateOf(year, month, 1)),
                value = month.toString(),
                options = List(12) { index ->
                    Triple(index.toString(), shortMonth.format(dateOf(year, index, 1)), !outOfRange(year * 12 + index))
                },
                style = style,
                onChange = { onMonthSelect(year * 12 + it.toInt()) },
            )
        }
        if (captionLayout == CalendarCaptionLayout.DropdownMonths) {
            BasicText(year.toString(), style = style, maxLines = 1, softWrap = false)
        } else {
            CalendarDropdown(
                label = "Choose the Year",
                text = year.toString(),
                value = year.toString(),
                options = (firstYear..maxOf(firstYear, lastYear)).map { Triple(it.toString(), it.toString(), true) },
                style = style,
                onChange = { onMonthSelect(it.toInt() * 12 + month) },
            )
        }
    }
}

@Composable
private fun CalendarDropdown(
    label: String,
    text: String,
    value: String,
    options: List<Triple<String, String, Boolean>>,
    style: TextStyle,
    onChange: (String) -> Unit,
) {
    val colors = EsuissTheme.colors
    var expanded by remember { mutableStateOf(false) }
    val interaction = remember { MutableInteractionSource() }
    val pressed by interaction.collectIsPressedAsState()
    DropdownMenu(
        expanded = expanded,
        onExpandedChange = { expanded = it },
        trigger = {
            Row(
                modifier = Modifier
                    .clip(CircleShape)
                    .alpha(if (pressed) 0.8f else 1f)
                    .clickable(
                        interactionSource = interaction,
                        indication = null,
                        role = Role.DropdownList,
                        onClick = { expanded = !expanded },
                    )
                    .semantics { contentDescription = label }
                    .padding(horizontal = 6.dp, vertical = 2.dp),
                horizontalArrangement = Arrangement.spacedBy(4.dp),
                verticalAlignment = Alignment.CenterVertically,
            ) {
                BasicText(text, style = style, maxLines = 1, softWrap = false)
                Icon(PhosphorIcons.Regular.CaretDown, contentDescription = null, size = 14.dp, tint = colors.mutedForeground)
            }
        },
    ) {
        DropdownMenuRadioGroup(
            value = value,
            onValueChange = {
                expanded = false
                if (it != value) onChange(it)
            },
        ) {
            options.forEach { (option, optionLabel, enabled) ->
                DropdownMenuRadioItem(option, optionLabel, enabled = enabled)
            }
        }
    }
}

@Composable
private fun RowScope.DayCell(
    date: Date,
    column: Int,
    visible: Boolean,
    outside: Boolean,
    disabled: Boolean,
    todayKey: Int,
    cell: Dp,
    size: CalendarSize,
    locale: Locale,
    dayButton: CalendarDayButtonSlot?,
    selection: DaySelection,
    onClick: () -> Unit,
) {
    val colors = EsuissTheme.colors
    Box(
        modifier = Modifier
            .weight(1f)
            .widthIn(min = cell)
            .aspectRatio(1f),
    ) {
        if (!visible) return@Box
        val inRange = selection == DaySelection.RangeStart || selection == DaySelection.RangeMiddle || selection == DaySelection.RangeEnd
        if (inRange) {
            val roundStart = selection == DaySelection.RangeStart || column == 0
            val roundEnd = selection == DaySelection.RangeEnd || column == 6
            val radius = cell
            Box(
                modifier = Modifier
                    .fillMaxSize()
                    .background(
                        colors.muted,
                        RoundedCornerShape(
                            topStart = if (roundStart) radius else 0.dp,
                            bottomStart = if (roundStart) radius else 0.dp,
                            topEnd = if (roundEnd) radius else 0.dp,
                            bottomEnd = if (roundEnd) radius else 0.dp,
                        ),
                    ),
            )
        }
        val state = CalendarDayState(
            selected = selection != DaySelection.None,
            today = dayKey(date) == todayKey,
            rangeStart = selection == DaySelection.RangeStart || selection == DaySelection.RangeBoth,
            rangeMiddle = selection == DaySelection.RangeMiddle,
            rangeEnd = selection == DaySelection.RangeEnd || selection == DaySelection.RangeBoth,
            outside = outside,
            disabled = disabled,
        )
        if (dayButton != null) {
            dayButton(date, state, onClick)
        } else {
            CalendarDayButton(date, state, onClick, size = size, locale = locale)
        }
    }
}

@Composable
private fun CalendarNavButton(
    icon: ImageVector,
    label: String,
    enabled: Boolean,
    variant: ButtonVariant,
    cell: Dp,
    onClick: () -> Unit,
) {
    val colors = EsuissTheme.colors
    val interaction = remember { MutableInteractionSource() }
    val pressed by interaction.collectIsPressedAsState()
    val background = when (variant) {
        ButtonVariant.Secondary -> colors.secondary
        ButtonVariant.Tinted -> colors.primary.copy(alpha = if (colors.isDark) 0.2f else 0.1f)
        ButtonVariant.Default -> colors.primary
        else -> Color.Transparent
    }
    val foreground = when {
        variant == ButtonVariant.Default -> colors.primaryForeground
        variant == ButtonVariant.Ghost || variant == ButtonVariant.Secondary || variant == ButtonVariant.Outline -> colors.foreground
        else -> colors.primary
    }
    val rtl = LocalLayoutDirection.current == LayoutDirection.Rtl
    Box(
        modifier = Modifier
            .size(cell)
            .alpha(
                when {
                    !enabled -> 0.5f
                    pressed -> 0.8f
                    else -> 1f
                },
            )
            .clip(CircleShape)
            .background(background, CircleShape)
            .clickable(
                interactionSource = interaction,
                indication = null,
                enabled = enabled,
                role = Role.Button,
                onClick = onClick,
            )
            .semantics { contentDescription = label },
        contentAlignment = Alignment.Center,
    ) {
        Icon(
            icon,
            contentDescription = null,
            size = 16.dp,
            tint = foreground,
            modifier = if (rtl) Modifier.graphicsLayer { scaleX = -1f } else Modifier,
        )
    }
}
```

- `kotlin/components/Button.kt`: see [Button](https://ui.suiss.app/docs/components/button.md)

- `kotlin/components/ButtonGroup.kt`: see [Button Group](https://ui.suiss.app/docs/components/button-group.md)

- `kotlin/components/Checkbox.kt`: see [Checkbox](https://ui.suiss.app/docs/components/checkbox.md)

- `kotlin/components/Content.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/components/Content.kt)

- `kotlin/components/DropdownMenu.kt`: see [Dropdown Menu](https://ui.suiss.app/docs/components/dropdown-menu.md)

- `kotlin/components/Field.kt`: see [Field](https://ui.suiss.app/docs/components/field.md)

- `kotlin/components/FormSupport.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/components/FormSupport.kt)

- `kotlin/components/Haptics.kt`: see [Haptics](https://ui.suiss.app/docs/components/haptics.md)

- `kotlin/components/Icon.kt`: see [Icon](https://ui.suiss.app/docs/components/icon.md)

- `kotlin/components/Label.kt`: see [Label](https://ui.suiss.app/docs/components/label.md)

- `kotlin/components/Overlay.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/components/Overlay.kt)

- `kotlin/components/Separator.kt`: see [Separator](https://ui.suiss.app/docs/components/separator.md)

4. Update the `com.esuiss.ui` package names to match your project.

## Usage

```tsx
import { CalendarDayButton, Calendar } from "@/components/ui/calendar"
```

```tsx
<SingleCalendar />
```

Pass `mode`, `selected` and `onSelect` to control the selection; `selected` is a `Date`, a `Date[]` or a `{ from, to }` range depending on the mode. Use `month`, `defaultMonth` and `onMonthChange` to control the visible month, and `numberOfMonths` to show several months.

Use `disabled` with date matchers, such as `{ dayOfWeek: [0, 6] }`, to block days. Limit navigation with `startMonth` and `endMonth`; with a year dropdown they default to the last 100 years.

Set `locale` for month and weekday names, and `weekStartsOn` to change the first day of the week. `CalendarDayButton` is exported for custom day rendering.

## Examples

### Sizes

The `sm`, `default` and `lg` sizes.

![Preview](https://ui.suiss.app/previews/components-calendar--sizes.light.webp)

```tsx
import * as React from "react"
import { ScrollView } from "react-native"
import { Calendar } from "@/components/ui/calendar"

type CalendarProps = React.ComponentProps<typeof Calendar>

function SingleCalendar(
  props: Omit<CalendarProps, "mode" | "selected" | "onSelect">
) {
  const today = new Date()
  const [date, setDate] = React.useState<Date | undefined>(
    new Date(today.getFullYear(), today.getMonth(), today.getDate() + 3)
  )
  return (
    <Calendar
      {...props}
      mode="single"
      selected={date}
      onSelect={setDate}
      className="rounded-2xl border border-border"
    />
  )
}

export function CalendarSizesExample() {
  return (
    <ScrollView contentContainerClassName="items-start gap-6">
      <SingleCalendar size="sm" />
      <SingleCalendar size="default" />
      <SingleCalendar size="lg" />
    </ScrollView>
  )
}
```

```swift
import EsuissUI
import SwiftUI

struct CalendarSizesExample: View {
  var body: some View {
    VStack(alignment: .leading, spacing: 24) {
      CalendarSingleDemo(size: .sm)
      CalendarSingleDemo(size: .default)
      CalendarSingleDemo(size: .lg)
    }
  }
}

enum CalendarDates {
  static func day(_ offset: Int) -> Date {
    let calendar = Foundation.Calendar.current
    let start = calendar.startOfDay(for: Date())
    return calendar.date(byAdding: .day, value: offset, to: start) ?? start
  }

  static func dayOfMonth(_ day: Int) -> Date {
    let calendar = Foundation.Calendar.current
    var components = calendar.dateComponents([.year, .month], from: Date())
    components.day = day
    return calendar.date(from: components) ?? Date()
  }
}

struct CalendarBordered: ViewModifier {
  func body(content: Content) -> some View {
    content
      .clipShape(RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous))
      .overlay {
        RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous).strokeBorder(EsuissColor.border)
      }
  }
}

struct CalendarSingleDemo: View {
  var size: DateCalendar.Size = .default
  var buttonVariant: VariantButtonStyle.Variant = .plain
  var captionLayout: DateCalendar.CaptionLayout = .label
  var showOutsideDays = false
  var showWeekNumber = false
  var weekendsDisabled = false

  @State private var date: Date? = CalendarDates.day(3)

  var body: some View {
    DateCalendar(
      selection: $date,
      size: size,
      buttonVariant: buttonVariant,
      captionLayout: captionLayout,
      showOutsideDays: showOutsideDays,
      showWeekNumber: showWeekNumber,
      isDisabled: { day in
        guard weekendsDisabled else { return false }
        let weekday = Foundation.Calendar.current.component(.weekday, from: day)
        return weekday == 1 || weekday == 7
      }
    )
    .modifier(CalendarBordered())
  }
}

func day(_ offset: Int) -> Date {
  let calendar = Foundation.Calendar.current
  let start = calendar.startOfDay(for: Date())
  return calendar.date(byAdding: .day, value: offset, to: start) ?? start
}

func dayOfMonth(_ day: Int) -> Date {
  let calendar = Foundation.Calendar.current
  var components = calendar.dateComponents([.year, .month], from: Date())
  components.day = day
  return calendar.date(from: components) ?? Date()
}
```

```kotlin
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Calendar
import com.esuiss.ui.components.CalendarCaptionLayout
import com.esuiss.ui.components.CalendarSize
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Radius
import java.util.Date
import java.util.GregorianCalendar
import java.util.Locale

fun calendarDay(offsetDays: Int = 0, dayOfMonth: Int? = null): Date {
    val now = GregorianCalendar()
    val result = GregorianCalendar(now.get(GregorianCalendar.YEAR), now.get(GregorianCalendar.MONTH), now.get(GregorianCalendar.DAY_OF_MONTH))
    if (dayOfMonth != null) result.set(GregorianCalendar.DAY_OF_MONTH, dayOfMonth)
    result.add(GregorianCalendar.DAY_OF_MONTH, offsetDays)
    return result.time
}

fun calendarIsWeekend(date: Date): Boolean {
    val day = GregorianCalendar().apply { time = date }.get(GregorianCalendar.DAY_OF_WEEK)
    return day == GregorianCalendar.SATURDAY || day == GregorianCalendar.SUNDAY
}

@Composable
fun CalendarFrame(content: @Composable () -> Unit) {
    Box(
        Modifier
            .border(1.dp, EsuissTheme.colors.border, RoundedCornerShape(Radius.xl2))
            .padding(1.dp),
    ) {
        content()
    }
}

@Composable
fun CalendarSingle(
    size: CalendarSize = CalendarSize.Default,
    captionLayout: CalendarCaptionLayout = CalendarCaptionLayout.Label,
    showWeekNumber: Boolean = false,
    showOutsideDays: Boolean = false,
    weekendsDisabled: Boolean = false,
    buttonVariant: ButtonVariant = ButtonVariant.Plain,
    locale: Locale = Locale.getDefault(),
    key: String = "",
) {
    var date by rememberSaveable(key) { mutableStateOf<Date?>(calendarDay(offsetDays = 3)) }
    CalendarFrame {
        Calendar(
            selected = date,
            onSelect = { date = it },
            size = size,
            captionLayout = captionLayout,
            showWeekNumber = showWeekNumber,
            showOutsideDays = showOutsideDays,
            disabled = { weekendsDisabled && calendarIsWeekend(it) },
            buttonVariant = buttonVariant,
            locale = locale,
        )
    }
}

@Composable
fun CalendarSizes() {
    Column(verticalArrangement = Arrangement.spacedBy(24.dp)) {
        CalendarSingle(size = CalendarSize.Sm, key = "sm")
        CalendarSingle(size = CalendarSize.Default, key = "default")
        CalendarSingle(size = CalendarSize.Lg, key = "lg")
    }
}
```

### Range

Range selection across two months.

![Preview](https://ui.suiss.app/previews/components-calendar--range.light.webp)

```tsx
import * as React from "react"
import { ScrollView } from "react-native"
import { DateRange } from "@/components/primitives/calendar"
import { Calendar } from "@/components/ui/calendar"

function RangeCalendar() {
  const today = new Date()
  const [range, setRange] = React.useState<DateRange | undefined>({
    from: today,
    to: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 6),
  })
  return (
    <ScrollView>
      <Calendar
        mode="range"
        selected={range}
        onSelect={setRange}
        numberOfMonths={2}
        className="rounded-2xl border border-border"
      />
    </ScrollView>
  )
}

export function CalendarRangeExample() {
  return (
    <RangeCalendar />
  )
}
```

```swift
import EsuissUI
import SwiftUI

enum CalendarDates {
  static func day(_ offset: Int) -> Date {
    let calendar = Foundation.Calendar.current
    let start = calendar.startOfDay(for: Date())
    return calendar.date(byAdding: .day, value: offset, to: start) ?? start
  }

  static func dayOfMonth(_ day: Int) -> Date {
    let calendar = Foundation.Calendar.current
    var components = calendar.dateComponents([.year, .month], from: Date())
    components.day = day
    return calendar.date(from: components) ?? Date()
  }
}

struct CalendarBordered: ViewModifier {
  func body(content: Content) -> some View {
    content
      .clipShape(RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous))
      .overlay {
        RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous).strokeBorder(EsuissColor.border)
      }
  }
}

struct CalendarRangeDemo: View {
  @State private var range: DateRange? = DateRange(from: CalendarDates.day(0), to: CalendarDates.day(6))

  var body: some View {
    DateCalendar(selection: $range, numberOfMonths: 2)
      .modifier(CalendarBordered())
  }
}

func day(_ offset: Int) -> Date {
  let calendar = Foundation.Calendar.current
  let start = calendar.startOfDay(for: Date())
  return calendar.date(byAdding: .day, value: offset, to: start) ?? start
}

func dayOfMonth(_ day: Int) -> Date {
  let calendar = Foundation.Calendar.current
  var components = calendar.dateComponents([.year, .month], from: Date())
  components.day = day
  return calendar.date(from: components) ?? Date()
}
```

```kotlin
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.Calendar
import com.esuiss.ui.components.DateRange
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Radius
import java.util.Date
import java.util.GregorianCalendar

fun calendarDay(offsetDays: Int = 0, dayOfMonth: Int? = null): Date {
    val now = GregorianCalendar()
    val result = GregorianCalendar(now.get(GregorianCalendar.YEAR), now.get(GregorianCalendar.MONTH), now.get(GregorianCalendar.DAY_OF_MONTH))
    if (dayOfMonth != null) result.set(GregorianCalendar.DAY_OF_MONTH, dayOfMonth)
    result.add(GregorianCalendar.DAY_OF_MONTH, offsetDays)
    return result.time
}

@Composable
fun CalendarFrame(content: @Composable () -> Unit) {
    Box(
        Modifier
            .border(1.dp, EsuissTheme.colors.border, RoundedCornerShape(Radius.xl2))
            .padding(1.dp),
    ) {
        content()
    }
}

@Composable
fun CalendarRange() {
    var from by rememberSaveable { mutableStateOf<Date?>(calendarDay()) }
    var to by rememberSaveable { mutableStateOf<Date?>(calendarDay(offsetDays = 6)) }
    CalendarFrame {
        Calendar(
            selected = if (from == null) null else DateRange(from, to),
            onSelect = { range: DateRange? ->
                from = range?.from
                to = range?.to
            },
            numberOfMonths = 2,
        )
    }
}
```

### Multiple

Several separate dates selected with `mode="multiple"`.

![Preview](https://ui.suiss.app/previews/components-calendar--multiple.light.webp)

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

function MultipleCalendar() {
  const today = new Date()
  const [dates, setDates] = React.useState<Date[] | undefined>([
    new Date(today.getFullYear(), today.getMonth(), 8),
    new Date(today.getFullYear(), today.getMonth(), 15),
    new Date(today.getFullYear(), today.getMonth(), 22),
  ])
  return (
    <Calendar
      mode="multiple"
      selected={dates}
      onSelect={setDates}
      className="rounded-2xl border border-border"
    />
  )
}

export function CalendarMultipleExample() {
  return (
    <MultipleCalendar />
  )
}
```

```swift
import EsuissUI
import SwiftUI

enum CalendarDates {
  static func day(_ offset: Int) -> Date {
    let calendar = Foundation.Calendar.current
    let start = calendar.startOfDay(for: Date())
    return calendar.date(byAdding: .day, value: offset, to: start) ?? start
  }

  static func dayOfMonth(_ day: Int) -> Date {
    let calendar = Foundation.Calendar.current
    var components = calendar.dateComponents([.year, .month], from: Date())
    components.day = day
    return calendar.date(from: components) ?? Date()
  }
}

struct CalendarBordered: ViewModifier {
  func body(content: Content) -> some View {
    content
      .clipShape(RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous))
      .overlay {
        RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous).strokeBorder(EsuissColor.border)
      }
  }
}

struct CalendarMultipleDemo: View {
  @State private var dates: Set<Date> = [
    CalendarDates.dayOfMonth(8),
    CalendarDates.dayOfMonth(15),
    CalendarDates.dayOfMonth(22),
  ]

  var body: some View {
    DateCalendar(selection: $dates)
      .modifier(CalendarBordered())
  }
}

func day(_ offset: Int) -> Date {
  let calendar = Foundation.Calendar.current
  let start = calendar.startOfDay(for: Date())
  return calendar.date(byAdding: .day, value: offset, to: start) ?? start
}

func dayOfMonth(_ day: Int) -> Date {
  let calendar = Foundation.Calendar.current
  var components = calendar.dateComponents([.year, .month], from: Date())
  components.day = day
  return calendar.date(from: components) ?? Date()
}
```

```kotlin
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.Calendar
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Radius
import java.util.Date
import java.util.GregorianCalendar

fun calendarDay(offsetDays: Int = 0, dayOfMonth: Int? = null): Date {
    val now = GregorianCalendar()
    val result = GregorianCalendar(now.get(GregorianCalendar.YEAR), now.get(GregorianCalendar.MONTH), now.get(GregorianCalendar.DAY_OF_MONTH))
    if (dayOfMonth != null) result.set(GregorianCalendar.DAY_OF_MONTH, dayOfMonth)
    result.add(GregorianCalendar.DAY_OF_MONTH, offsetDays)
    return result.time
}

@Composable
fun CalendarFrame(content: @Composable () -> Unit) {
    Box(
        Modifier
            .border(1.dp, EsuissTheme.colors.border, RoundedCornerShape(Radius.xl2))
            .padding(1.dp),
    ) {
        content()
    }
}

@Composable
fun CalendarMultiple() {
    var dates by rememberSaveable {
        mutableStateOf(listOf(calendarDay(dayOfMonth = 8), calendarDay(dayOfMonth = 15), calendarDay(dayOfMonth = 22)))
    }
    CalendarFrame {
        Calendar(selected = dates, onSelect = { value: List<Date> -> dates = value })
    }
}
```

### Dropdown caption

Month and year dropdowns in the caption.

![Preview](https://ui.suiss.app/previews/components-calendar--dropdown-caption.light.webp)

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

type CalendarProps = React.ComponentProps<typeof Calendar>

function SingleCalendar(
  props: Omit<CalendarProps, "mode" | "selected" | "onSelect">
) {
  const today = new Date()
  const [date, setDate] = React.useState<Date | undefined>(
    new Date(today.getFullYear(), today.getMonth(), today.getDate() + 3)
  )
  return (
    <Calendar
      {...props}
      mode="single"
      selected={date}
      onSelect={setDate}
      className="rounded-2xl border border-border"
    />
  )
}

export function CalendarDropdownCaptionExample() {
  return (
    <SingleCalendar captionLayout="dropdown" />
  )
}
```

```swift
import EsuissUI
import SwiftUI

struct CalendarDropdownCaptionExample: View {
  var body: some View {
    CalendarSingleDemo(captionLayout: .dropdown)
  }
}

enum CalendarDates {
  static func day(_ offset: Int) -> Date {
    let calendar = Foundation.Calendar.current
    let start = calendar.startOfDay(for: Date())
    return calendar.date(byAdding: .day, value: offset, to: start) ?? start
  }

  static func dayOfMonth(_ day: Int) -> Date {
    let calendar = Foundation.Calendar.current
    var components = calendar.dateComponents([.year, .month], from: Date())
    components.day = day
    return calendar.date(from: components) ?? Date()
  }
}

struct CalendarBordered: ViewModifier {
  func body(content: Content) -> some View {
    content
      .clipShape(RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous))
      .overlay {
        RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous).strokeBorder(EsuissColor.border)
      }
  }
}

struct CalendarSingleDemo: View {
  var size: DateCalendar.Size = .default
  var buttonVariant: VariantButtonStyle.Variant = .plain
  var captionLayout: DateCalendar.CaptionLayout = .label
  var showOutsideDays = false
  var showWeekNumber = false
  var weekendsDisabled = false

  @State private var date: Date? = CalendarDates.day(3)

  var body: some View {
    DateCalendar(
      selection: $date,
      size: size,
      buttonVariant: buttonVariant,
      captionLayout: captionLayout,
      showOutsideDays: showOutsideDays,
      showWeekNumber: showWeekNumber,
      isDisabled: { day in
        guard weekendsDisabled else { return false }
        let weekday = Foundation.Calendar.current.component(.weekday, from: day)
        return weekday == 1 || weekday == 7
      }
    )
    .modifier(CalendarBordered())
  }
}

func day(_ offset: Int) -> Date {
  let calendar = Foundation.Calendar.current
  let start = calendar.startOfDay(for: Date())
  return calendar.date(byAdding: .day, value: offset, to: start) ?? start
}

func dayOfMonth(_ day: Int) -> Date {
  let calendar = Foundation.Calendar.current
  var components = calendar.dateComponents([.year, .month], from: Date())
  components.day = day
  return calendar.date(from: components) ?? Date()
}
```

```kotlin
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Calendar
import com.esuiss.ui.components.CalendarCaptionLayout
import com.esuiss.ui.components.CalendarSize
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Radius
import java.util.Date
import java.util.GregorianCalendar
import java.util.Locale

@Composable
fun CalendarDropdownCaptionExample() {
    CalendarSingle(captionLayout = CalendarCaptionLayout.Dropdown)
}

fun calendarDay(offsetDays: Int = 0, dayOfMonth: Int? = null): Date {
    val now = GregorianCalendar()
    val result = GregorianCalendar(now.get(GregorianCalendar.YEAR), now.get(GregorianCalendar.MONTH), now.get(GregorianCalendar.DAY_OF_MONTH))
    if (dayOfMonth != null) result.set(GregorianCalendar.DAY_OF_MONTH, dayOfMonth)
    result.add(GregorianCalendar.DAY_OF_MONTH, offsetDays)
    return result.time
}

fun calendarIsWeekend(date: Date): Boolean {
    val day = GregorianCalendar().apply { time = date }.get(GregorianCalendar.DAY_OF_WEEK)
    return day == GregorianCalendar.SATURDAY || day == GregorianCalendar.SUNDAY
}

@Composable
fun CalendarFrame(content: @Composable () -> Unit) {
    Box(
        Modifier
            .border(1.dp, EsuissTheme.colors.border, RoundedCornerShape(Radius.xl2))
            .padding(1.dp),
    ) {
        content()
    }
}

@Composable
fun CalendarSingle(
    size: CalendarSize = CalendarSize.Default,
    captionLayout: CalendarCaptionLayout = CalendarCaptionLayout.Label,
    showWeekNumber: Boolean = false,
    showOutsideDays: Boolean = false,
    weekendsDisabled: Boolean = false,
    buttonVariant: ButtonVariant = ButtonVariant.Plain,
    locale: Locale = Locale.getDefault(),
    key: String = "",
) {
    var date by rememberSaveable(key) { mutableStateOf<Date?>(calendarDay(offsetDays = 3)) }
    CalendarFrame {
        Calendar(
            selected = date,
            onSelect = { date = it },
            size = size,
            captionLayout = captionLayout,
            showWeekNumber = showWeekNumber,
            showOutsideDays = showOutsideDays,
            disabled = { weekendsDisabled && calendarIsWeekend(it) },
            buttonVariant = buttonVariant,
            locale = locale,
        )
    }
}
```

### Week numbers

Week numbers in a leading column.

![Preview](https://ui.suiss.app/previews/components-calendar--week-numbers.light.webp)

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

type CalendarProps = React.ComponentProps<typeof Calendar>

function SingleCalendar(
  props: Omit<CalendarProps, "mode" | "selected" | "onSelect">
) {
  const today = new Date()
  const [date, setDate] = React.useState<Date | undefined>(
    new Date(today.getFullYear(), today.getMonth(), today.getDate() + 3)
  )
  return (
    <Calendar
      {...props}
      mode="single"
      selected={date}
      onSelect={setDate}
      className="rounded-2xl border border-border"
    />
  )
}

export function CalendarWeekNumbersExample() {
  return (
    <SingleCalendar showWeekNumber />
  )
}
```

```swift
import EsuissUI
import SwiftUI

struct CalendarWeekNumbersExample: View {
  var body: some View {
    CalendarSingleDemo(showWeekNumber: true)
  }
}

enum CalendarDates {
  static func day(_ offset: Int) -> Date {
    let calendar = Foundation.Calendar.current
    let start = calendar.startOfDay(for: Date())
    return calendar.date(byAdding: .day, value: offset, to: start) ?? start
  }

  static func dayOfMonth(_ day: Int) -> Date {
    let calendar = Foundation.Calendar.current
    var components = calendar.dateComponents([.year, .month], from: Date())
    components.day = day
    return calendar.date(from: components) ?? Date()
  }
}

struct CalendarBordered: ViewModifier {
  func body(content: Content) -> some View {
    content
      .clipShape(RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous))
      .overlay {
        RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous).strokeBorder(EsuissColor.border)
      }
  }
}

struct CalendarSingleDemo: View {
  var size: DateCalendar.Size = .default
  var buttonVariant: VariantButtonStyle.Variant = .plain
  var captionLayout: DateCalendar.CaptionLayout = .label
  var showOutsideDays = false
  var showWeekNumber = false
  var weekendsDisabled = false

  @State private var date: Date? = CalendarDates.day(3)

  var body: some View {
    DateCalendar(
      selection: $date,
      size: size,
      buttonVariant: buttonVariant,
      captionLayout: captionLayout,
      showOutsideDays: showOutsideDays,
      showWeekNumber: showWeekNumber,
      isDisabled: { day in
        guard weekendsDisabled else { return false }
        let weekday = Foundation.Calendar.current.component(.weekday, from: day)
        return weekday == 1 || weekday == 7
      }
    )
    .modifier(CalendarBordered())
  }
}

func day(_ offset: Int) -> Date {
  let calendar = Foundation.Calendar.current
  let start = calendar.startOfDay(for: Date())
  return calendar.date(byAdding: .day, value: offset, to: start) ?? start
}

func dayOfMonth(_ day: Int) -> Date {
  let calendar = Foundation.Calendar.current
  var components = calendar.dateComponents([.year, .month], from: Date())
  components.day = day
  return calendar.date(from: components) ?? Date()
}
```

```kotlin
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Calendar
import com.esuiss.ui.components.CalendarCaptionLayout
import com.esuiss.ui.components.CalendarSize
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Radius
import java.util.Date
import java.util.GregorianCalendar
import java.util.Locale

@Composable
fun CalendarWeekNumbersExample() {
    CalendarSingle(showWeekNumber = true)
}

fun calendarDay(offsetDays: Int = 0, dayOfMonth: Int? = null): Date {
    val now = GregorianCalendar()
    val result = GregorianCalendar(now.get(GregorianCalendar.YEAR), now.get(GregorianCalendar.MONTH), now.get(GregorianCalendar.DAY_OF_MONTH))
    if (dayOfMonth != null) result.set(GregorianCalendar.DAY_OF_MONTH, dayOfMonth)
    result.add(GregorianCalendar.DAY_OF_MONTH, offsetDays)
    return result.time
}

fun calendarIsWeekend(date: Date): Boolean {
    val day = GregorianCalendar().apply { time = date }.get(GregorianCalendar.DAY_OF_WEEK)
    return day == GregorianCalendar.SATURDAY || day == GregorianCalendar.SUNDAY
}

@Composable
fun CalendarFrame(content: @Composable () -> Unit) {
    Box(
        Modifier
            .border(1.dp, EsuissTheme.colors.border, RoundedCornerShape(Radius.xl2))
            .padding(1.dp),
    ) {
        content()
    }
}

@Composable
fun CalendarSingle(
    size: CalendarSize = CalendarSize.Default,
    captionLayout: CalendarCaptionLayout = CalendarCaptionLayout.Label,
    showWeekNumber: Boolean = false,
    showOutsideDays: Boolean = false,
    weekendsDisabled: Boolean = false,
    buttonVariant: ButtonVariant = ButtonVariant.Plain,
    locale: Locale = Locale.getDefault(),
    key: String = "",
) {
    var date by rememberSaveable(key) { mutableStateOf<Date?>(calendarDay(offsetDays = 3)) }
    CalendarFrame {
        Calendar(
            selected = date,
            onSelect = { date = it },
            size = size,
            captionLayout = captionLayout,
            showWeekNumber = showWeekNumber,
            showOutsideDays = showOutsideDays,
            disabled = { weekendsDisabled && calendarIsWeekend(it) },
            buttonVariant = buttonVariant,
            locale = locale,
        )
    }
}
```

### Disabled days

Weekends disabled with a `dayOfWeek` matcher.

![Preview](https://ui.suiss.app/previews/components-calendar--disabled-days.light.webp)

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

type CalendarProps = React.ComponentProps<typeof Calendar>

function SingleCalendar(
  props: Omit<CalendarProps, "mode" | "selected" | "onSelect">
) {
  const today = new Date()
  const [date, setDate] = React.useState<Date | undefined>(
    new Date(today.getFullYear(), today.getMonth(), today.getDate() + 3)
  )
  return (
    <Calendar
      {...props}
      mode="single"
      selected={date}
      onSelect={setDate}
      className="rounded-2xl border border-border"
    />
  )
}

export function CalendarDisabledDaysExample() {
  return (
    <SingleCalendar disabled={{ dayOfWeek: [0, 6] }} />
  )
}
```

```swift
import EsuissUI
import SwiftUI

struct CalendarDisabledDaysExample: View {
  var body: some View {
    CalendarSingleDemo(weekendsDisabled: true)
  }
}

enum CalendarDates {
  static func day(_ offset: Int) -> Date {
    let calendar = Foundation.Calendar.current
    let start = calendar.startOfDay(for: Date())
    return calendar.date(byAdding: .day, value: offset, to: start) ?? start
  }

  static func dayOfMonth(_ day: Int) -> Date {
    let calendar = Foundation.Calendar.current
    var components = calendar.dateComponents([.year, .month], from: Date())
    components.day = day
    return calendar.date(from: components) ?? Date()
  }
}

struct CalendarBordered: ViewModifier {
  func body(content: Content) -> some View {
    content
      .clipShape(RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous))
      .overlay {
        RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous).strokeBorder(EsuissColor.border)
      }
  }
}

struct CalendarSingleDemo: View {
  var size: DateCalendar.Size = .default
  var buttonVariant: VariantButtonStyle.Variant = .plain
  var captionLayout: DateCalendar.CaptionLayout = .label
  var showOutsideDays = false
  var showWeekNumber = false
  var weekendsDisabled = false

  @State private var date: Date? = CalendarDates.day(3)

  var body: some View {
    DateCalendar(
      selection: $date,
      size: size,
      buttonVariant: buttonVariant,
      captionLayout: captionLayout,
      showOutsideDays: showOutsideDays,
      showWeekNumber: showWeekNumber,
      isDisabled: { day in
        guard weekendsDisabled else { return false }
        let weekday = Foundation.Calendar.current.component(.weekday, from: day)
        return weekday == 1 || weekday == 7
      }
    )
    .modifier(CalendarBordered())
  }
}

func day(_ offset: Int) -> Date {
  let calendar = Foundation.Calendar.current
  let start = calendar.startOfDay(for: Date())
  return calendar.date(byAdding: .day, value: offset, to: start) ?? start
}

func dayOfMonth(_ day: Int) -> Date {
  let calendar = Foundation.Calendar.current
  var components = calendar.dateComponents([.year, .month], from: Date())
  components.day = day
  return calendar.date(from: components) ?? Date()
}
```

```kotlin
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Calendar
import com.esuiss.ui.components.CalendarCaptionLayout
import com.esuiss.ui.components.CalendarSize
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Radius
import java.util.Date
import java.util.GregorianCalendar
import java.util.Locale

@Composable
fun CalendarDisabledDaysExample() {
    CalendarSingle(weekendsDisabled = true)
}

fun calendarDay(offsetDays: Int = 0, dayOfMonth: Int? = null): Date {
    val now = GregorianCalendar()
    val result = GregorianCalendar(now.get(GregorianCalendar.YEAR), now.get(GregorianCalendar.MONTH), now.get(GregorianCalendar.DAY_OF_MONTH))
    if (dayOfMonth != null) result.set(GregorianCalendar.DAY_OF_MONTH, dayOfMonth)
    result.add(GregorianCalendar.DAY_OF_MONTH, offsetDays)
    return result.time
}

fun calendarIsWeekend(date: Date): Boolean {
    val day = GregorianCalendar().apply { time = date }.get(GregorianCalendar.DAY_OF_WEEK)
    return day == GregorianCalendar.SATURDAY || day == GregorianCalendar.SUNDAY
}

@Composable
fun CalendarFrame(content: @Composable () -> Unit) {
    Box(
        Modifier
            .border(1.dp, EsuissTheme.colors.border, RoundedCornerShape(Radius.xl2))
            .padding(1.dp),
    ) {
        content()
    }
}

@Composable
fun CalendarSingle(
    size: CalendarSize = CalendarSize.Default,
    captionLayout: CalendarCaptionLayout = CalendarCaptionLayout.Label,
    showWeekNumber: Boolean = false,
    showOutsideDays: Boolean = false,
    weekendsDisabled: Boolean = false,
    buttonVariant: ButtonVariant = ButtonVariant.Plain,
    locale: Locale = Locale.getDefault(),
    key: String = "",
) {
    var date by rememberSaveable(key) { mutableStateOf<Date?>(calendarDay(offsetDays = 3)) }
    CalendarFrame {
        Calendar(
            selected = date,
            onSelect = { date = it },
            size = size,
            captionLayout = captionLayout,
            showWeekNumber = showWeekNumber,
            showOutsideDays = showOutsideDays,
            disabled = { weekendsDisabled && calendarIsWeekend(it) },
            buttonVariant = buttonVariant,
            locale = locale,
        )
    }
}
```

### Outside days

Days from adjacent months shown with `showOutsideDays`.

![Preview](https://ui.suiss.app/previews/components-calendar--outside-days.light.webp)

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

type CalendarProps = React.ComponentProps<typeof Calendar>

function SingleCalendar(
  props: Omit<CalendarProps, "mode" | "selected" | "onSelect">
) {
  const today = new Date()
  const [date, setDate] = React.useState<Date | undefined>(
    new Date(today.getFullYear(), today.getMonth(), today.getDate() + 3)
  )
  return (
    <Calendar
      {...props}
      mode="single"
      selected={date}
      onSelect={setDate}
      className="rounded-2xl border border-border"
    />
  )
}

export function CalendarOutsideDaysExample() {
  return (
    <SingleCalendar showOutsideDays />
  )
}
```

```swift
import EsuissUI
import SwiftUI

struct CalendarOutsideDaysExample: View {
  var body: some View {
    CalendarSingleDemo(showOutsideDays: true)
  }
}

enum CalendarDates {
  static func day(_ offset: Int) -> Date {
    let calendar = Foundation.Calendar.current
    let start = calendar.startOfDay(for: Date())
    return calendar.date(byAdding: .day, value: offset, to: start) ?? start
  }

  static func dayOfMonth(_ day: Int) -> Date {
    let calendar = Foundation.Calendar.current
    var components = calendar.dateComponents([.year, .month], from: Date())
    components.day = day
    return calendar.date(from: components) ?? Date()
  }
}

struct CalendarBordered: ViewModifier {
  func body(content: Content) -> some View {
    content
      .clipShape(RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous))
      .overlay {
        RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous).strokeBorder(EsuissColor.border)
      }
  }
}

struct CalendarSingleDemo: View {
  var size: DateCalendar.Size = .default
  var buttonVariant: VariantButtonStyle.Variant = .plain
  var captionLayout: DateCalendar.CaptionLayout = .label
  var showOutsideDays = false
  var showWeekNumber = false
  var weekendsDisabled = false

  @State private var date: Date? = CalendarDates.day(3)

  var body: some View {
    DateCalendar(
      selection: $date,
      size: size,
      buttonVariant: buttonVariant,
      captionLayout: captionLayout,
      showOutsideDays: showOutsideDays,
      showWeekNumber: showWeekNumber,
      isDisabled: { day in
        guard weekendsDisabled else { return false }
        let weekday = Foundation.Calendar.current.component(.weekday, from: day)
        return weekday == 1 || weekday == 7
      }
    )
    .modifier(CalendarBordered())
  }
}

func day(_ offset: Int) -> Date {
  let calendar = Foundation.Calendar.current
  let start = calendar.startOfDay(for: Date())
  return calendar.date(byAdding: .day, value: offset, to: start) ?? start
}

func dayOfMonth(_ day: Int) -> Date {
  let calendar = Foundation.Calendar.current
  var components = calendar.dateComponents([.year, .month], from: Date())
  components.day = day
  return calendar.date(from: components) ?? Date()
}
```

```kotlin
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Calendar
import com.esuiss.ui.components.CalendarCaptionLayout
import com.esuiss.ui.components.CalendarSize
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Radius
import java.util.Date
import java.util.GregorianCalendar
import java.util.Locale

@Composable
fun CalendarOutsideDaysExample() {
    CalendarSingle(showOutsideDays = true)
}

fun calendarDay(offsetDays: Int = 0, dayOfMonth: Int? = null): Date {
    val now = GregorianCalendar()
    val result = GregorianCalendar(now.get(GregorianCalendar.YEAR), now.get(GregorianCalendar.MONTH), now.get(GregorianCalendar.DAY_OF_MONTH))
    if (dayOfMonth != null) result.set(GregorianCalendar.DAY_OF_MONTH, dayOfMonth)
    result.add(GregorianCalendar.DAY_OF_MONTH, offsetDays)
    return result.time
}

fun calendarIsWeekend(date: Date): Boolean {
    val day = GregorianCalendar().apply { time = date }.get(GregorianCalendar.DAY_OF_WEEK)
    return day == GregorianCalendar.SATURDAY || day == GregorianCalendar.SUNDAY
}

@Composable
fun CalendarFrame(content: @Composable () -> Unit) {
    Box(
        Modifier
            .border(1.dp, EsuissTheme.colors.border, RoundedCornerShape(Radius.xl2))
            .padding(1.dp),
    ) {
        content()
    }
}

@Composable
fun CalendarSingle(
    size: CalendarSize = CalendarSize.Default,
    captionLayout: CalendarCaptionLayout = CalendarCaptionLayout.Label,
    showWeekNumber: Boolean = false,
    showOutsideDays: Boolean = false,
    weekendsDisabled: Boolean = false,
    buttonVariant: ButtonVariant = ButtonVariant.Plain,
    locale: Locale = Locale.getDefault(),
    key: String = "",
) {
    var date by rememberSaveable(key) { mutableStateOf<Date?>(calendarDay(offsetDays = 3)) }
    CalendarFrame {
        Calendar(
            selected = date,
            onSelect = { date = it },
            size = size,
            captionLayout = captionLayout,
            showWeekNumber = showWeekNumber,
            showOutsideDays = showOutsideDays,
            disabled = { weekendsDisabled && calendarIsWeekend(it) },
            buttonVariant = buttonVariant,
            locale = locale,
        )
    }
}
```

### Button variants

Navigation buttons in the `ghost`, `secondary` and `tinted` variants.

![Preview](https://ui.suiss.app/previews/components-calendar--button-variants.light.webp)

```tsx
import * as React from "react"
import { ScrollView } from "react-native"
import { Calendar } from "@/components/ui/calendar"

type CalendarProps = React.ComponentProps<typeof Calendar>

function SingleCalendar(
  props: Omit<CalendarProps, "mode" | "selected" | "onSelect">
) {
  const today = new Date()
  const [date, setDate] = React.useState<Date | undefined>(
    new Date(today.getFullYear(), today.getMonth(), today.getDate() + 3)
  )
  return (
    <Calendar
      {...props}
      mode="single"
      selected={date}
      onSelect={setDate}
      className="rounded-2xl border border-border"
    />
  )
}

export function CalendarButtonVariantsExample() {
  return (
    <ScrollView contentContainerClassName="items-start gap-6">
      <SingleCalendar buttonVariant="ghost" />
      <SingleCalendar buttonVariant="secondary" />
      <SingleCalendar buttonVariant="tinted" />
    </ScrollView>
  )
}
```

```swift
import EsuissUI
import SwiftUI

struct CalendarButtonVariantsExample: View {
  var body: some View {
    VStack(alignment: .leading, spacing: 24) {
      CalendarSingleDemo(buttonVariant: .ghost)
      CalendarSingleDemo(buttonVariant: .secondary)
      CalendarSingleDemo(buttonVariant: .tinted)
    }
  }
}

enum CalendarDates {
  static func day(_ offset: Int) -> Date {
    let calendar = Foundation.Calendar.current
    let start = calendar.startOfDay(for: Date())
    return calendar.date(byAdding: .day, value: offset, to: start) ?? start
  }

  static func dayOfMonth(_ day: Int) -> Date {
    let calendar = Foundation.Calendar.current
    var components = calendar.dateComponents([.year, .month], from: Date())
    components.day = day
    return calendar.date(from: components) ?? Date()
  }
}

struct CalendarBordered: ViewModifier {
  func body(content: Content) -> some View {
    content
      .clipShape(RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous))
      .overlay {
        RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous).strokeBorder(EsuissColor.border)
      }
  }
}

struct CalendarSingleDemo: View {
  var size: DateCalendar.Size = .default
  var buttonVariant: VariantButtonStyle.Variant = .plain
  var captionLayout: DateCalendar.CaptionLayout = .label
  var showOutsideDays = false
  var showWeekNumber = false
  var weekendsDisabled = false

  @State private var date: Date? = CalendarDates.day(3)

  var body: some View {
    DateCalendar(
      selection: $date,
      size: size,
      buttonVariant: buttonVariant,
      captionLayout: captionLayout,
      showOutsideDays: showOutsideDays,
      showWeekNumber: showWeekNumber,
      isDisabled: { day in
        guard weekendsDisabled else { return false }
        let weekday = Foundation.Calendar.current.component(.weekday, from: day)
        return weekday == 1 || weekday == 7
      }
    )
    .modifier(CalendarBordered())
  }
}

func day(_ offset: Int) -> Date {
  let calendar = Foundation.Calendar.current
  let start = calendar.startOfDay(for: Date())
  return calendar.date(byAdding: .day, value: offset, to: start) ?? start
}

func dayOfMonth(_ day: Int) -> Date {
  let calendar = Foundation.Calendar.current
  var components = calendar.dateComponents([.year, .month], from: Date())
  components.day = day
  return calendar.date(from: components) ?? Date()
}
```

```kotlin
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Calendar
import com.esuiss.ui.components.CalendarCaptionLayout
import com.esuiss.ui.components.CalendarSize
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Radius
import java.util.Date
import java.util.GregorianCalendar
import java.util.Locale

fun calendarDay(offsetDays: Int = 0, dayOfMonth: Int? = null): Date {
    val now = GregorianCalendar()
    val result = GregorianCalendar(now.get(GregorianCalendar.YEAR), now.get(GregorianCalendar.MONTH), now.get(GregorianCalendar.DAY_OF_MONTH))
    if (dayOfMonth != null) result.set(GregorianCalendar.DAY_OF_MONTH, dayOfMonth)
    result.add(GregorianCalendar.DAY_OF_MONTH, offsetDays)
    return result.time
}

fun calendarIsWeekend(date: Date): Boolean {
    val day = GregorianCalendar().apply { time = date }.get(GregorianCalendar.DAY_OF_WEEK)
    return day == GregorianCalendar.SATURDAY || day == GregorianCalendar.SUNDAY
}

@Composable
fun CalendarFrame(content: @Composable () -> Unit) {
    Box(
        Modifier
            .border(1.dp, EsuissTheme.colors.border, RoundedCornerShape(Radius.xl2))
            .padding(1.dp),
    ) {
        content()
    }
}

@Composable
fun CalendarSingle(
    size: CalendarSize = CalendarSize.Default,
    captionLayout: CalendarCaptionLayout = CalendarCaptionLayout.Label,
    showWeekNumber: Boolean = false,
    showOutsideDays: Boolean = false,
    weekendsDisabled: Boolean = false,
    buttonVariant: ButtonVariant = ButtonVariant.Plain,
    locale: Locale = Locale.getDefault(),
    key: String = "",
) {
    var date by rememberSaveable(key) { mutableStateOf<Date?>(calendarDay(offsetDays = 3)) }
    CalendarFrame {
        Calendar(
            selected = date,
            onSelect = { date = it },
            size = size,
            captionLayout = captionLayout,
            showWeekNumber = showWeekNumber,
            showOutsideDays = showOutsideDays,
            disabled = { weekendsDisabled && calendarIsWeekend(it) },
            buttonVariant = buttonVariant,
            locale = locale,
        )
    }
}

@Composable
fun CalendarButtonVariants() {
    Column(verticalArrangement = Arrangement.spacedBy(24.dp)) {
        CalendarSingle(buttonVariant = ButtonVariant.Ghost, key = "ghost")
        CalendarSingle(buttonVariant = ButtonVariant.Secondary, key = "secondary")
        CalendarSingle(buttonVariant = ButtonVariant.Tinted, key = "tinted")
    }
}
```

### Locale

Turkish month and weekday names with `locale="tr-TR"`.

![Preview](https://ui.suiss.app/previews/components-calendar--locale.light.webp)

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

type CalendarProps = React.ComponentProps<typeof Calendar>

function SingleCalendar(
  props: Omit<CalendarProps, "mode" | "selected" | "onSelect">
) {
  const today = new Date()
  const [date, setDate] = React.useState<Date | undefined>(
    new Date(today.getFullYear(), today.getMonth(), today.getDate() + 3)
  )
  return (
    <Calendar
      {...props}
      mode="single"
      selected={date}
      onSelect={setDate}
      className="rounded-2xl border border-border"
    />
  )
}

export function CalendarLocaleExample() {
  return (
    <SingleCalendar locale="tr-TR" />
  )
}
```

```swift
import EsuissUI
import SwiftUI

struct CalendarLocaleExample: View {
  var body: some View {
    CalendarSingleDemo()
      .environment(\.locale, Locale(identifier: "tr_TR"))
  }
}

enum CalendarDates {
  static func day(_ offset: Int) -> Date {
    let calendar = Foundation.Calendar.current
    let start = calendar.startOfDay(for: Date())
    return calendar.date(byAdding: .day, value: offset, to: start) ?? start
  }

  static func dayOfMonth(_ day: Int) -> Date {
    let calendar = Foundation.Calendar.current
    var components = calendar.dateComponents([.year, .month], from: Date())
    components.day = day
    return calendar.date(from: components) ?? Date()
  }
}

struct CalendarBordered: ViewModifier {
  func body(content: Content) -> some View {
    content
      .clipShape(RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous))
      .overlay {
        RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous).strokeBorder(EsuissColor.border)
      }
  }
}

struct CalendarSingleDemo: View {
  var size: DateCalendar.Size = .default
  var buttonVariant: VariantButtonStyle.Variant = .plain
  var captionLayout: DateCalendar.CaptionLayout = .label
  var showOutsideDays = false
  var showWeekNumber = false
  var weekendsDisabled = false

  @State private var date: Date? = CalendarDates.day(3)

  var body: some View {
    DateCalendar(
      selection: $date,
      size: size,
      buttonVariant: buttonVariant,
      captionLayout: captionLayout,
      showOutsideDays: showOutsideDays,
      showWeekNumber: showWeekNumber,
      isDisabled: { day in
        guard weekendsDisabled else { return false }
        let weekday = Foundation.Calendar.current.component(.weekday, from: day)
        return weekday == 1 || weekday == 7
      }
    )
    .modifier(CalendarBordered())
  }
}

func day(_ offset: Int) -> Date {
  let calendar = Foundation.Calendar.current
  let start = calendar.startOfDay(for: Date())
  return calendar.date(byAdding: .day, value: offset, to: start) ?? start
}

func dayOfMonth(_ day: Int) -> Date {
  let calendar = Foundation.Calendar.current
  var components = calendar.dateComponents([.year, .month], from: Date())
  components.day = day
  return calendar.date(from: components) ?? Date()
}
```

```kotlin
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Calendar
import com.esuiss.ui.components.CalendarCaptionLayout
import com.esuiss.ui.components.CalendarSize
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Radius
import java.util.Date
import java.util.GregorianCalendar
import java.util.Locale

@Composable
fun CalendarLocaleExample() {
    CalendarSingle(locale = Locale.forLanguageTag("tr-TR"))
}

fun calendarDay(offsetDays: Int = 0, dayOfMonth: Int? = null): Date {
    val now = GregorianCalendar()
    val result = GregorianCalendar(now.get(GregorianCalendar.YEAR), now.get(GregorianCalendar.MONTH), now.get(GregorianCalendar.DAY_OF_MONTH))
    if (dayOfMonth != null) result.set(GregorianCalendar.DAY_OF_MONTH, dayOfMonth)
    result.add(GregorianCalendar.DAY_OF_MONTH, offsetDays)
    return result.time
}

fun calendarIsWeekend(date: Date): Boolean {
    val day = GregorianCalendar().apply { time = date }.get(GregorianCalendar.DAY_OF_WEEK)
    return day == GregorianCalendar.SATURDAY || day == GregorianCalendar.SUNDAY
}

@Composable
fun CalendarFrame(content: @Composable () -> Unit) {
    Box(
        Modifier
            .border(1.dp, EsuissTheme.colors.border, RoundedCornerShape(Radius.xl2))
            .padding(1.dp),
    ) {
        content()
    }
}

@Composable
fun CalendarSingle(
    size: CalendarSize = CalendarSize.Default,
    captionLayout: CalendarCaptionLayout = CalendarCaptionLayout.Label,
    showWeekNumber: Boolean = false,
    showOutsideDays: Boolean = false,
    weekendsDisabled: Boolean = false,
    buttonVariant: ButtonVariant = ButtonVariant.Plain,
    locale: Locale = Locale.getDefault(),
    key: String = "",
) {
    var date by rememberSaveable(key) { mutableStateOf<Date?>(calendarDay(offsetDays = 3)) }
    CalendarFrame {
        Calendar(
            selected = date,
            onSelect = { date = it },
            size = size,
            captionLayout = captionLayout,
            showWeekNumber = showWeekNumber,
            showOutsideDays = showOutsideDays,
            disabled = { weekendsDisabled && calendarIsWeekend(it) },
            buttonVariant = buttonVariant,
            locale = locale,
        )
    }
}
```

## API Reference

### CalendarDayButton

Pressable day cell styled for each selection state.

Also accepts the props of `React.ComponentProps<typeof CalendarPrimitive.DayButton>`.

### Calendar

Month grids with caption and navigation.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `buttonVariant` | `ButtonProps["variant"]` | `"plain"` | Button variant for the navigation buttons. Defaults to `plain`. |
| `size` | `CalendarSize` | `"default"` | `sm`, `default` or `lg`. |
| `captionLayout` | `CaptionLayout` | `"label"` | `label`, `dropdown`, `dropdown-months` or `dropdown-years`. |
| `showWeekNumber` | `boolean` | `false` | Shows week numbers. |

Also accepts the props of `React.ComponentProps<typeof CalendarPrimitive.Root>`.

## Accessibility

- Each month is a `grid` labelled with its caption.
- Day cells have the `button` role, a full date label, and `selected` and `disabled` state.
- On React Native and Android, the previous and next buttons are labelled with the relative month in the calendar's locale, such as "Last month" and "Next month" in English. On iOS they are labelled "Previous month" and "Next month".
- Caption dropdowns are labelled "Choose the Month" and "Choose the Year".

## Notes

- Navigation arrows swap in right-to-left layouts.
- Uses `bg-primary`, `bg-primary/10 dark:bg-primary/20`, `bg-muted` and `text-primary` tokens; light and dark follow the system appearance.

