# Sync (Booking) — `@sonordev/site-kit/sync`

Embeddable booking/scheduling widget — like Calendly, built into Sonor. Appointments, consultations, classes.

## Usage

```tsx
'use client'
import { BookingWidget } from '@sonordev/site-kit/sync'

export default function BookPage() {
  return (
    <BookingWidget
      onBookingComplete={(result) => {
        console.log('Booked:', result.booking.confirmationCode)
      }}
    />
  )
}
```

## Props

```ts
interface BookingWidgetProps {
  orgSlug?: string                    // Organization slug (for public mode)
  apiKey?: string                     // API key (auto-resolved from env)
  apiUrl?: string                     // Default: https://api.sonor.io
  bookingTypeSlug?: string            // Show specific type only
  timezone?: string                   // Guest timezone (auto-detected)
  className?: string
  daysToShow?: number                 // Days of availability to display
  onBookingComplete?: (result: BookingResult) => void
  onError?: (error: Error) => void
  hideTypeSelector?: boolean          // Hide type selector for single-type embed

  styles?: {                          // Custom theme
    primaryColor?: string
    borderRadius?: string
    fontFamily?: string
    backgroundColor?: string
    textPrimary?: string
    borderColor?: string
  }
}
```

## UI Flow

1. **Type selector:** choose a booking type, unless a specific `bookingTypeSlug` is provided.
2. **Meeting format:** when the booking type enables meeting options, choose virtual, office, or a visit to the guest's location. Travel meetings validate the address before showing availability.
3. **Calendar and times:** choose an available time and reserve it while completing contact details.
4. **Guest info:** name, email, phone, and notes. Every booking requires a phone number.
5. **Result:** a confirmed booking shows calendar links when Sonor sends the guest's invitation. When the host's Google Calendar sends it, the widget says who the invite is coming from instead (see [Calendar invitations](#calendar-invitations)). Meetings that require review show a pending request without calendar links.

Changing a meeting format restores the selected format, address, and access notes. Guests can return to the service selector when it's available. Contact details survive changes to the format or reserved time.

Expired meeting details and time reservations show a recovery action. Guests can recheck their meeting details or choose another time without re-entering their contact information. An active hold remains usable if its prepared meeting token expires, until the hold's own deadline.

Navigation is disabled while a reservation or booking request is running. Stale responses cannot reopen a previous step, and holds created after the widget is removed are released.

## Accessibility

Screen readers and AI agent browsers (which act on elements by their accessible names) can book without guessing:

- **Days** are buttons named with the full date, year included: "Wednesday, October 7, 2026". The picked day is pressed (`aria-pressed="true"`), and days that can't be booked are disabled.
- **Times** are buttons named with the time and the date, "10:30 AM, Wednesday, October 7, 2026", in the booking's time zone. They still show just the time. The picked time is pressed.
- **Confirm**, the button that reserves the picked time, is named "Confirm 10:30 AM, Wednesday, October 7, 2026".

Every name contains what its button shows (a time's and Confirm's start with it), so voice control can still target a button by its visible text (WCAG 2.5.3). None of this changes what the widget sends.

## API Functions

```ts
import {
  fetchBookingTypes, fetchBookingTypeDetails,
  fetchAvailability, fetchAvailableDates,
  createSlotHold, releaseSlotHold, createBooking,
  prepareMeeting,
  detectTimezone, formatTime, formatDate, formatDuration,
} from '@sonordev/site-kit/sync'
```

## Types

```ts
interface BookingType {
  slug: string; name: string; description?: string;
  duration_minutes: number; color?: string;
  location_type: 'virtual' | 'phone' | 'in_person' | 'custom';
  price_cents?: number; currency?: string; is_active: boolean;
}

interface BookingResult {
  success: boolean
  booking: {
    id: string; confirmationCode: string; scheduledAt: string;
    durationMinutes: number; hostName?: string; timezone: string;
  }
  cancelUrl: string; rescheduleUrl: string;
  invitation?: 'google' | 'sonor'   // who sends the guest's calendar invite
  calendarLinks: { google: string; outlook: string; ics: string }
}
```

## Calendar invitations

Each booking gives the guest one calendar invitation, and `invitation` says who sends it:

- **`'google'`**: the host has Google Calendar connected, so Google invites the guest to the host's event. Sonor's emails don't attach a second invite, and the success screen says who the invite is coming from instead of showing calendar links. The Google and Outlook links build a separate event with no tie to that invitation, so a guest who clicked one would have two entries for the same meeting.
- **`'sonor'`**: there's no Google event for the booking, usually because the host hasn't connected a calendar. Sonor's confirmation email carries the invite, and the success screen shows the calendar links.

APIs older than the field don't send it; treat a missing `invitation` like `'sonor'`. `calendarLinks` is still sent for every booking so older widgets keep working. If you build your own success screen with `createBooking`, check `invitation` before you render `calendarLinks`.

## Features

- Automatic timezone detection
- Slot hold with expiry (prevents double-booking)
- Business hours support
- Multiple hosts per booking type
- Custom meeting URL / location

## Booking regression checks

Run `pnpm exec vitest run src/sync/booking-flow.test.ts` for expiry and server-error recovery checks. Start `pnpm exec vite --host 127.0.0.1 --port 5189`, then run `node test/meeting-flow/run.mjs` and `node test/meeting-flow/regression.mjs` for the real widget flows. The browser harness intercepts requests to a reserved test domain, so it creates no live bookings.
