# CalendarModel

> Headless state model for calendar grids: tracks the keyboard-focused day, the visible month page, and an in-progress range selection (anchor plus hover preview), leaving value storage and rendering to the consumer.

```ts
import { CalendarModel } from "@sdxc/ui/behaviors";
```

Owns the ephemeral state around picking a date: the keyboard-focused day,
the visible month page, and a range's anchor plus hover preview. The chosen
value stays with the consumer, and every state change dispatches `"change"`.

## Signature

```ts
new CalendarModel(options?: CalendarModel.Options)
```

## Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| `options?` | `CalendarModel.Options` | Initial focus, selectable bounds, and disabled-day rule. |

## Properties

| Property | Type | Description |
| --- | --- | --- |
| `focusedDate` | `Date` | The day that currently carries keyboard/roving-tabindex focus. Always a fresh `Date` instance, so callers may mutate it freely. |
| `visibleMonth` | `Date` | The first day of the month page on screen. Follows focus into a new month, and moves on its own when the consumer pages the grid via showMonth/showNextMonth/showPreviousMonth. |
| `anchorDate` | `Date \| null` | The first day picked while building a range, or `null` when no range selection is in progress. |
| `previewDate` | `Date \| null` | The day currently under hover/focus while a range's second endpoint is being chosen, or `null` when there is no pending range or no day has been previewed yet. |
| `previewRange` | `CalendarModel.Range \| null` | The range implied by the current anchor and preview day, normalized so `start` comes first; `null` when no range selection is in progress. Before a preview day is set it collapses to a single day on the anchor. |
| `min` | `Date \| null` | Earliest day the model accepts, or `null` when unbounded. |
| `max` | `Date \| null` | Latest day the model accepts, or `null` when unbounded. |

## Methods

| Method | Description |
| --- | --- |
| `focusDate(date: Date): void` | Moves focus to an arbitrary day, clamped to the selectable bounds, and pages the visible month to match. Suits pointer selection of a grid cell; the directional `focus*` methods own the keyboard offset math. |
| `focusNextDay(): void` | Moves focus one day forward (typically the ArrowRight key). |
| `focusPreviousDay(): void` | Moves focus one day back (typically the ArrowLeft key). |
| `focusNextWeek(): void` | Moves focus one week forward (typically the ArrowDown key). |
| `focusPreviousWeek(): void` | Moves focus one week back (typically the ArrowUp key). |
| `focusNextMonth(): void` | Moves focus one month forward, clamping the day of month to the target month's last day when it's shorter (typically the PageDown key). |
| `focusPreviousMonth(): void` | Moves focus one month back, clamping the day of month to the target month's last day when it's shorter (typically the PageUp key). |
| `focusMonthStart(): void` | Moves focus to the first day of the focused month (typically the Home key). |
| `focusMonthEnd(): void` | Moves focus to the last day of the focused month (typically the End key). |
| `showMonth(date: Date): void` | Pages the visible month to the month containing `date`, leaving focus where it is — the focused day scrolls out of the rendered grid until focus moves again. Suits a header's month/year picker. |
| `showNextMonth(): void` | Pages the visible month forward by one, leaving focus where it is. |
| `showPreviousMonth(): void` | Pages the visible month back by one, leaving focus where it is. |
| `beginRange(date: Date): void` | Starts a range selection at `date`, clearing any preview from a previous attempt. Call this when the user picks the first endpoint (e.g. the first click in a range calendar). |
| `updateRangePreview(date: Date): void` | Updates the pending second endpoint so previewRange tracks the day under hover or focus. A no-op while no range is in progress, so hover handlers can call it unconditionally. |
| `completeRange(date: Date): CalendarModel.Range \| null` | Commits the range from the current anchor to `date` and clears the pending selection, returning it normalized. With no range in progress the consumer's selected value stays untouched. |
| `cancelRange(): void` | Abandons an in-progress range selection (e.g. on Escape or blur), leaving the consumer's selected value untouched. A no-op when no range is in progress. |
| `isFocused(date: Date): boolean` | Whether `date` is the day currently carrying keyboard focus. |
| `isRangeAnchor(date: Date): boolean` | Whether `date` is the anchor of an in-progress range selection. |
| `isInPreviewRange(date: Date): boolean` | Whether `date` falls within the current previewRange, inclusive of both endpoints. Always `false` when no range is in progress. |
| `isInVisibleMonth(date: Date): boolean` | Whether `date` falls in the same month as visibleMonth. Useful for dimming the leading/trailing days a month grid renders from adjacent months. |
| `isDisabled(date: Date): boolean` | Whether `date` falls outside the selectable bounds — before min, after max, or rejected by the `isDateDisabled` predicate. |

## Examples

```tsx
let model = new CalendarModel({ focusedDate: new Date(2026, 0, 15) });
model.addEventListener("change", () => update());
model.focusNextDay();
```

## Used with it

The events, constants and types its module publishes, each imported from `@sdxc/ui/behaviors` too.

### CalendarModel.Range

An inclusive start/end pair of calendar days, always normalized so
`start` comes first, whatever order the two endpoints were picked in.

#### Members

| Member | Type | Description |
| --- | --- | --- |
| `start` | `Date` |  |
| `end` | `Date` |  |

### CalendarModel.Options

Constructor options for CalendarModel.

#### Members

| Member | Type | Description |
| --- | --- | --- |
| `focusedDate?` | `Date` | Day that starts focused. Defaults to today when omitted. Only the calendar date is read; time-of-day is discarded. |
| `min?` | `Date` | Earliest day the model will focus or accept as a range endpoint; navigation and range methods clamp to it. |
| `max?` | `Date` | Latest day the model will focus or accept as a range endpoint; navigation and range methods clamp to it. |
| `isDateDisabled?` | `(date: Date) => boolean` | Predicate marking individual days as unselectable (closures on a weekend, already-booked slots, …). Navigation still visits these days, so keyboard users can reach a selectable day past them. |

### CalendarModel.EventMap

Events dispatched by CalendarModel as its state changes.

#### Members

| Member | Type | Description |
| --- | --- | --- |
| `change` | `Event` | Dispatched after focus, the visible month, the range anchor, or the range preview changes. |
