Calendar Availability
Original · freeavailability slot picker
byWeb InnoventixReact + Tailwind
calendaravailabilitycalendars
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/calendar-availability.jsoncalendar-availability.tsx
"use client";
import { type KeyboardEvent, useEffect, useId, useMemo, useRef, useState } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
type Duration = 30 | 60;
type Cell = {
date: Date;
key: string;
inMonth: boolean;
isPast: boolean;
isToday: boolean;
weekend: boolean;
count: number;
};
type DisplaySlot = {
start: string;
startLabel: string;
endLabel: string;
};
type Booking = {
dateLabel: string;
startLabel: string;
endLabel: string;
duration: Duration;
timezoneLabel: string;
};
const TIMES = [
"09:00", "09:30", "10:00", "10:30", "11:00", "11:30",
"13:00", "13:30", "14:00", "14:30", "15:00", "15:30", "16:00", "16:30",
] as const;
const MONTHS_LONG = [
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December",
];
const WEEKDAYS_LONG = [
"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday",
];
const WEEKDAY_MIN = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
const TIMEZONES = [
{ value: "pt", label: "Pacific Time · GMT-7" },
{ value: "et", label: "Eastern Time · GMT-4" },
{ value: "gmt", label: "London · GMT+1" },
{ value: "pkt", label: "Karachi · GMT+5" },
{ value: "sgt", label: "Singapore · GMT+8" },
];
function keyOf(d: Date): string {
return `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
}
function addDays(d: Date, n: number): Date {
return new Date(d.getFullYear(), d.getMonth(), d.getDate() + n);
}
function addMonths(d: Date, n: number): Date {
const base = new Date(d.getFullYear(), d.getMonth() + n, 1);
const last = new Date(base.getFullYear(), base.getMonth() + 1, 0).getDate();
base.setDate(Math.min(d.getDate(), last));
return base;
}
function startOfWeekMonday(d: Date): Date {
const g = (d.getDay() + 6) % 7;
return addDays(d, -g);
}
function toMinutes(t: string): number {
const [h, m] = t.split(":").map(Number);
return h * 60 + m;
}
function label12(mins: number): string {
const h24 = Math.floor(mins / 60);
const m = mins % 60;
const ampm = h24 < 12 ? "AM" : "PM";
let h = h24 % 12;
if (h === 0) h = 12;
return `${h}:${m.toString().padStart(2, "0")} ${ampm}`;
}
function isWeekend(d: Date): boolean {
const g = d.getDay();
return g === 0 || g === 6;
}
function slotAvailability(d: Date): boolean[] {
const n = d.getFullYear() * 10000 + (d.getMonth() + 1) * 100 + d.getDate();
return TIMES.map((_, i) => {
const h = (n * 9301 + i * 49297 + i * i * 769) >>> 0;
return h % 100 >= 42;
});
}
function longDate(d: Date): string {
return `${WEEKDAYS_LONG[d.getDay()]}, ${MONTHS_LONG[d.getMonth()]} ${d.getDate()}`;
}
export default function CalendarAvailability() {
const reduce = useReducedMotion();
const monthLabelId = useId();
const gridLabelId = useId();
const [today] = useState<Date>(() => {
const n = new Date();
return new Date(n.getFullYear(), n.getMonth(), n.getDate());
});
const [viewYear, setViewYear] = useState<number>(today.getFullYear());
const [viewMonth, setViewMonth] = useState<number>(today.getMonth());
const [focusedDate, setFocusedDate] = useState<Date>(today);
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
const [duration, setDuration] = useState<Duration>(30);
const [selectedSlot, setSelectedSlot] = useState<string | null>(null);
const [timezone, setTimezone] = useState<string>(TIMEZONES[0].value);
const [booking, setBooking] = useState<Booking | null>(null);
const dayRefs = useRef<Map<string, HTMLButtonElement | null>>(new Map());
const shouldFocusRef = useRef<boolean>(false);
useEffect(() => {
if (!shouldFocusRef.current) return;
const el = dayRefs.current.get(keyOf(focusedDate));
if (el) el.focus();
shouldFocusRef.current = false;
}, [focusedDate]);
const cells = useMemo<Cell[]>(() => {
const start = startOfWeekMonday(new Date(viewYear, viewMonth, 1));
const list: Cell[] = [];
for (let i = 0; i < 42; i++) {
const date = addDays(start, i);
const inMonth = date.getMonth() === viewMonth;
const isPast = date.getTime() < today.getTime();
const weekend = isWeekend(date);
const count = isPast || weekend ? 0 : slotAvailability(date).filter(Boolean).length;
list.push({
date,
key: keyOf(date),
inMonth,
isPast,
isToday: keyOf(date) === keyOf(today),
weekend,
count,
});
}
return list;
}, [viewYear, viewMonth, today]);
const displaySlots = useMemo<DisplaySlot[]>(() => {
if (!selectedDate) return [];
const avail = slotAvailability(selectedDate);
const out: DisplaySlot[] = [];
for (let i = 0; i < TIMES.length; i++) {
if (!avail[i]) continue;
const startMin = toMinutes(TIMES[i]);
if (duration === 30) {
out.push({ start: TIMES[i], startLabel: label12(startMin), endLabel: label12(startMin + 30) });
} else {
const next = i + 1;
if (next < TIMES.length && avail[next] && toMinutes(TIMES[next]) - startMin === 30) {
out.push({ start: TIMES[i], startLabel: label12(startMin), endLabel: label12(startMin + 60) });
}
}
}
return out;
}, [selectedDate, duration]);
const focusedKey = keyOf(focusedDate);
const selectedKey = selectedDate ? keyOf(selectedDate) : null;
const timezoneLabel = TIMEZONES.find((t) => t.value === timezone)?.label ?? "";
const canPrev =
viewYear > today.getFullYear() ||
(viewYear === today.getFullYear() && viewMonth > today.getMonth());
function isSelectable(c: Cell): boolean {
return c.inMonth && !c.isPast && !c.weekend && c.count > 0;
}
function goMonth(delta: number) {
if (delta < 0 && !canPrev) return;
const base = new Date(viewYear, viewMonth + delta, 1);
setViewYear(base.getFullYear());
setViewMonth(base.getMonth());
setFocusedDate(base);
}
function moveFocus(target: Date) {
if (target.getFullYear() !== viewYear || target.getMonth() !== viewMonth) {
setViewYear(target.getFullYear());
setViewMonth(target.getMonth());
}
shouldFocusRef.current = true;
setFocusedDate(target);
}
function onDayKeyDown(e: KeyboardEvent<HTMLButtonElement>, date: Date) {
let handled = true;
switch (e.key) {
case "ArrowRight": moveFocus(addDays(date, 1)); break;
case "ArrowLeft": moveFocus(addDays(date, -1)); break;
case "ArrowDown": moveFocus(addDays(date, 7)); break;
case "ArrowUp": moveFocus(addDays(date, -7)); break;
case "Home": moveFocus(startOfWeekMonday(date)); break;
case "End": moveFocus(addDays(startOfWeekMonday(date), 6)); break;
case "PageUp": moveFocus(addMonths(date, -1)); break;
case "PageDown": moveFocus(addMonths(date, 1)); break;
default: handled = false;
}
if (handled) e.preventDefault();
}
function selectDay(c: Cell) {
if (!isSelectable(c)) return;
setSelectedDate(c.date);
setSelectedSlot(null);
setBooking(null);
setFocusedDate(c.date);
}
function changeDuration(d: Duration) {
setDuration(d);
setSelectedSlot(null);
setBooking(null);
}
function confirmBooking() {
if (!selectedDate || !selectedSlot) return;
const slot = displaySlots.find((s) => s.start === selectedSlot);
if (!slot) return;
setBooking({
dateLabel: longDate(selectedDate),
startLabel: slot.startLabel,
endLabel: slot.endLabel,
duration,
timezoneLabel,
});
}
const selectedSlotLabel = selectedSlot
? displaySlots.find((s) => s.start === selectedSlot)?.startLabel ?? null
: null;
const announce = booking
? `Confirmed: ${booking.dateLabel} at ${booking.startLabel}, ${booking.timezoneLabel}.`
: selectedDate
? selectedSlotLabel
? `${longDate(selectedDate)} at ${selectedSlotLabel} selected. Ready to confirm.`
: `${longDate(selectedDate)} selected. ${displaySlots.length} ${duration}-minute openings.`
: "Select a date to view available times.";
return (
<section className="relative w-full overflow-hidden bg-slate-50 px-4 py-16 text-slate-900 sm:px-6 sm:py-20 dark:bg-slate-950 dark:text-slate-100">
<style>{`
@keyframes cavail-rise {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes cavail-pop {
0% { transform: scale(.85); opacity: 0; }
60% { transform: scale(1.04); }
100% { transform: scale(1); opacity: 1; }
}
@keyframes cavail-draw {
from { stroke-dashoffset: 26; }
to { stroke-dashoffset: 0; }
}
@keyframes cavail-ping {
0% { transform: scale(1); opacity: .55; }
70%, 100% { transform: scale(2.4); opacity: 0; }
}
.cavail-slot { animation: cavail-rise .32s cubic-bezier(.22,.8,.28,1) both; }
.cavail-check-path {
stroke-dasharray: 26;
stroke-dashoffset: 26;
animation: cavail-draw .5s .12s cubic-bezier(.65,0,.35,1) forwards;
}
.cavail-ring { animation: cavail-ping 1.9s cubic-bezier(0,0,.2,1) infinite; }
@media (prefers-reduced-motion: reduce) {
.cavail-slot, .cavail-check-path, .cavail-ring { animation: none !important; }
.cavail-check-path { stroke-dashoffset: 0; }
}
`}</style>
{/* atmosphere */}
<div
aria-hidden="true"
className="pointer-events-none absolute -top-24 left-1/2 h-72 w-[42rem] max-w-[90vw] -translate-x-1/2 rounded-full bg-gradient-to-br from-indigo-300/40 via-violet-300/25 to-sky-200/30 blur-3xl dark:from-indigo-600/25 dark:via-violet-700/15 dark:to-sky-800/15"
/>
<div className="relative mx-auto w-full max-w-5xl">
<div className="overflow-hidden rounded-3xl border border-slate-200/80 bg-white shadow-xl shadow-slate-900/5 ring-1 ring-slate-900/5 dark:border-slate-800 dark:bg-slate-900 dark:shadow-black/40 dark:ring-white/5">
{/* header */}
<div className="flex flex-col gap-5 border-b border-slate-200/80 p-6 sm:p-8 dark:border-slate-800">
<div className="flex items-start gap-4">
<span
aria-hidden="true"
className="flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl bg-gradient-to-br from-indigo-500 to-violet-600 text-base font-semibold tracking-tight text-white shadow-lg shadow-indigo-500/25"
>
AR
</span>
<div className="min-w-0">
<div className="flex items-center gap-2">
<span className="relative flex h-2 w-2">
<span className="cavail-ring absolute inline-flex h-full w-full rounded-full bg-emerald-500" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-emerald-500" />
</span>
<span className="text-xs font-medium uppercase tracking-wider text-emerald-600 dark:text-emerald-400">
Booking open
</span>
</div>
<h2 className="mt-1 text-xl font-semibold tracking-tight text-slate-900 sm:text-2xl dark:text-white">
{duration}-minute intro call with Ava Reyes
</h2>
<p className="mt-0.5 text-sm text-slate-500 dark:text-slate-400">
Product strategy consult · Video via Meet · No prep needed
</p>
</div>
</div>
{/* controls */}
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
<fieldset className="min-w-0">
<legend className="mb-1.5 text-xs font-medium text-slate-500 dark:text-slate-400">
Meeting length
</legend>
<div className="inline-flex rounded-xl bg-slate-100 p-1 dark:bg-slate-800">
{([30, 60] as Duration[]).map((d) => (
<label key={d} className="relative cursor-pointer">
<input
type="radio"
name="cavail-duration"
value={d}
checked={duration === d}
onChange={() => changeDuration(d)}
className="peer sr-only"
/>
<span className="block rounded-lg px-4 py-1.5 text-sm font-medium text-slate-600 transition-colors peer-checked:bg-white peer-checked:text-indigo-700 peer-checked:shadow-sm peer-focus-visible:outline peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-indigo-500 dark:text-slate-300 dark:peer-checked:bg-slate-950 dark:peer-checked:text-indigo-300">
{d} min
</span>
</label>
))}
</div>
</fieldset>
<div className="min-w-0">
<label
htmlFor="cavail-tz"
className="mb-1.5 block text-xs font-medium text-slate-500 dark:text-slate-400"
>
Time zone
</label>
<div className="relative">
<select
id="cavail-tz"
value={timezone}
onChange={(e) => setTimezone(e.target.value)}
className="w-full appearance-none rounded-xl border border-slate-200 bg-white py-2 pl-3 pr-9 text-sm font-medium text-slate-700 transition-colors hover:border-slate-300 focus-visible:border-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 sm:w-64 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-200 dark:hover:border-slate-600"
>
{TIMEZONES.map((tz) => (
<option key={tz.value} value={tz.value}>
{tz.label}
</option>
))}
</select>
<svg
aria-hidden="true"
viewBox="0 0 24 24"
className="pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="m6 9 6 6 6-6" />
</svg>
</div>
</div>
</div>
</div>
{/* body */}
<div className="grid gap-0 md:grid-cols-2">
{/* calendar */}
<div className="border-b border-slate-200/80 p-6 sm:p-8 md:border-b-0 md:border-r dark:border-slate-800">
<div className="mb-4 flex items-center justify-between">
<h3 id={monthLabelId} className="text-base font-semibold tracking-tight text-slate-900 dark:text-white">
{MONTHS_LONG[viewMonth]} {viewYear}
</h3>
<div className="flex gap-1">
<button
type="button"
onClick={() => goMonth(-1)}
disabled={!canPrev}
aria-label="Previous month"
className="flex h-9 w-9 items-center justify-center rounded-lg border border-slate-200 text-slate-600 transition-colors hover:bg-slate-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-800"
>
<svg viewBox="0 0 24 24" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="m15 18-6-6 6-6" />
</svg>
</button>
<button
type="button"
onClick={() => goMonth(1)}
aria-label="Next month"
className="flex h-9 w-9 items-center justify-center rounded-lg border border-slate-200 text-slate-600 transition-colors hover:bg-slate-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-800"
>
<svg viewBox="0 0 24 24" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="m9 18 6-6-6-6" />
</svg>
</button>
</div>
</div>
<div className="grid grid-cols-7 gap-1">
{WEEKDAY_MIN.map((w) => (
<div
key={w}
className="pb-2 text-center text-xs font-medium text-slate-400 dark:text-slate-500"
aria-hidden="true"
>
{w}
</div>
))}
</div>
<div
role="grid"
aria-labelledby={`${monthLabelId} ${gridLabelId}`}
className="grid grid-cols-7 gap-1"
>
<span id={gridLabelId} className="sr-only">
Choose an available date
</span>
{cells.map((c) => {
if (!c.inMonth) {
return (
<div key={c.key} role="gridcell" aria-hidden="true" className="aspect-square" />
);
}
const selectable = isSelectable(c);
const isSelected = c.key === selectedKey;
const tabbable = c.key === focusedKey;
return (
<div key={c.key} role="gridcell" className="relative">
<button
ref={(el) => {
dayRefs.current.set(c.key, el);
}}
type="button"
tabIndex={tabbable ? 0 : -1}
aria-selected={isSelected}
aria-disabled={!selectable}
aria-label={`${longDate(c.date)}, ${
selectable
? `${c.count} openings`
: c.weekend
? "closed"
: c.isPast
? "past"
: "no openings"
}`}
onClick={() => selectDay(c)}
onKeyDown={(e) => onDayKeyDown(e, c.date)}
className={[
"flex aspect-square w-full flex-col items-center justify-center rounded-xl text-sm font-medium transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500",
isSelected
? "bg-indigo-600 text-white shadow-md shadow-indigo-500/30 dark:bg-indigo-500"
: selectable
? "text-slate-700 hover:bg-indigo-50 dark:text-slate-200 dark:hover:bg-indigo-500/10"
: "cursor-not-allowed text-slate-300 dark:text-slate-600",
!isSelected && c.isToday ? "ring-1 ring-inset ring-indigo-400 dark:ring-indigo-500" : "",
].join(" ")}
>
<span>{c.date.getDate()}</span>
<span
aria-hidden="true"
className={[
"mt-0.5 h-1 w-1 rounded-full",
isSelected
? "bg-white/80"
: selectable
? "bg-emerald-500"
: "bg-transparent",
].join(" ")}
/>
</button>
</div>
);
})}
</div>
<div className="mt-4 flex items-center gap-4 text-xs text-slate-500 dark:text-slate-400">
<span className="flex items-center gap-1.5">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500" aria-hidden="true" />
Available
</span>
<span className="flex items-center gap-1.5">
<span className="h-3 w-3 rounded-full ring-1 ring-indigo-400 dark:ring-indigo-500" aria-hidden="true" />
Today
</span>
</div>
</div>
{/* slots / confirmation */}
<div className="p-6 sm:p-8">
<AnimatePresence mode="wait" initial={false}>
{booking ? (
<motion.div
key="confirmed"
initial={reduce ? false : { opacity: 0, scale: 0.97 }}
animate={{ opacity: 1, scale: 1 }}
exit={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.98 }}
transition={{ duration: reduce ? 0 : 0.3, ease: [0.22, 0.8, 0.28, 1] }}
className="flex h-full flex-col"
>
<div className="flex flex-1 flex-col items-center justify-center py-6 text-center">
<span
style={reduce ? undefined : { animation: "cavail-pop .4s cubic-bezier(.22,.8,.28,1) both" }}
className="flex h-16 w-16 items-center justify-center rounded-2xl bg-emerald-100 text-emerald-600 dark:bg-emerald-500/15 dark:text-emerald-400"
>
<svg viewBox="0 0 24 24" className="h-8 w-8" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path className="cavail-check-path" d="m5 13 4 4L19 7" />
</svg>
</span>
<h3 className="mt-5 text-lg font-semibold tracking-tight text-slate-900 dark:text-white">
You’re booked
</h3>
<p className="mt-1 text-sm text-slate-500 dark:text-slate-400">
A calendar invite is on its way to your inbox.
</p>
<dl className="mt-6 w-full max-w-xs space-y-2.5 rounded-2xl border border-slate-200 bg-slate-50 p-4 text-left text-sm dark:border-slate-800 dark:bg-slate-950/50">
<div className="flex justify-between gap-4">
<dt className="text-slate-500 dark:text-slate-400">Date</dt>
<dd className="font-medium text-slate-900 dark:text-white">{booking.dateLabel}</dd>
</div>
<div className="flex justify-between gap-4">
<dt className="text-slate-500 dark:text-slate-400">Time</dt>
<dd className="font-medium text-slate-900 dark:text-white">
{booking.startLabel} – {booking.endLabel}
</dd>
</div>
<div className="flex justify-between gap-4">
<dt className="text-slate-500 dark:text-slate-400">Zone</dt>
<dd className="font-medium text-slate-900 dark:text-white">{booking.timezoneLabel}</dd>
</div>
</dl>
</div>
<button
type="button"
onClick={() => setBooking(null)}
className="mt-2 w-full rounded-xl border border-slate-200 px-4 py-2.5 text-sm font-medium text-slate-600 transition-colors hover:bg-slate-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-800"
>
Change time
</button>
</motion.div>
) : (
<motion.div
key="slots"
initial={reduce ? false : { opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: reduce ? 0 : 0.2 }}
className="flex h-full flex-col"
>
<div className="mb-4">
<h3 className="text-base font-semibold tracking-tight text-slate-900 dark:text-white">
{selectedDate ? longDate(selectedDate) : "Pick a date"}
</h3>
<p className="mt-0.5 text-sm text-slate-500 dark:text-slate-400">
{selectedDate
? `${displaySlots.length} ${duration}-minute ${
displaySlots.length === 1 ? "opening" : "openings"
} · ${timezoneLabel}`
: "Available times appear here once you choose a day."}
</p>
</div>
{!selectedDate ? (
<div className="flex flex-1 flex-col items-center justify-center rounded-2xl border border-dashed border-slate-200 py-14 text-center dark:border-slate-800">
<svg viewBox="0 0 24 24" className="h-10 w-10 text-slate-300 dark:text-slate-600" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<rect x="3" y="4" width="18" height="18" rx="2" />
<path d="M16 2v4M8 2v4M3 10h18" />
</svg>
<p className="mt-3 max-w-[16rem] text-sm text-slate-400 dark:text-slate-500">
Select an available date from the calendar to see open times.
</p>
</div>
) : displaySlots.length === 0 ? (
<div className="flex flex-1 flex-col items-center justify-center rounded-2xl border border-dashed border-slate-200 py-14 text-center dark:border-slate-800">
<p className="text-sm font-medium text-slate-500 dark:text-slate-400">
No {duration}-minute openings on this day.
</p>
<p className="mt-1 text-sm text-slate-400 dark:text-slate-500">
Try a 30-minute call or another date.
</p>
</div>
) : (
<div
role="listbox"
aria-label={`Available times on ${longDate(selectedDate)}`}
className="grid max-h-72 grid-cols-2 gap-2 overflow-y-auto pr-1 sm:grid-cols-3"
>
{displaySlots.map((slot, i) => {
const active = slot.start === selectedSlot;
return (
<button
key={slot.start}
type="button"
role="option"
aria-selected={active}
aria-label={`${slot.startLabel} to ${slot.endLabel}`}
onClick={() => setSelectedSlot(active ? null : slot.start)}
style={reduce ? undefined : { animationDelay: `${i * 26}ms` }}
className={[
"cavail-slot rounded-xl border px-2 py-2.5 text-center text-sm font-medium tabular-nums transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500",
active
? "border-indigo-600 bg-indigo-600 text-white shadow-sm shadow-indigo-500/30 dark:border-indigo-500 dark:bg-indigo-500"
: "border-slate-200 text-slate-700 hover:border-indigo-400 hover:bg-indigo-50 dark:border-slate-700 dark:text-slate-200 dark:hover:border-indigo-500 dark:hover:bg-indigo-500/10",
].join(" ")}
>
{slot.startLabel}
</button>
);
})}
</div>
)}
<button
type="button"
onClick={confirmBooking}
disabled={!selectedSlot}
className="mt-5 flex w-full items-center justify-center gap-2 rounded-xl bg-indigo-600 px-4 py-3 text-sm font-semibold text-white shadow-lg shadow-indigo-500/25 transition-colors hover:bg-indigo-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 disabled:cursor-not-allowed disabled:bg-slate-200 disabled:text-slate-400 disabled:shadow-none dark:bg-indigo-500 dark:hover:bg-indigo-400 dark:disabled:bg-slate-800 dark:disabled:text-slate-600"
>
{selectedSlotLabel ? (
<>
Confirm {selectedSlotLabel}
<svg viewBox="0 0 24 24" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M5 12h14M13 6l6 6-6 6" />
</svg>
</>
) : (
"Select a time"
)}
</button>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
</div>
<p className="mt-4 text-center text-xs text-slate-400 dark:text-slate-500">
Times shown in {timezoneLabel}. Reschedule anytime from your confirmation email.
</p>
<div aria-live="polite" role="status" className="sr-only">
{announce}
</div>
</div>
</section>
);
}Dependencies
motion
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Calendar Date Picker
Originaldate picker with a dropdown calendar

Calendar Range Picker
Originaldate range picker with two months

Calendar Inline
Originalalways-visible inline calendar

Calendar Month Year
Originalmonth and year quick selector

Calendar Event
Originalmonth calendar with event dots

Calendar Mini
Originalcompact mini calendar widget

Calendar Time Picker
Originaltime picker with hour/minute

Calendar Datetime
Originalcombined date and time picker

Calendar Week
Originalweek view with time slots

Calendar Agenda
Originalagenda list of upcoming days

Calendar Booking
Originalbooking calendar with time slots

Calendar Multi Month
Originaltwo-month scrolling calendar

