# Almanac — Animated App UI Block for React

> Scheduling widget in three panes: a host card with duration chips, a keyboard-navigable month grid with availability dots and direction-aware month slides, and a day's time slots that cascade in, with a 12h/24h toggle and an IANA time-zone select (plain Intl, no date library). Picking a slot splits it into Confirm, then a details form slides in, an async onBook runs, and the button morphs into a confirmation card with a drawn check and an .ics Add to calendar button.

- Page: https://crucibleui.com/components/app/almanac
- Category: [app](https://crucibleui.com/components/app)
- Tier: pro (one-time purchase, see https://crucibleui.com/pricing)
- Engine: motion
- npm dependencies: motion

## Install

```bash
npx shadcn@latest add @crucible/almanac
```

Pro install requires adding the token-bearing `@crucible` registry to components.json (issued at https://crucibleui.com/account after purchase).

Import from `@/components/crucible/almanac`.

## Props (`AlmanacProps`)

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `host?` | `AlmanacHost` | `a sample design-review host` | Host and event details for the left pane. |
| `durations?` | `number[]` | `[15, 30, 60]` | Duration chips in minutes. Changing the chip recomputes the slots. |
| `defaultDuration?` | `number` | `the middle entry of `durations`` | Initially selected duration. |
| `availability?` | `AlmanacAvailability` | — | Open slots for a day. Omit for a deterministic seeded generator (weekdays, working hours in `hostTimeZone`, a few busy blocks, nothing in the past, 60-day booking window) relative to `today`. |
| `today?` | `Date \| string` | — | The day treated as "today" — a `Date` or `"YYYY-MM-DD"`. Omit to use the real current day, computed after mount (a skeleton renders until then, so server and client markup match). |
| `timeZones?` | `string[]` | — | IANA zones offered in the time-zone select. The viewer's detected zone is added if missing. |
| `defaultTimeZone?` | `string` | `the viewer's detected zone (after mount)` | Initial time zone. |
| `hostTimeZone?` | `string` | `"America/New_York"` | Zone the default availability generator schedules working hours in. |
| `defaultHourCycle?` | `"12h" \| "24h"` | `"12h"` | Initial clock format. |
| `weekStartsOn?` | `0 \| 1` | `0` | First day of the week in the month grid (0 = Sunday, 1 = Monday). |
| `onBook?` | `(booking: AlmanacBooking) => void \| Promise<unknown>` | — | Called with the booking when the details form is submitted. Return a Promise to hold the button in a pending state — resolve morphs into the confirmation card, reject shows the error's message inline. |
| `onAddToCalendar?` | `(booking: AlmanacBooking) => void` | `downloads an .ics file for the booking` | Override "Add to calendar". |
| `onDurationChange?` | `(minutes: number) => void` | — | Fires when the duration chip changes. |
| `onTimeZoneChange?` | `(tz: string) => void` | — | Fires when the time zone changes. |
| `accentColor?` | `string` | `"#a5b4fc"` | Signature accent for selection, focus rings and the confirm button. |
| `ref?` | `React.Ref<AlmanacHandle>` | — | Imperative handle (`reset()`, `replay()`). React 19 ref-as-prop. |
| `className?` | `string` | — | Extra classes for the `<section>` root. |

Also accepts: `Omit<React.ComponentPropsWithoutRef<"section">, "children" | "onChange" | "onDurationChange">`.

## Usage example

```tsx
"use client";

import * as React from "react";

import { Almanac } from "@/components/crucible/almanac";

export default function AlmanacDemo() {
  return (
    <div className="relative isolate flex h-full min-h-[600px] w-full items-center justify-center overflow-hidden rounded-xl bg-neutral-950">
      <div
        aria-hidden
        className="pointer-events-none absolute inset-0 -z-10"
        style={{ background: "radial-gradient(60% 50% at 50% 0%, rgba(165,180,252,0.08), transparent 70%)" }}
      />
      <div className="relative z-10 flex w-full max-w-5xl flex-col gap-3 px-4 py-6 sm:px-6">
        <p className="text-xs text-neutral-500">Pick a day, then a time.</p>
        <Almanac onBook={() => new Promise<void>((resolve) => window.setTimeout(resolve, 1000))} />
      </div>
    </div>
  );
}
```

## Accessibility & performance

Honors `prefers-reduced-motion` with a designed static frame or fade (never a blank box) and passes `className` through.
