# `Drafter.Widget.Calendar`
[🔗](https://github.com/jaman/drafter/blob/main/lib/drafter/widget/calendar.ex#L1)

A monthly calendar grid widget with date selection and keyboard navigation.

Displays a single month at a time with a title row showing the month name and year,
a weekday header row (Su Mo Tu We Th Fr Sa), and up to six week rows. Today's date
is highlighted automatically, and the selected date receives a distinct visual treatment.

## Component tag

Tag `:calendar`, built by `Drafter.App` as `{:calendar, opts}`:

    calendar(opts)

There is no positional argument; every prop comes from `opts`.
`from_component_opts/2` wraps `:on_select` with `Drafter.Widget.Callback`, so
it may be given as an atom event name.

## Options

  * `:selected_date` - `t:Date.t/0` to highlight. Default `nil`. The cursor starts
    on this date, or on `Date.utc_today/0` when it is `nil`
  * `:on_select` - atom event name or one-arity function receiving the confirmed
    `t:Date.t/0`. Default `nil`. Its return value is discarded, so it cannot emit
    an action; use it for its side effect
  * `:min_date` - earliest navigable `t:Date.t/0`. Default `nil`, no constraint
  * `:max_date` - latest navigable `t:Date.t/0`. Default `nil`, no constraint
  * `:style` - `t:map/0` of inline style overrides. Default `%{}`
  * `:class` - theme class atom or list of them, reaching `mount/1` as
    `:classes`. Default `[]`

`update/2` re-reads `:selected_date`, `:min_date`, `:max_date`, `:on_select`,
`:style` and `:classes`. `:cursor_date` and `:view_date` are owned by the widget
and survive a re-render, so changing `:selected_date` after mount moves the
highlight but not the cursor or the displayed month.

## Widget value

`Drafter.get_widget_value/1` is not implemented for this widget; the chosen date
is reported through `:on_select`.

## Key bindings

  * `←` / `→` — move cursor one day backward/forward
  * `↑` / `↓` — move cursor one week backward/forward
  * `Enter` / `Space` — select the highlighted date and fire `:on_select`

A move that would leave the `:min_date`..`:max_date` range is ignored. Crossing a
month boundary scrolls the view to the new month. Every other key bubbles.

## Usage

    calendar(selected_date: ~D[2026-08-02], on_select: :date_chosen)

# `t`

```elixir
@type t() :: %Drafter.Widget.Calendar{
  classes: [atom()],
  cursor_date: Date.t(),
  max_date: Date.t() | nil,
  min_date: Date.t() | nil,
  on_select: (Date.t() -&gt; term()) | nil,
  selected_date: Date.t() | nil,
  style: map(),
  view_date: Date.t()
}
```

# `component_tag`

```elixir
@spec component_tag() :: :calendar
```

The registry tag for this widget.

    iex> Drafter.Widget.Calendar.component_tag()
    :calendar

# `focused`

# `from_component_opts`

```elixir
@spec from_component_opts(
  term(),
  keyword()
) :: Drafter.Widget.props()
```

Turns the `{:calendar, opts}` element into a props map for `mount/1`.

The positional argument is ignored; every prop comes from `opts`. `:class` is
normalised into `:classes` and `:on_select` is wrapped by
`Drafter.Widget.Callback.wrap_1/1`.

    iex> props = Drafter.Widget.Calendar.from_component_opts(nil, selected_date: ~D[2026-08-02])
    iex> {props.selected_date, props.on_select, props.min_date, props.style, props.classes}
    {~D[2026-08-02], nil, nil, %{}, []}

# `handle_event`

# `handle_key`

```elixir
@spec handle_key(Drafter.Widget.key(), t()) :: {:ok, t()} | {:bubble, t()}
```

Moves the cursor or confirms the date under it.

Arrow keys return `{:ok, state}`, with the state unchanged when the target date
falls outside `:min_date`..`:max_date`. `:enter` and `:" "` set `:selected_date`
to the cursor date, call `:on_select`, and return `{:ok, state}`. Every other key
returns `{:bubble, state}`.

    iex> cal = Drafter.Widget.Calendar.mount(%{selected_date: ~D[2026-08-02]})
    iex> {:ok, moved} = Drafter.Widget.Calendar.handle_key(:down, cal)
    iex> moved.cursor_date
    ~D[2026-08-09]

    iex> cal = Drafter.Widget.Calendar.mount(%{selected_date: ~D[2026-08-02]})
    iex> {:ok, moved} = Drafter.Widget.Calendar.handle_key(:left, cal)
    iex> {moved.cursor_date, moved.view_date}
    {~D[2026-08-01], ~D[2026-08-01]}

    iex> cal = Drafter.Widget.Calendar.mount(%{selected_date: ~D[2026-08-02], min_date: ~D[2026-08-02]})
    iex> {:ok, blocked} = Drafter.Widget.Calendar.handle_key(:left, cal)
    iex> blocked.cursor_date
    ~D[2026-08-02]

    iex> cal = Drafter.Widget.Calendar.mount(%{selected_date: ~D[2026-08-31]})
    iex> {:ok, next} = Drafter.Widget.Calendar.handle_key(:right, cal)
    iex> {next.cursor_date, next.view_date}
    {~D[2026-09-01], ~D[2026-09-01]}

    iex> cal = Drafter.Widget.Calendar.mount(%{selected_date: ~D[2026-08-02]})
    iex> {:ok, chosen} = Drafter.Widget.Calendar.handle_key(:enter, cal)
    iex> chosen.selected_date
    ~D[2026-08-02]

    iex> cal = Drafter.Widget.Calendar.mount(%{selected_date: ~D[2026-08-02]})
    iex> Drafter.Widget.Calendar.handle_key(:escape, cal) |> elem(0)
    :bubble

# `handle_press`

```elixir
@spec handle_press(integer(), integer(), t()) :: {:ok, t()}
```

Selects the date under the pressed cell.

Rows `2` through `7` are the week rows and every four columns are one weekday
column, so the cell is `{div(x, 4), y - 2}`. A press on the title or header row,
or beyond the seventh column, returns `{:ok, state}` unchanged. A hit moves the
cursor to that date, sets `:selected_date`, and calls `:on_select`; the
`:min_date`/`:max_date` range is not applied on this path.

# `mount`

```elixir
@spec mount(Drafter.Widget.props()) :: t()
```

Builds the calendar state from `props`.

The cursor starts on `:selected_date`, or on `Date.utc_today/0` when that is
`nil`, and the view opens on the first day of the cursor's month.

    iex> cal = Drafter.Widget.Calendar.mount(%{selected_date: ~D[2026-08-02]})
    iex> {cal.cursor_date, cal.view_date, cal.selected_date}
    {~D[2026-08-02], ~D[2026-08-01], ~D[2026-08-02]}

    iex> cal = Drafter.Widget.Calendar.mount(%{})
    iex> {cal.selected_date, cal.min_date, cal.max_date, cal.classes, cal.style}
    {nil, nil, nil, [], %{}}

# `preferred_height`

```elixir
@spec preferred_height(
  term(),
  keyword()
) :: pos_integer()
```

Always `9`: a title row, a weekday header row and six week rows.

    iex> Drafter.Widget.Calendar.preferred_height(nil, [])
    9

# `render`

```elixir
@spec render(t(), Drafter.Widget.rect()) :: [Drafter.Draw.Strip.t()]
```

Draws the month into `rect`.

Always returns nine strips: a title row, a weekday header row, up to six week
rows, and blank rows making up the difference. `rect.height` is not consulted, and
`rect.width` is widened to at least 28 columns, four per weekday column.

# `unmount`

# `update`

```elixir
@spec update(Drafter.Widget.props(), t()) :: t()
```

Folds fresh props into `state`.

Re-reads `:selected_date`, `:min_date`, `:max_date`, `:on_select`, `:style` and
`:classes`. `:cursor_date` and `:view_date` are left untouched, so the displayed
month does not follow a new `:selected_date`.

    iex> cal = Drafter.Widget.Calendar.mount(%{selected_date: ~D[2026-08-02]})
    iex> updated = Drafter.Widget.Calendar.update(%{selected_date: ~D[2026-12-25]}, cal)
    iex> {updated.selected_date, updated.view_date}
    {~D[2026-12-25], ~D[2026-08-01]}

# `update_props_from_mount`

```elixir
@spec update_props_from_mount(Drafter.Widget.props(), t(), keyword()) ::
  Drafter.Widget.props()
```

Returns `mount_props` unchanged, so a re-render passes every option through to
`update/2`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
