# Schedule Meeting

A booking panel composing a date range, a time window, a phone number, and a confirm step.

## Installation

```bash
npx shadcn@latest add https://mwui.vercel.app/r/schedule-meeting.json
```

[Registry JSON](https://mwui.vercel.app/r/schedule-meeting.json)

## Preview

```tsx
import { ScheduleMeeting } from "@/components/schedule-meeting";

export function Preview() {
  return <ScheduleMeeting />;
}
```


## Source

### components/schedule-meeting.tsx

```tsx
"use client";

import { IconCalendarEvent, IconClock, IconPhone } from "@tabler/icons-react";
import * as React from "react";

import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
  Card,
  CardAction,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";
import { ConfirmDialogProvider, useConfirm } from "@/components/ui/confirm-dialog";
import {
  DateRangePicker,
  formatRangeLabel,
  type DateRange,
} from "@/components/ui/date-range-picker";
import { Field, FieldDescription, FieldGroup, FieldLabel } from "@/components/ui/field";
import { PhoneInput } from "@/components/ui/phone-input";
import { Separator } from "@/components/ui/separator";
import { TimePicker } from "@/components/ui/time-picker";
import { cn } from "@/lib/utils";

type MeetingBooking = {
  range: DateRange | null;
  startTime: string | null;
  endTime: string | null;
  phone: string;
};

type ScheduleMeetingProps = React.ComponentProps<"div"> & {
  defaultBooking?: Partial<MeetingBooking>;
  locale?: string;
  onSchedule?: (booking: MeetingBooking) => void | Promise<void>;
};

function ScheduleMeeting({ ...props }: ScheduleMeetingProps) {
  return (
    <ConfirmDialogProvider>
      <ScheduleMeetingCard {...props} />
    </ConfirmDialogProvider>
  );
}

function ScheduleMeetingCard({
  defaultBooking,
  locale,
  onSchedule,
  className,
  ...props
}: ScheduleMeetingProps) {
  const confirm = useConfirm();

  const [range, setRange] = React.useState<DateRange | null>(defaultBooking?.range ?? null);
  const [startTime, setStartTime] = React.useState<string | null>(
    defaultBooking?.startTime ?? "09:00",
  );
  const [endTime, setEndTime] = React.useState<string | null>(defaultBooking?.endTime ?? "09:30");
  const [phone, setPhone] = React.useState(defaultBooking?.phone ?? "");
  const [scheduled, setScheduled] = React.useState(false);

  const booking: MeetingBooking = { range, startTime, endTime, phone };
  const isComplete = Boolean(range?.end && startTime && endTime && phone.trim().length > 6);

  const handleSchedule = async () => {
    const accepted = await confirm({
      title: "Confirm this meeting?",
      description: `${formatRangeLabel(range, locale)} · ${startTime ?? ""}–${endTime ?? ""} · ${phone}`,
      confirmLabel: "Schedule",
      cancelLabel: "Back",
      onConfirm: () => onSchedule?.(booking),
    });

    if (accepted) {
      setScheduled(true);
    }
  };

  return (
    <Card className={cn("w-full max-w-xl", className)} {...props}>
      <CardHeader>
        <CardTitle>Schedule a meeting</CardTitle>
        <CardDescription>Pick the dates, a time window, and a number to call.</CardDescription>
        <CardAction>
          <Badge variant={scheduled ? "default" : "secondary"}>
            {scheduled ? "Scheduled" : "Draft"}
          </Badge>
        </CardAction>
      </CardHeader>

      <CardContent>
        <FieldGroup>
          <Field>
            <FieldLabel htmlFor="schedule-meeting-dates">Dates</FieldLabel>
            <DateRangePicker
              id="schedule-meeting-dates"
              locale={locale}
              numberOfMonths={1}
              onValueChange={(next) => {
                setRange(next);
                setScheduled(false);
              }}
              presets={false}
              value={range}
            />
          </Field>

          <div className="grid gap-4 sm:grid-cols-2">
            <Field>
              <FieldLabel htmlFor="schedule-meeting-start">Starts at</FieldLabel>
              <TimePicker
                id="schedule-meeting-start"
                onValueChange={(next) => {
                  setStartTime(next);
                  setScheduled(false);
                }}
                value={startTime}
              />
            </Field>
            <Field>
              <FieldLabel htmlFor="schedule-meeting-end">Ends at</FieldLabel>
              <TimePicker
                id="schedule-meeting-end"
                min={startTime ?? undefined}
                onValueChange={(next) => {
                  setEndTime(next);
                  setScheduled(false);
                }}
                value={endTime}
              />
              <FieldDescription>Must be after the start time.</FieldDescription>
            </Field>
          </div>

          <Field>
            <FieldLabel htmlFor="schedule-meeting-phone">Contact number</FieldLabel>
            <PhoneInput
              id="schedule-meeting-phone"
              onValueChange={(next) => {
                setPhone(next);
                setScheduled(false);
              }}
              value={phone}
            />
          </Field>
        </FieldGroup>

        <Separator className="my-6" />

        <dl className="grid gap-3 text-sm">
          <SummaryRow icon={<IconCalendarEvent />} label="When">
            {formatRangeLabel(range, locale)}
          </SummaryRow>
          <SummaryRow icon={<IconClock />} label="Time">
            {startTime && endTime ? `${startTime} – ${endTime}` : "No time selected"}
          </SummaryRow>
          <SummaryRow icon={<IconPhone />} label="Call">
            {phone.trim() || "No number yet"}
          </SummaryRow>
        </dl>
      </CardContent>

      <CardFooter className="justify-end gap-2">
        <Button
          onClick={() => {
            setRange(null);
            setPhone("");
            setScheduled(false);
          }}
          variant="ghost"
        >
          Reset
        </Button>
        <Button
          disabled={!isComplete || scheduled}
          onClick={() => {
            void handleSchedule();
          }}
        >
          {scheduled ? "Scheduled" : "Review and schedule"}
        </Button>
      </CardFooter>
    </Card>
  );
}

function SummaryRow({
  icon,
  label,
  children,
}: {
  icon: React.ReactNode;
  label: string;
  children: React.ReactNode;
}) {
  return (
    <div className="flex items-center gap-3">
      <span className="text-muted-foreground [&_svg]:size-4">{icon}</span>
      <dt className="w-12 shrink-0 text-muted-foreground">{label}</dt>
      <dd className="font-medium">{children}</dd>
    </div>
  );
}

export { ScheduleMeeting, type MeetingBooking, type ScheduleMeetingProps };
```



## Usage

A booking panel for scheduling a call. It pairs the date range picker with two time pickers, a
phone input, and a live summary, then routes the submit through a confirmation dialog.

The block renders its own `ConfirmDialogProvider`, so it works standalone without extra setup. The
end time picker receives the start time as its `min`, so the two stay consistent.

```tsx
import { ScheduleMeeting } from "@/components/schedule-meeting";

<ScheduleMeeting
  onSchedule={(booking) => {
    console.log(booking);
  }}
/>;
```

`onSchedule` runs while the confirmation dialog is pending, so returning a promise keeps the dialog
in its loading state until the request settles.

