diff --git a/docs/rfcs/005-calendar-preview.md b/docs/rfcs/005-calendar-preview.md
new file mode 100644
index 000000000..049397a7b
--- /dev/null
+++ b/docs/rfcs/005-calendar-preview.md
@@ -0,0 +1,518 @@
+---
+ID: RFC 005
+Created: August 24, 2026
+Status: Draft
+RFC PR: https://github.com/raystack/apsara/pull/890
+---
+
+# Calendar Rewrite: `CalendarPreview`
+
+Replace `Calendar`, `DatePicker`, and `RangePicker` with one subcomposed root, `CalendarPreview`, that owns date state and popover state explicitly and exposes every surface as a dot-notation part.
+
+Every recurring bug in this family traces to one fact: **popover open state is private**, so the pickers cannot hand dismissal to Base UI. That costs 185 lines of bespoke popover machinery, three suppression branches inside one callback, and a month/year navigation feature that is off by default but still demoed.
+
+Breaking change, no compatibility shim. `CalendarPreview` ships alongside the current family; the old exports are removed one release later. Scope: single and range selection, month-year navigation, granularity (day / month / quarter / half-year / year), presets, and time-of-day.
+
+**Target package:** `@raystack/apsara` (`packages/raystack/components/calendar-preview/`)
+
+## Table of Contents
+
+- [Calendar Rewrite: `CalendarPreview`](#calendar-rewrite-calendarpreview)
+ - [Table of Contents](#table-of-contents)
+ - [Background](#background)
+ - [Current Architecture](#current-architecture)
+ - [Current Problems](#current-problems)
+ - [Goals and Non-Goals](#goals-and-non-goals)
+ - [Proposal](#proposal)
+ - [API at a Glance](#api-at-a-glance)
+ - [Recipes](#recipes)
+ - [Root Props](#root-props)
+ - [Parts](#parts)
+ - [State Ownership](#state-ownership)
+ - [Field Integration](#field-integration)
+ - [Conventions This Follows](#conventions-this-follows)
+ - [Internal Architecture](#internal-architecture)
+ - [File Layout](#file-layout)
+ - [The Date Adapter](#the-date-adapter)
+ - [The react-day-picker Boundary](#the-react-day-picker-boundary)
+ - [Dependencies](#dependencies)
+ - [The `data-slot` Contract](#the-data-slot-contract)
+ - [Breaking Changes](#breaking-changes)
+ - [Migration Map](#migration-map)
+ - [Repo-Internal Follow-ups](#repo-internal-follow-ups)
+ - [Implementation Plan](#implementation-plan)
+ - [Testing](#testing)
+ - [Open Items](#open-items)
+ - [Alternatives](#alternatives)
+ - [Helpful Links](#helpful-links)
+
+## Background
+
+### Current Architecture
+
+`packages/raystack/components/calendar/` ships three flat exports across 1,071 lines of TypeScript and 345 of CSS. The barrel exports `Calendar`, `DatePicker`, `RangePicker`, and re-exports react-day-picker's `DateRange` type directly.
+
+| File | Lines | Role |
+|---|---|---|
+| `calendar.tsx` | 269 | Wraps react-day-picker's `DayPicker`; 5 component overrides, 20 `classNames` keys |
+| `date-picker.tsx` | 293 | Single-date picker with a typable input |
+| `range-picker.tsx` | 324 | Two-input range picker |
+| `use-picker-popover.ts` | 185 | Bespoke open/close, outside-click, and dropdown carve-out |
+| `calendar.module.css` | 345 | Shared styles |
+
+### Current Problems
+
+| # | Problem | Evidence in the repo | Rewrite's answer |
+|---|---|---|---|
+| 1 | No dot-notation composition | Three flat exports, zero sub-parts, against `composition.md`'s "one name per component". `slotProps`, `children`-as-function, and `onErrorChange` appear in no other component. | One root, every surface a part |
+| 2 | **Open state is private** | Neither picker takes `open` / `onOpenChange`; both pass `popover.isOpen` into `Popover` and expose nothing. `Menu`, `Select`, `Sidebar`, and `Tour` roots all expose it. | `open` / `defaultOpen` / `onOpenChange` on the root |
+| 3 | 185 lines of popover machinery | `use-picker-popover.ts` — inventory below | File deleted; Base UI owns dismissal |
+| 4 | `Date` identity churn | Three `biome-ignore`s for `useExhaustiveDependencies` — two reading *compare on timestamp, not Date identity*, one *engage/disengage are stable*. A fourth effect with the same problem is unguarded (below). | `dayKey()` / `epoch()` internally; zero suppressions |
+| 5 | Month/year nav off by default | `range-picker.tsx`: `'dropdown'` mounts Apsara `Select`s whose unmount loops ("Maximum update depth"). `date-picker.runtime.test.tsx` still asks for real-browser verification and credits a `useMemo` that no longer exists — `date-picker.tsx` has zero. The "With Dropdowns" demo shows it on standalone `` with nothing marking it unsafe inside a picker. | `.Nav` is ours; RDP never mounts a `Select` |
+| 6 | Spread-last unsatisfiable | `SKILL.md` requires `...props` last; `Calendar` complies, the pickers cannot — eight keys are pinned after the consumer spread. Detail below. | RDP's union never reaches a part |
+| 7 | `dayjs.extend()` is import-order dependent | Four modules extend independently. `range-picker.tsx` extends nothing and rides on `calendar.tsx` importing first; `date-picker.tsx` forwards `timeZone` without extending `utc`/`timezone`; ordering is enforced by a comment. This is the failure class behind the 0.49.0 P0 — a `TypeError` on every keystroke — and two tests guard it. | One `date-adapter.ts` owns every `extend()` |
+| 8 | Partial range disable impossible | Disabling either input gates the whole picker, because the state machine rewrites both `from` and `to` regardless of which input was clicked. Two tests assert the gate; the source comment and CHANGELOG 0.49.0 both say to constrain the calendar instead. | `lock='from' \| 'to'` on `RangeProps` |
+| 9 | `FilterChip` absorbs the cost | Sole production consumer, hit three ways. Detail below. | Compose parts; delete `toDateValue()`, the merge, and the dead CSS |
+| 10 | Published types are wrong | `props.ts` types both `slotProps.calendar` as the full docs `CalendarProps`, including `mode`, `selected`, `onSelect`, `footer` — none of which the real slot type accepts. `RangePickerProps` and `RangePickerSlotProps` are unexported and absent from the barrel, so **consumers cannot type a `RangePicker` wrapper**. `pickerGroupClassName` is undocumented. Of six deprecated props, exactly one carries an `@deprecated` marker. | Regenerated in phase 5 against the real types |
+| 11 | A documented integration was never built | CHANGELOG 0.49.0 claims `DataTable` / `DataView` columns gained `filterProps.calendar`. `data-view.types.tsx` has only `{ select?: BaseSelectProps }` — the slot exists solely on `DataTable`, which is `@deprecated`. | Added to `DataView` in phase 4 |
+| 12 | Fails 2 of 8 `SKILL.md` checklist items | CSS carries three `Todo: var does not exist` markers, a hardcoded `max-height: 260px`, and eight `var(--rs-space-10, 40px)` fallbacks. No interactive `playground` in `demo.ts` — 54 other components have one. | Phase 5 exit criteria |
+
+Three further house rules fail off-checklist: dot-notation composition, spread-last in the pickers, and docs matching code. The `data-slot` contract is the one genuinely clean part of this family, and the rewrite preserves it.
+
+**What the 185 lines contain.** Six refs — two DOM handles, two internal flags, and two mirrors of state and props kept purely so callback identities stay stable. Three suppression branches inside a single `onOpenChange`: one swallows the dropdown's own open-change, one swallows `trigger-press` closes because Base UI's `useClick` toggles against the input's `onFocus`, one swallows redundant re-opens. Then a handler named `handleMouseDown` registered on `'mouseup'`, an uncleaned `setTimeout`, and a `setIsOpen` returned from the hook that neither picker ever calls.
+
+**Why spread-last is unsatisfiable.** `required` discriminates react-day-picker's prop union, so a widened value breaks the narrowing — which forces `required={true}` to sit after the consumer spread, and seven more keys with it: `timeZone`, `onDropdownOpen`, `mode`, `month`, `selected`, `onSelect`, `onMonthChange`. Three of those (`month`, `onMonthChange`, `timeZone`) are reachable through `slotProps.calendar`, because they live in RDP's `PropsBase`, so a consumer can pass them and watch them vanish. The slot type — `Omit & CalendarPropsExtended` — already excludes the rest.
+
+**How `FilterChip` absorbs it.** Three ways. The shallow `slotProps.input` merge lets a consumer-supplied `classNames` object **replace** the chip's own, dropping its container class and breaking layout. `showCalendarIcon={false}` sits before the consumer spread exactly as `SKILL.md` prescribes, so a consumer can re-enable the icon and break the chip. And two `[class*="…"]` rules reach at what they assume are `Input`'s hashed class names for `helper-text` and `input-error-wrapper` — strings that appear nowhere else in the repo, and `Input` renders no such element, so both rules are dead code suppressing nothing.
+
+**The causal chain.** `use-picker-popover.ts`'s header comment names its own reasons: `captionLayout='dropdown'` renders `Select`s whose portals look "outside" to a naive dismiss handler, and `isOpen` is read through a ref because Base UI's store subscriber re-binds on `onOpenChange` identity change and looped on mount. Both dissolve once `.Nav` renders the `Select`s outside the grid and `useControlled` supplies a stable setter. None of the 185 lines is wrong for what it is asked to do — it exists only because the component owns dismissal instead of Base UI.
+
+**The unguarded effect** is `setViewMonth` in `date-picker.tsx`:
+
+```tsx
+useEffect(() => {
+ if (popover.isOpen) {
+ setViewMonth(calendarProps?.defaultMonth ?? selectedDate ?? new Date());
+ }
+}, [popover.isOpen, selectedDate, calendarProps?.defaultMonth]);
+```
+
+`calendarProps` is a fresh object literal every render — `{ ...legacyCalendarProps, ...slotProps?.calendar }` — so an inline `slotProps={{ calendar: { defaultMonth: new Date(2025, 0) } }}` yields a fresh `Date` identity per render → effect refires → `setViewMonth` → re-render → loop.
+
+## Goals and Non-Goals
+
+**Goals.** One export with dot-notation parts, matching `composition.md`. Explicit ownership of every piece of state — selection, view month, open, granularity, validity. react-day-picker fully isolated, so its union never reaches a consumer and spread-last becomes satisfiable at every part. Month/year navigation on by default. Full feature set: day / month / quarter / half-year / year granularity, single and dual month, presets, time-of-day. Zero `slotProps`. All eight `SKILL.md` checklist items pass.
+
+**Non-goals.** Preserving the old API — this is a rewrite. Locale/i18n expansion beyond what RDP already gives us (tracked as follow-up). Replacing the date library with Temporal (see [The Date Adapter](#the-date-adapter) for the seam that makes it possible later).
+
+## Proposal
+
+### API at a Glance
+
+```tsx
+import { CalendarPreview } from '@raystack/apsara';
+
+// Zero-config recipe for the common case
+
+
+// Same component, fully composed, when you need control
+
+
+
+
+
+
+ Last 7 days
+
+
+
+
+
+
+
+
+
+```
+
+Full part tree:
+
+```
+ state owner
+├── anchor (render-friendly)
+│ ├── single text field
+│ └── paired start/end fields
+└── portaled popover surface
+ ├──
+ │ └──
+ ├── Day | Month | Quarter | Half-year | Year
+ ├── caption + chevrons + month/year selects
+ ├── the day grid (replaces `Calendar`)
+ ├── month / quarter / half-year / year grid
+ ├── time-of-day
+ └──
+ ├──
+ └──
+```
+
+### Recipes
+
+Pre-composed compositions hung off the same object, implemented as compositions of the parts below — no private code paths.
+
+```tsx
+
+
+
+
+ // no popover
+```
+
+**Recipes take no `slotProps` and no escape hatches.** The moment you need to change what is inside the popover, you drop to parts — there is no third state where you configure structure through props. Precedent for hanging non-part values off a root: `Dialog` carries `createHandle`; `Combobox` carries `useFilter` and `useFilteredItems`.
+
+### Root Props
+
+```tsx
+type CalendarSelection = 'single' | 'range' | 'multiple';
+type CalendarGranularity = 'day' | 'month' | 'quarter' | 'half-year' | 'year';
+
+interface CalendarPreviewBaseProps {
+ /** @defaultValue 'day' */
+ granularity?: CalendarGranularity;
+ /** Switchable granularities. Renders `GranularityTabs` when >1. */
+ granularities?: CalendarGranularity[];
+
+ // popover state (was entirely private)
+ open?: boolean;
+ defaultOpen?: boolean;
+ onOpenChange?: (open: boolean, details?: { reason?: string }) => void;
+
+ // view state, independent of selection
+ month?: Date;
+ defaultMonth?: Date;
+ onMonthChange?: (month: Date) => void;
+
+ minDate?: Date;
+ maxDate?: Date;
+ isDateUnavailable?: (date: Date) => boolean;
+
+ /** @defaultValue 'DD MMM YYYY' */
+ format?: string;
+ timeZone?: string;
+ /** @defaultValue 0 */
+ weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
+
+ /**
+ * `'immediate'` fires `onValueChange` on every interaction.
+ * `'explicit'` buffers until `Apply` (requires a `Footer`).
+ * @defaultValue 'immediate'
+ */
+ commit?: 'immediate' | 'explicit';
+
+ /** Compose in `Field` for UI. */
+ onValidityChange?: (validity: {
+ valid: boolean;
+ reason?: 'unparseable' | 'out-of-bounds' | 'unavailable';
+ }) => void;
+
+ disabled?: boolean;
+ readOnly?: boolean;
+ children?: ReactNode;
+}
+
+interface SingleProps extends CalendarPreviewBaseProps {
+ selection?: 'single';
+ value?: Date | null;
+ defaultValue?: Date | null;
+ onValueChange?: (value: Date | null) => void;
+}
+
+interface RangeProps extends CalendarPreviewBaseProps {
+ selection: 'range';
+ value?: DateRangeValue | null; // { from: Date | null; to: Date | null }
+ defaultValue?: DateRangeValue | null;
+ onValueChange?: (value: DateRangeValue | null) => void;
+ /** Makes one endpoint read-only in both the input and the grid. */
+ lock?: 'from' | 'to';
+}
+
+interface MultipleProps extends CalendarPreviewBaseProps {
+ selection: 'multiple';
+ value?: Date[];
+ defaultValue?: Date[];
+ onValueChange?: (value: Date[]) => void;
+ maxSelected?: number;
+}
+```
+
+| Choice | Replaces | Why |
+|---|---|---|
+| `onValueChange` | `onSelect` | Matches `Select`, `Combobox`, `Accordion`. Still fires on each step under `commit='immediate'`, but the value is always a complete `DateRangeValue` with explicit `null`s — today the docs must tell consumers to gate on `range.to`. |
+| `DateRangeValue` | RDP's `DateRange` | Ours, not react-day-picker's, which currently leaks through the barrel. |
+| `commit` | — | Makes a footer-with-actions layout expressible. Today `footer` is a bare `ReactNode` with no way to write back into state, which is why presets are unimplementable. |
+| `isDateUnavailable` | RDP's `disabled` matcher | Covers the common predicate without learning RDP's matcher DSL. RDP matchers stay reachable on `.Grid`. |
+| `onValidityChange` | `onErrorChange` | Still renders no error UI — `Field` does — but reports a state object with a reason, not a stringly-typed message. |
+| `lock` | whole-picker disable | `RangeProps` only. Makes one endpoint read-only in input and grid, closing the partial-disable gate. |
+
+### Parts
+
+| Part | Parent | Element | Purpose | Key props |
+|---|---|---|---|---|
+| `.Trigger` | root | `div` | Anchors the popover. Never renders a `