{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "booking-slots",
  "type": "registry:ui",
  "title": "Booking Slots",
  "description": "Pick a day, then a time that splits in place to reveal Confirm.",
  "categories": [
    "scheduling"
  ],
  "dependencies": [
    "@base-ui/react",
    "motion"
  ],
  "registryDependencies": [
    "https://www.stealth.pm/r/tokens.json",
    "https://www.stealth.pm/r/cn.json",
    "https://www.stealth.pm/r/icons.json",
    "https://www.stealth.pm/r/motion.json",
    "https://www.stealth.pm/r/use-controllable-state.json"
  ],
  "files": [
    {
      "path": "components/ui/booking-slots.tsx",
      "content": "\"use client\";\nimport { Toggle } from \"@base-ui/react/toggle\";\nimport { ToggleGroup } from \"@base-ui/react/toggle-group\";\nimport { AnimatePresence, LayoutGroup, motion, useIsPresent, useReducedMotion } from \"motion/react\";\nimport { useCallback, useEffect, useId, useMemo, useRef, useState, useSyncExternalStore } from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { ArrowRight, ChevronLeft, ChevronRight, Globe, Loader, Refresh } from \"@/lib/icons\";\nimport { ease, spring } from \"@/lib/motion\";\nimport { useControllableState } from \"@/lib/use-controllable-state\";\n\n/* ------------------------------------------------------------------ */\n/* Plain days. Every Date used as a day is local midnight and stands    */\n/* for a calendar date in the display time zone; math goes through the  */\n/* calendar fields, never milliseconds, so DST can't move a day.        */\n/* ------------------------------------------------------------------ */\n\nconst startOfDay = (d: Date) => new Date(d.getFullYear(), d.getMonth(), d.getDate());\nconst startOfMonth = (d: Date) => new Date(d.getFullYear(), d.getMonth(), 1);\nconst addDays = (d: Date, n: number) => new Date(d.getFullYear(), d.getMonth(), d.getDate() + n);\nconst addMonths = (d: Date, n: number) => new Date(d.getFullYear(), d.getMonth() + n, 1);\nconst dayNum = (d: Date) => d.getFullYear() * 10000 + d.getMonth() * 100 + d.getDate();\nconst compareDays = (a: Date, b: Date) => dayNum(a) - dayNum(b);\nconst sameDay = (a: Date | null | undefined, b: Date | null | undefined) => !!a && !!b && dayNum(a) === dayNum(b);\nconst monthIndex = (d: Date) => d.getFullYear() * 12 + d.getMonth();\nconst pad = (n: number, w = 2) => String(n).padStart(w, \"0\");\nconst dayKey = (d: Date) => `${pad(d.getFullYear(), 4)}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;\nconst fromDayKey = (s: string) => {\n  const [y, m, d] = s.split(\"-\").map(Number);\n  return new Date(y, m - 1, d);\n};\n\n/* ------------------------------------------------------------------ */\n/* Time zones, through Intl only.                                      */\n/* ------------------------------------------------------------------ */\n\nconst partsCache = new Map<string, Intl.DateTimeFormat>();\nfunction zoneParts(at: Date, timeZone: string) {\n  let f = partsCache.get(timeZone);\n  if (!f) {\n    f = new Intl.DateTimeFormat(\"en-US\", { timeZone, hourCycle: \"h23\", year: \"numeric\", month: \"numeric\", day: \"numeric\", hour: \"numeric\", minute: \"numeric\", second: \"numeric\" });\n    partsCache.set(timeZone, f);\n  }\n  const p: Record<string, number> = {};\n  for (const part of f.formatToParts(at)) if (part.type !== \"literal\") p[part.type] = Number(part.value);\n  return p;\n}\n\n/** Minutes the zone is ahead of UTC at that instant (London in summer is 60). */\nexport function zoneOffset(at: Date, timeZone: string) {\n  const p = zoneParts(at, timeZone);\n  const asUTC = Date.UTC(p.year, p.month - 1, p.day, p.hour % 24, p.minute, p.second);\n  return Math.round((asUTC - Math.floor(at.getTime() / 1000) * 1000) / 60000);\n}\n\n/**\n * The instant a wall-clock time happens on a day in a zone: `atZonedTime(day, 9 * 60 + 30, \"Europe/London\")`\n * is 9:30 in London that day. Re-checks the offset at the result, so days that change clocks land right.\n */\nexport function atZonedTime(day: Date, minutes: number, timeZone: string) {\n  const guess = Date.UTC(day.getFullYear(), day.getMonth(), day.getDate(), 0, minutes);\n  const first = zoneOffset(new Date(guess), timeZone);\n  const second = zoneOffset(new Date(guess - first * 60000), timeZone);\n  return new Date(guess - second * 60000);\n}\n\n// Old names some engines still hand back, shown by their current ones.\nconst RENAMED: Record<string, string> = { Calcutta: \"Kolkata\", Saigon: \"Ho Chi Minh City\", Katmandu: \"Kathmandu\", Kiev: \"Kyiv\", Rangoon: \"Yangon\", Godthab: \"Nuuk\" };\nfunction zoneCity(timeZone: string) {\n  if (timeZone === \"UTC\" || timeZone === \"Etc/UTC\") return \"UTC\";\n  const last = (timeZone.split(\"/\").pop() ?? timeZone).replace(/_/g, \" \");\n  return RENAMED[last.replace(/ /g, \"\")] ?? last;\n}\nfunction zoneOffsetLabel(timeZone: string, at: Date) {\n  try {\n    const part = new Intl.DateTimeFormat(\"en-US\", { timeZone, timeZoneName: \"shortOffset\" }).formatToParts(at).find((p) => p.type === \"timeZoneName\");\n    return part?.value ?? \"\";\n  } catch {\n    return \"\";\n  }\n}\n\n/* ------------------------------------------------------------------ */\n/* Browser-only facts: today, the viewer's zone and language. Each     */\n/* reads null on the server and settles on hydration without mismatch. */\n/* ------------------------------------------------------------------ */\n\nfunction subscribeToday(notify: () => void) {\n  let timer = 0;\n  const arm = () => {\n    const now = new Date();\n    const midnight = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);\n    timer = window.setTimeout(() => (notify(), arm()), midnight.getTime() - now.getTime() + 50);\n  };\n  arm();\n  document.addEventListener(\"visibilitychange\", notify);\n  return () => {\n    window.clearTimeout(timer);\n    document.removeEventListener(\"visibilitychange\", notify);\n  };\n}\nconst noop = () => () => {};\nfunction useToday() {\n  const key = useSyncExternalStore(subscribeToday, () => dayKey(new Date()), () => null);\n  return useMemo(() => (key ? fromDayKey(key) : null), [key]);\n}\nfunction useViewerZone() {\n  return useSyncExternalStore(noop, () => Intl.DateTimeFormat().resolvedOptions().timeZone, () => null);\n}\nfunction useLocale(locale?: string) {\n  const browser = useSyncExternalStore(noop, () => navigator.language, () => \"en-US\");\n  return locale ?? browser;\n}\n\nfunction weekStartOf(locale: string): number {\n  try {\n    const l = new Intl.Locale(locale) as Intl.Locale & { getWeekInfo?: () => { firstDay: number }; weekInfo?: { firstDay: number } };\n    const info = l.getWeekInfo?.() ?? l.weekInfo;\n    if (info) return info.firstDay % 7;\n    return [\"US\", \"CA\", \"JP\", \"BR\", \"MX\", \"IN\", \"IL\", \"KR\", \"PH\", \"AU\"].includes(l.maximize().region ?? \"\") ? 0 : 1;\n  } catch {\n    return 1;\n  }\n}\nfunction prefers12h(locale: string) {\n  try {\n    const cycle = new Intl.DateTimeFormat(locale, { hour: \"numeric\" }).resolvedOptions().hourCycle;\n    return cycle === \"h11\" || cycle === \"h12\";\n  } catch {\n    return false;\n  }\n}\n\n/* ------------------------------------------------------------------ */\n\nexport type BookingSlot = {\n  start: Date;\n  /** Taken or blocked slots are skipped: they're never drawn and never reached with the keyboard. */\n  available?: boolean;\n};\n\ntype SlotsResult = (Date | BookingSlot)[];\n\nexport type BookingSlotsProps = Omit<React.ComponentProps<\"div\">, \"defaultValue\" | \"onChange\" | \"children\"> & {\n  /** Times for a day. Sync or async; a rejected promise shows a retry. The day is local midnight for that date. */\n  getSlots: (day: Date) => SlotsResult | Promise<SlotsResult>;\n  /** Days that can be opened at all. Others are drawn faint and skipped by \"next available\". */\n  isDateAvailable?: (day: Date) => boolean;\n  /** The open day, when controlled. */\n  date?: Date | null;\n  defaultDate?: Date | null;\n  onDateChange?: (day: Date) => void;\n  /** The picked (not yet confirmed) slot. */\n  value?: Date | null;\n  defaultValue?: Date | null;\n  onValueChange?: (slot: Date | null) => void;\n  /** Called by Confirm. Return a promise to show progress; reject (with a message, optionally) to report it. */\n  onConfirm?: (slot: Date) => void | Promise<void>;\n  /** Meeting length in minutes, for the end time and the announcements. */\n  duration?: number;\n  /** IANA zone the times are shown in. Defaults to the viewer's. */\n  timeZone?: string;\n  /** 12-hour clock. Defaults to the locale's habit; the footer toggle changes it. */\n  hour12?: boolean;\n  defaultHour12?: boolean;\n  onHour12Change?: (hour12: boolean) => void;\n  /** Earliest bookable day. Defaults to today. */\n  min?: Date;\n  max?: Date;\n  locale?: string;\n  weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;\n  /** Replaces the zone name in the footer, e.g. with a time zone picker. */\n  timeZoneControl?: React.ReactNode;\n  confirmLabel?: string;\n  disabled?: boolean;\n};\n\ntype Loaded = { key: string; status: \"ready\"; slots: Date[] } | { key: string; status: \"error\"; message: string };\ntype Slide = { dir: number; reduce: boolean };\ntype Status = { kind: \"idle\" } | { kind: \"busy\"; at: number } | { kind: \"error\"; at: number; message: string };\n\nconst gridVariants = {\n  enter: (c: Slide) => (c.reduce ? { opacity: 0 } : { opacity: 0, x: c.dir * 20 }),\n  center: (c: Slide) => ({ opacity: 1, x: 0, transition: { duration: c.reduce ? 0.15 : 0.24, ease: ease.out } }),\n  exit: (c: Slide) => (c.reduce ? { opacity: 0, transition: { duration: 0.1 } } : { opacity: 0, x: c.dir * -20, transition: { duration: 0.15, ease: ease.out } }),\n};\n\n/**\n * Loads the slots for one day. A spinner-free wait: nothing for the first 150ms, then a skeleton\n * that stays at least 300ms so it never flickers. Results are cached per day.\n */\nfunction useDaySlots(getSlots: BookingSlotsProps[\"getSlots\"], day: Date | null) {\n  const key = day ? dayKey(day) : null;\n  const [cache, setCache] = useState<Record<string, Loaded>>({});\n  const [skeletonFor, setSkeletonFor] = useState<string | null>(null);\n  const getRef = useRef(getSlots);\n  useEffect(() => {\n    getRef.current = getSlots;\n  });\n  const cached = key ? cache[key] : undefined;\n  // A failed day stays failed until Try again clears it; it doesn't hammer the server on every visit.\n  const needsLoad = !!key && !cached;\n\n  useEffect(() => {\n    if (!key || !needsLoad) return;\n    let live = true;\n    let shownAt = 0;\n    let hold = 0;\n    const show = window.setTimeout(() => {\n      shownAt = performance.now();\n      setSkeletonFor(key);\n    }, 150);\n    const settle = (entry: Loaded) => {\n      window.clearTimeout(show);\n      const wait = shownAt ? Math.max(0, 300 - (performance.now() - shownAt)) : 0;\n      hold = window.setTimeout(() => {\n        if (!live) return;\n        setCache((c) => ({ ...c, [key]: entry }));\n        setSkeletonFor((k) => (k === key ? null : k));\n      }, wait);\n    };\n    Promise.resolve()\n      .then(() => getRef.current(fromDayKey(key)))\n      .then(\n        (result) => {\n          const slots = result\n            .filter((s) => (s instanceof Date ? true : s.available !== false))\n            .map((s) => (s instanceof Date ? s : s.start))\n            .sort((a, b) => a.getTime() - b.getTime());\n          if (live) settle({ key, status: \"ready\", slots });\n        },\n        (error: unknown) => {\n          if (live) settle({ key, status: \"error\", message: error instanceof Error && error.message ? error.message : \"\" });\n        },\n      );\n    return () => {\n      live = false;\n      window.clearTimeout(show);\n      window.clearTimeout(hold);\n    };\n  }, [key, needsLoad]);\n\n  const retry = useCallback(() => {\n    if (!key) return;\n    setCache((c) => {\n      const next = { ...c };\n      delete next[key];\n      return next;\n    });\n  }, [key]);\n\n  // Days we've learned are fully booked, so the month can say so and \"next available\" can skip them.\n  const full = useMemo(() => new Set(Object.values(cache).flatMap((e) => (e.status === \"ready\" && e.slots.length === 0 ? [e.key] : []))), [cache]);\n  return { loaded: cached, skeleton: !cached && skeletonFor === key, retry, full };\n}\n\nexport function BookingSlots({\n  getSlots,\n  isDateAvailable,\n  date: dateProp,\n  defaultDate,\n  onDateChange,\n  value: valueProp,\n  defaultValue = null,\n  onValueChange,\n  onConfirm,\n  duration = 30,\n  timeZone: zoneProp,\n  hour12: hour12Prop,\n  defaultHour12,\n  onHour12Change,\n  min: minProp,\n  max,\n  locale: localeProp,\n  weekStartsOn,\n  timeZoneControl,\n  confirmLabel = \"Confirm\",\n  disabled = false,\n  className,\n  ...rest\n}: BookingSlotsProps) {\n  const reduce = !!useReducedMotion();\n  const uid = useId();\n  const today = useToday();\n  const viewerZone = useViewerZone();\n  const locale = useLocale(localeProp);\n  const timeZone = zoneProp ?? viewerZone;\n  const min = minProp ? startOfDay(minProp) : today;\n\n  const canOpen = useCallback(\n    (d: Date) => !(min && compareDays(d, min) < 0) && !(max && compareDays(d, max) > 0) && (isDateAvailable?.(d) ?? true),\n    [min, max, isDateAvailable],\n  );\n  const nextOpen = useCallback(\n    (from: Date, skipFirst = false, skip?: Set<string>) => {\n      for (let i = skipFirst ? 1 : 0; i < 120; i++) {\n        const d = addDays(from, i);\n        if (max && compareDays(d, max) > 0) return null;\n        if (canOpen(d) && !skip?.has(dayKey(d))) return d;\n      }\n      return null;\n    },\n    [canOpen, max],\n  );\n\n  // With no date given, open the first bookable day once today is known.\n  const autoDate = useMemo(() => (today && min ? nextOpen(min) : null), [today, min, nextOpen]);\n  const [dateInner, setDateInner] = useState<Date | null>(defaultDate ? startOfDay(defaultDate) : null);\n  const date = dateProp !== undefined ? dateProp && startOfDay(dateProp) : (dateInner ?? autoDate);\n\n  const [value, setValue] = useControllableState<Date | null>({ value: valueProp, defaultValue, onChange: onValueChange });\n  const [hour12Inner, setHour12Inner] = useState<boolean | undefined>(defaultHour12);\n  const hour12 = hour12Prop ?? hour12Inner ?? prefers12h(locale);\n  const setHour12 = (next: boolean) => {\n    if (hour12Prop === undefined) setHour12Inner(next);\n    onHour12Change?.(next);\n  };\n\n  const [monthInner, setMonthInner] = useState<Date | null>(null);\n  const month = monthInner ?? (date ? startOfMonth(date) : today ? startOfMonth(today) : null);\n  // 1 forward, -1 back, 0 for keyboard moves that should land on the same frame.\n  const [dir, setDir] = useState(1);\n  const slide = useMemo<Slide>(() => ({ dir, reduce }), [dir, reduce]);\n  const [focusDay, setFocusDay] = useState<Date | null>(null);\n  const [keyboard, setKeyboard] = useState(false);\n  const [status, setStatus] = useState<Status>({ kind: \"idle\" });\n  const [booked, setBooked] = useState<Date | null>(null);\n  const gridRef = useRef<HTMLDivElement>(null);\n\n  const { loaded, skeleton, retry, full } = useDaySlots(getSlots, date);\n\n  const fmt = useMemo(() => {\n    const tz = timeZone ?? undefined;\n    return {\n      caption: new Intl.DateTimeFormat(locale, { month: \"long\", year: \"numeric\" }),\n      weekShort: new Intl.DateTimeFormat(locale, { weekday: \"short\" }),\n      weekLong: new Intl.DateTimeFormat(locale, { weekday: \"long\" }),\n      dayLong: new Intl.DateTimeFormat(locale, { weekday: \"long\", day: \"numeric\", month: \"long\" }),\n      dayShort: new Intl.DateTimeFormat(locale, { weekday: \"short\", day: \"numeric\", month: \"short\" }),\n      heading: new Intl.DateTimeFormat(locale, { weekday: \"long\", day: \"numeric\", month: \"long\" }),\n      num: new Intl.NumberFormat(locale),\n      time: new Intl.DateTimeFormat(locale, { hour: \"numeric\", minute: \"2-digit\", hour12, timeZone: tz }),\n    };\n  }, [locale, hour12, timeZone]);\n  const weekStart = weekStartsOn ?? weekStartOf(locale);\n\n  const weekdays = useMemo(\n    () =>\n      Array.from({ length: 7 }, (_, i) => {\n        // Jan 4 2026 is a Sunday; walk from the locale's first day.\n        const d = new Date(2026, 0, 4 + ((weekStart + i) % 7));\n        return { short: fmt.weekShort.format(d), long: fmt.weekLong.format(d) };\n      }),\n    [weekStart, fmt],\n  );\n\n  const rows = useMemo(() => {\n    if (!month) return null;\n    const lead = (month.getDay() - weekStart + 7) % 7;\n    const first = addDays(month, -lead);\n    // Always six weeks, so the card never changes height between months.\n    return Array.from({ length: 6 }, (_, r) => Array.from({ length: 7 }, (_, c) => addDays(first, r * 7 + c)));\n  }, [month, weekStart]);\n\n  const selectDate = (d: Date) => {\n    if (disabled || !canOpen(d)) return;\n    setFocusDay(d);\n    if (sameDay(d, date)) return;\n    if (dateProp === undefined) setDateInner(d);\n    onDateChange?.(d);\n    setValue(null);\n    setStatus({ kind: \"idle\" });\n    if (month && monthIndex(d) !== monthIndex(month)) showMonth(startOfMonth(d));\n  };\n\n  const showMonth = (m: Date) => {\n    if (!month) return;\n    setDir(monthIndex(m) > monthIndex(month) ? 1 : -1);\n    setMonthInner(m);\n  };\n\n  const canPrev = !!month && !(min && monthIndex(month) <= monthIndex(min));\n  const canNext = !!month && !(max && monthIndex(month) >= monthIndex(max));\n\n  // The day that takes Tab in the grid: the focused one if it's in view, else the open day, else the first.\n  const tabDay = useMemo(() => {\n    if (!month || !rows) return null;\n    const inView = (d: Date | null) => !!d && monthIndex(d) === monthIndex(month);\n    if (inView(focusDay)) return focusDay;\n    if (inView(date)) return date;\n    return month;\n  }, [month, rows, focusDay, date]);\n\n  const onGridKeyDown = (e: React.KeyboardEvent) => {\n    if (!tabDay || !month) return;\n    const map: Record<string, () => Date> = {\n      ArrowLeft: () => addDays(tabDay, -1),\n      ArrowRight: () => addDays(tabDay, 1),\n      ArrowUp: () => addDays(tabDay, -7),\n      ArrowDown: () => addDays(tabDay, 7),\n      Home: () => addDays(tabDay, -((tabDay.getDay() - weekStart + 7) % 7)),\n      End: () => addDays(tabDay, 6 - ((tabDay.getDay() - weekStart + 7) % 7)),\n      PageUp: () => new Date(tabDay.getFullYear(), tabDay.getMonth() - 1, Math.min(tabDay.getDate(), 28)),\n      PageDown: () => new Date(tabDay.getFullYear(), tabDay.getMonth() + 1, Math.min(tabDay.getDate(), 28)),\n    };\n    const go = map[e.key];\n    if (!go) return;\n    e.preventDefault();\n    let next = go();\n    if (min && compareDays(next, min) < 0) next = min;\n    if (max && compareDays(next, max) > 0) next = startOfDay(max);\n    setKeyboard(true);\n    setFocusDay(next);\n    if (monthIndex(next) !== monthIndex(month)) {\n      // Arrow keys render the new month on the same frame: motion here reads as lag.\n      setDir(0);\n      setMonthInner(startOfMonth(next));\n    }\n    requestAnimationFrame(() => gridRef.current?.querySelector<HTMLElement>(`[data-day=\"${dayKey(next)}\"]`)?.focus());\n  };\n\n  /* Slots ------------------------------------------------------------ */\n\n  const slots = loaded?.status === \"ready\" ? loaded.slots : null;\n  const selectedKey = value ? value.getTime() : null;\n  const [focusSlot, setFocusSlot] = useState<number | null>(null);\n  const tabSlot = slots?.some((s) => s.getTime() === focusSlot) ? focusSlot : slots?.some((s) => s.getTime() === selectedKey) ? selectedKey : (slots?.[0]?.getTime() ?? null);\n\n  const pick = (slot: Date) => {\n    if (disabled || status.kind === \"busy\") return;\n    setStatus({ kind: \"idle\" });\n    setValue(value && value.getTime() === slot.getTime() ? null : slot);\n  };\n\n  const confirm = async (slot: Date) => {\n    if (status.kind === \"busy\") return;\n    const at = slot.getTime();\n    if (!onConfirm) {\n      setBooked(slot);\n      return;\n    }\n    setStatus({ kind: \"busy\", at });\n    try {\n      await onConfirm(slot);\n      setStatus({ kind: \"idle\" });\n      setBooked(slot);\n    } catch (error) {\n      setStatus({ kind: \"error\", at, message: error instanceof Error && error.message ? error.message : \"\" });\n    }\n  };\n\n  const onListKeyDown = (e: React.KeyboardEvent<HTMLUListElement>) => {\n    const buttons = Array.from(e.currentTarget.querySelectorAll<HTMLButtonElement>(\"[data-slot]\"));\n    const at = buttons.indexOf(document.activeElement as HTMLButtonElement);\n    if (e.key === \"Escape\" && value) {\n      e.preventDefault();\n      const el = buttons.find((b) => Number(b.dataset.slot) === value.getTime());\n      setValue(null);\n      setStatus({ kind: \"idle\" });\n      el?.focus();\n      return;\n    }\n    if (at < 0) return;\n    const cols = Math.max(1, Math.round(e.currentTarget.clientWidth / buttons[0].closest(\"li\")!.clientWidth));\n    const target =\n      e.key === \"ArrowDown\" ? at + cols : e.key === \"ArrowUp\" ? at - cols : e.key === \"ArrowRight\" && cols > 1 ? at + 1 : e.key === \"ArrowLeft\" && cols > 1 ? at - 1 : e.key === \"Home\" ? 0 : e.key === \"End\" ? buttons.length - 1 : null;\n    if (target === null) return;\n    e.preventDefault();\n    buttons[Math.max(0, Math.min(buttons.length - 1, target))]?.focus();\n  };\n\n  const zoneLabel = timeZone ? zoneCity(timeZone) : null;\n  const offsetLabel = timeZone && today ? zoneOffsetLabel(timeZone, date ?? today) : null;\n  const next = date && slots && slots.length === 0 ? nextOpen(date, true, full) : null;\n  const heading = date ? fmt.heading.format(date) : null;\n  // \"11:00 – 11:30 AM\": formatRange drops the repeated day period where the locale does.\n  const timeRange = (slot: Date) => fmt.time.formatRange(slot, new Date(slot.getTime() + duration * 60000));\n\n  const announce = booked\n    ? `Booked ${fmt.dayLong.format(date ?? booked)}, ${timeRange(booked)}`\n    : status.kind === \"error\"\n      ? `Couldn't book ${fmt.time.format(new Date(status.at))}`\n      : slots && heading\n        ? slots.length\n          ? `${slots.length} ${slots.length === 1 ? \"time\" : \"times\"} available on ${heading}`\n          : `No times left on ${heading}`\n        : loaded?.status === \"error\"\n          ? \"Couldn't load times\"\n          : \"\";\n\n  return (\n    <div\n      data-disabled={disabled || undefined}\n      data-state={booked ? \"booked\" : value ? \"picked\" : \"picking\"}\n      className={cn(\n        \"@container flex w-full max-w-[520px] select-none flex-col overflow-hidden rounded-xl border border-line bg-raised text-fg shadow-[var(--shadow)]\",\n        disabled && \"pointer-events-none opacity-50\",\n        className,\n      )}\n      onPointerDown={() => setKeyboard(false)}\n      {...rest}\n    >\n      <div className=\"flex flex-col @min-[500px]:flex-row\">\n        {/* Month ---------------------------------------------------- */}\n        <div\n          inert={!!booked}\n          className={cn(\n            \"flex shrink-0 justify-center p-3 transition-opacity duration-200 @min-[500px]:border-r @min-[500px]:border-line\",\n            booked && \"opacity-40\",\n          )}\n        >\n          <div className=\"flex flex-col gap-2\">\n            <div className=\"flex h-8 items-center gap-1\">\n              <div className=\"relative h-5 min-w-0 flex-1 overflow-hidden pl-1.5\">\n                <AnimatePresence initial={false} custom={slide} mode=\"popLayout\">\n                  {month && (\n                    <motion.div\n                      key={monthIndex(month)}\n                      id={`${uid}-caption`}\n                      custom={slide}\n                      initial={slide.dir === 0 ? false : { opacity: 0, x: reduce ? 0 : slide.dir * 8 }}\n                      animate={{ opacity: 1, x: 0, transition: { duration: 0.22, ease: ease.out } }}\n                      exit={slide.dir === 0 ? { opacity: 0, transition: { duration: 0 } } : { opacity: 0, x: reduce ? 0 : slide.dir * -8, transition: { duration: 0.12 } }}\n                      aria-live=\"polite\"\n                      className=\"truncate text-[13px] font-medium leading-5 tracking-[-0.01em]\"\n                    >\n                      {fmt.caption.format(month)}\n                    </motion.div>\n                  )}\n                </AnimatePresence>\n              </div>\n              <NavButton side=\"prev\" disabled={!canPrev} onClick={() => month && showMonth(addMonths(month, -1))} />\n              <NavButton side=\"next\" disabled={!canNext} onClick={() => month && showMonth(addMonths(month, 1))} />\n            </div>\n\n            <div>\n              <div aria-hidden className=\"flex\">\n                {weekdays.map((d) => (\n                  <span key={d.long} className=\"grid h-7 w-9 place-items-center text-[11px] text-fg-3 pointer-coarse:w-10\">\n                    {d.short}\n                  </span>\n                ))}\n              </div>\n              <div ref={gridRef} className=\"relative -m-1 overflow-hidden p-1\">\n                {month && rows ? (\n                  <AnimatePresence initial={false} custom={slide} mode=\"popLayout\">\n                    <MonthGrid key={monthIndex(month)} slide={slide} captionId={`${uid}-caption`} weekdays={weekdays} onKeyDown={onGridKeyDown}>\n                      {rows.map((row, r) => (\n                        <tr key={r}>\n                          {row.map((d) => {\n                            const outside = monthIndex(d) !== monthIndex(month);\n                            if (outside) return <td key={dayKey(d)} role=\"presentation\" className=\"size-9 p-0 pointer-coarse:size-10\" />;\n                            const open = canOpen(d);\n                            const isFull = open && full.has(dayKey(d));\n                            const selected = sameDay(d, date);\n                            const isToday = sameDay(d, today);\n                            return (\n                              <td key={dayKey(d)} role=\"gridcell\" aria-selected={selected} className=\"relative size-9 p-0 pointer-coarse:size-10\">\n                                <button\n                                  type=\"button\"\n                                  data-day={dayKey(d)}\n                                  data-open={open || undefined}\n                                  tabIndex={sameDay(d, tabDay) ? 0 : -1}\n                                  aria-label={`${fmt.dayLong.format(d)}${!open ? \", unavailable\" : isFull ? \", fully booked\" : \"\"}`}\n                                  aria-current={isToday ? \"date\" : undefined}\n                                  aria-disabled={!open || undefined}\n                                  onClick={() => selectDate(d)}\n                                  onFocus={() => setFocusDay(d)}\n                                  className={cn(\n                                    \"group/day relative grid size-9 place-items-center rounded-lg text-[13px] tabular outline-none pointer-coarse:size-10\",\n                                    \"transition-[background-color,color,scale] duration-150 focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-1 focus-visible:outline-fg-3\",\n                                    !open\n                                      ? \"cursor-default text-fg-4\"\n                                      : selected\n                                        ? \"font-medium text-frame\"\n                                        : isFull\n                                          ? \"text-fg-3 line-through decoration-fg-4 hover:bg-hover active:scale-[0.92] active:duration-75\"\n                                          : \"bg-fg/[0.06] font-medium text-fg hover:bg-fg/[0.11] active:scale-[0.92] active:duration-75\",\n                                  )}\n                                >\n                                  {selected && (\n                                    <motion.span\n                                      layoutId=\"day\"\n                                      aria-hidden\n                                      className=\"absolute inset-0 rounded-lg bg-fg\"\n                                      transition={reduce || keyboard ? { duration: 0 } : spring.snappy}\n                                    />\n                                  )}\n                                  <span className=\"relative\">{fmt.num.format(d.getDate())}</span>\n                                  {isToday && <span aria-hidden className=\"absolute bottom-1 left-1/2 size-1 -translate-x-1/2 rounded-full bg-current opacity-70\" />}\n                                </button>\n                              </td>\n                            );\n                          })}\n                        </tr>\n                      ))}\n                    </MonthGrid>\n                  </AnimatePresence>\n                ) : (\n                  <div aria-hidden className=\"h-[228px] w-[252px] pointer-coarse:h-[252px] pointer-coarse:w-[280px]\" />\n                )}\n              </div>\n          </div>\n          </div>\n        </div>\n\n        {/* Times ---------------------------------------------------- */}\n        <div className=\"relative min-h-0 min-w-0 flex-1 border-t border-line @min-[500px]:border-t-0\">\n          <div className=\"flex flex-col @min-[500px]:absolute @min-[500px]:inset-0\">\n            <div className=\"flex h-14 shrink-0 flex-col justify-center px-3\">\n              <p className=\"truncate text-[13px] font-medium leading-5 tracking-[-0.01em]\" suppressHydrationWarning>\n                {heading ?? \" \"}\n              </p>\n              <p className=\"text-[12px] leading-4 text-fg-3 tabular\">\n                {fmt.num.format(duration)} min\n                {slots && slots.length > 0 && !booked && (\n                  <span className=\"text-fg-4\">\n                    {\" · \"}\n                    {slots.length} open\n                  </span>\n                )}\n              </p>\n            </div>\n\n            <div className=\"relative min-h-0 flex-1\">\n              <AnimatePresence initial={false} mode=\"popLayout\">\n                {booked ? (\n                  <Booked\n                    key=\"booked\"\n                    reduce={reduce}\n                    range={timeRange(booked)}\n                    zone={zoneLabel}\n                    onChange={() => {\n                      setBooked(null);\n                      setValue(null);\n                    }}\n                  />\n                ) : slots ? (\n                  slots.length ? (\n                    <motion.ul\n                      key={`list-${loaded?.key}`}\n                      role=\"list\"\n                      aria-label={heading ? `Available times, ${heading}` : \"Available times\"}\n                      onKeyDown={onListKeyDown}\n                      exit={{ opacity: 0, transition: { duration: 0.1 } }}\n                      className={cn(\n                        \"grid max-h-[232px] grid-cols-[repeat(auto-fill,minmax(148px,1fr))] content-start gap-1.5 overflow-y-auto overscroll-contain px-3 pt-1 pb-6 @min-[500px]:absolute @min-[500px]:inset-0 @min-[500px]:max-h-none\",\n                        \"[mask-image:linear-gradient(to_bottom,var(--fg)_calc(100%-28px),transparent)]\",\n                      )}\n                    >\n                      {slots.map((slot, i) => {\n                        const at = slot.getTime();\n                        const selected = at === selectedKey;\n                        const busy = status.kind === \"busy\" && status.at === at;\n                        const failed = status.kind === \"error\" && status.at === at;\n                        const label = fmt.time.format(slot);\n                        return (\n                          <motion.li\n                            key={at}\n                            initial={reduce ? { opacity: 0 } : { opacity: 0, y: 6, filter: \"blur(2px)\" }}\n                            animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n                            transition={{ duration: reduce ? 0.15 : 0.26, ease: ease.out, delay: Math.min(i, 8) * 0.02 }}\n                            className=\"flex flex-col\"\n                          >\n                            <SlotRow\n                              at={at}\n                              label={label}\n                              selected={selected}\n                              busy={busy}\n                              failed={failed}\n                              tabbable={at === tabSlot}\n                              confirmLabel={failed ? \"Try again\" : confirmLabel}\n                              confirmName={`${failed ? \"Try again\" : confirmLabel}: ${heading}, ${timeRange(slot)}`}\n                              slotName={`${label}, ${heading}`}\n                              onPick={() => pick(slot)}\n                              onConfirm={() => confirm(slot)}\n                              onFocus={() => setFocusSlot(at)}\n                            />\n                            <AnimatePresence initial={false}>\n                              {failed && (\n                                <motion.p\n                                  role=\"alert\"\n                                  initial={{ opacity: 0, height: 0 }}\n                                  animate={{ opacity: 1, height: \"auto\" }}\n                                  exit={{ opacity: 0, height: 0, transition: { duration: 0.12 } }}\n                                  transition={{ duration: 0.2, ease: ease.out }}\n                                  className=\"overflow-hidden text-[12px] leading-4 text-danger\"\n                                >\n                                  <span className=\"block pt-1.5 pb-0.5\">{status.kind === \"error\" && status.message ? status.message : \"Couldn’t book this time. Pick another or try again.\"}</span>\n                                </motion.p>\n                              )}\n                            </AnimatePresence>\n                          </motion.li>\n                        );\n                      })}\n                    </motion.ul>\n                  ) : (\n                    <Message key={`empty-${loaded?.key}`} reduce={reduce} title=\"No times left on this day\">\n                      {next ? (\n                        <button type=\"button\" onClick={() => selectDate(next)} className={cn(secondaryButton, \"gap-1.5\")}>\n                          Next available, {fmt.dayShort.format(next)}\n                          <ArrowRight size={14} className=\"text-fg-3 transition-transform duration-200 ease-out-expo group-hover/btn:translate-x-0.5\" />\n                        </button>\n                      ) : (\n                        <p className=\"text-[12px] text-fg-3\">Nothing else is open yet.</p>\n                      )}\n                    </Message>\n                  )\n                ) : loaded?.status === \"error\" ? (\n                  <Message key=\"error\" reduce={reduce} title=\"Couldn’t load times\" tone=\"danger\" detail={loaded.message || \"Check your connection and try again.\"}>\n                    <button type=\"button\" onClick={retry} className={cn(secondaryButton, \"gap-1.5\")}>\n                      <Refresh size={14} className=\"text-fg-3 transition-transform duration-300 ease-out-expo group-active/btn:-rotate-45\" />\n                      Try again\n                    </button>\n                  </Message>\n                ) : skeleton ? (\n                  <motion.div\n                    key=\"skeleton\"\n                    aria-hidden\n                    initial={{ opacity: 0 }}\n                    animate={{ opacity: 1 }}\n                    exit={{ opacity: 0, transition: { duration: 0.1 } }}\n                    className=\"grid grid-cols-[repeat(auto-fill,minmax(148px,1fr))] content-start gap-1.5 px-3 pt-1\"\n                  >\n                    {Array.from({ length: 5 }, (_, i) => (\n                      <span key={i} className=\"h-9 animate-pulse-soft rounded-lg bg-hover\" style={{ animationDelay: `${i * 120}ms` }} />\n                    ))}\n                  </motion.div>\n                ) : null}\n              </AnimatePresence>\n            </div>\n          </div>\n        </div>\n      </div>\n\n      {/* Zone ------------------------------------------------------- */}\n      <div className=\"flex min-h-10 items-center gap-2 border-t border-line py-1 pr-1.5 pl-3\">\n        <Globe size={14} className=\"shrink-0 text-fg-3\" />\n        <div className=\"flex min-w-0 flex-1 items-center gap-1.5 text-[12px]\">\n          {timeZoneControl ?? (\n            <>\n              <span className=\"truncate text-fg-2\">\n                {zoneLabel ? (\n                  <>\n                    Times in <span className=\"text-fg\">{zoneLabel}</span>\n                  </>\n                ) : (\n                  <span className=\"inline-block h-3 w-28 rounded bg-hover align-middle\" />\n                )}\n              </span>\n              {offsetLabel && <span className=\"shrink-0 font-mono text-2xs text-fg-4\">{offsetLabel}</span>}\n            </>\n          )}\n        </div>\n        <HourToggle hour12={hour12} onChange={setHour12} reduce={reduce} />\n      </div>\n\n      <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n        {announce}\n      </span>\n    </div>\n  );\n}\n\n/* ------------------------------------------------------------------ */\n\nconst secondaryButton = cn(\n  \"group/btn inline-flex h-8 items-center rounded-lg border border-line-2 bg-raised px-2.5 text-[12.5px] font-medium text-fg shadow-[var(--shadow)]\",\n  \"outline-none focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-fg-3\",\n  \"transition-[background-color,border-color,scale] duration-150 hover:border-fg-4 hover:bg-hover active:scale-[0.97] active:duration-75\",\n);\n\nfunction MonthGrid({\n  slide,\n  captionId,\n  weekdays,\n  onKeyDown,\n  children,\n}: {\n  slide: Slide;\n  captionId: string;\n  weekdays: { short: string; long: string }[];\n  onKeyDown: (e: React.KeyboardEvent) => void;\n  children: React.ReactNode;\n}) {\n  // A grid on its way out lingers for its exit; it must not take focus or clicks.\n  const present = useIsPresent();\n  const instant = slide.dir === 0;\n  return (\n    <LayoutGroup id={captionId + String(present)}>\n      <motion.table\n        role=\"grid\"\n        aria-labelledby={captionId}\n        inert={!present}\n        custom={slide}\n        variants={gridVariants}\n        initial={instant ? false : \"enter\"}\n        animate=\"center\"\n        exit={instant ? { opacity: 0, transition: { duration: 0 } } : \"exit\"}\n        onKeyDown={onKeyDown}\n        className=\"border-separate border-spacing-0\"\n      >\n        <thead className=\"sr-only\">\n          <tr>\n            {weekdays.map((d) => (\n              <th key={d.long} scope=\"col\" abbr={d.long}>\n                {d.short}\n              </th>\n            ))}\n          </tr>\n        </thead>\n        <tbody>{children}</tbody>\n      </motion.table>\n    </LayoutGroup>\n  );\n}\n\nfunction NavButton({ side, disabled, onClick }: { side: \"prev\" | \"next\"; disabled: boolean; onClick: () => void }) {\n  const Icon = side === \"prev\" ? ChevronLeft : ChevronRight;\n  return (\n    <button\n      type=\"button\"\n      onClick={onClick}\n      disabled={disabled}\n      aria-label={side === \"prev\" ? \"Previous month\" : \"Next month\"}\n      className={cn(\n        \"group/nav grid size-7 shrink-0 place-items-center rounded-md text-fg-2 outline-none pointer-coarse:size-10\",\n        \"transition-[background-color,color,scale] duration-150 hover:bg-hover hover:text-fg active:scale-[0.92] active:duration-75\",\n        \"focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-fg-3\",\n        \"disabled:pointer-events-none disabled:opacity-35\",\n      )}\n    >\n      <Icon\n        className={cn(\n          \"transition-transform duration-200 ease-out-expo\",\n          side === \"prev\" ? \"group-hover/nav:-translate-x-px group-active/nav:-translate-x-0.5\" : \"group-hover/nav:translate-x-px group-active/nav:translate-x-0.5\",\n        )}\n      />\n    </button>\n  );\n}\n\n/**\n * One slot. Picking it splits the row in place: the time slides into the left half and Confirm\n * opens in the right, by transitioning the grid's second track from 0fr to 1fr. Real widths, so\n * nothing is stretched mid-flight and nothing around the row moves.\n */\nfunction SlotRow({\n  at,\n  label,\n  selected,\n  busy,\n  failed,\n  tabbable,\n  confirmLabel,\n  confirmName,\n  slotName,\n  onPick,\n  onConfirm,\n  onFocus,\n}: {\n  at: number;\n  label: string;\n  selected: boolean;\n  busy: boolean;\n  failed: boolean;\n  tabbable: boolean;\n  confirmLabel: string;\n  confirmName: string;\n  slotName: string;\n  onPick: () => void;\n  onConfirm: () => void;\n  onFocus: () => void;\n}) {\n  const confirmRef = useRef<HTMLButtonElement>(null);\n  return (\n    <div\n      data-selected={selected || undefined}\n      className={cn(\n        \"grid grid-cols-[minmax(0,1fr)_minmax(0,0fr)] transition-[grid-template-columns] duration-200 ease-out-quart\",\n        \"data-selected:grid-cols-[minmax(0,1fr)_minmax(0,1fr)] data-selected:duration-[320ms] data-selected:ease-out-expo\",\n      )}\n    >\n      <button\n        type=\"button\"\n        data-slot={at}\n        aria-pressed={selected}\n        aria-label={slotName}\n        tabIndex={tabbable ? 0 : -1}\n        onFocus={onFocus}\n        onClick={onPick}\n        onKeyDown={(e) => {\n          // Enter on a picked slot moves straight to Confirm, so booking is two presses.\n          if (selected && e.key === \"Enter\") {\n            e.preventDefault();\n            confirmRef.current?.focus();\n          }\n        }}\n        disabled={busy}\n        className={cn(\n          \"relative h-9 min-w-0 rounded-lg border text-[13px] font-medium tabular outline-none pointer-coarse:h-10\",\n          \"transition-[background-color,border-color,color,scale] duration-150 active:scale-[0.97] active:duration-75\",\n          \"focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-fg-3\",\n          selected ? \"border-transparent bg-fg/[0.08] text-fg-2\" : \"border-line-2 bg-raised text-fg shadow-[var(--shadow)] hover:border-fg-4 hover:bg-hover\",\n          \"disabled:active:scale-100\",\n        )}\n      >\n        <span className=\"block truncate px-2\">{label}</span>\n      </button>\n      <div className=\"min-w-0 overflow-hidden\">\n        <div className=\"pl-1.5\">\n          <button\n            ref={confirmRef}\n            type=\"button\"\n            inert={!selected}\n            aria-hidden={!selected || undefined}\n            aria-label={confirmName}\n            aria-busy={busy || undefined}\n            tabIndex={selected ? 0 : -1}\n            onClick={onConfirm}\n            data-busy={busy || undefined}\n            className={cn(\n              \"group/confirm relative grid h-9 w-full min-w-0 place-items-center rounded-lg text-[13px] font-medium outline-none pointer-coarse:h-10\",\n              \"transition-[background-color,opacity,translate,scale] duration-200 ease-out-expo active:scale-[0.97] active:duration-75\",\n              // The half it lives in clips, so its focus ring is drawn inside the fill instead of around it.\n              \"focus-visible:outline-solid focus-visible:outline-1 focus-visible:-outline-offset-4 focus-visible:outline-frame/80\",\n              \"bg-fg text-frame hover:bg-fg/90\",\n              // It arrives a beat after the split starts, from the seam, and leaves at once.\n              selected ? \"translate-x-0 opacity-100 delay-75\" : \"-translate-x-2 opacity-0 duration-100\",\n              \"data-busy:pointer-events-none\",\n            )}\n          >\n            <span className=\"col-start-1 row-start-1 truncate px-2 transition-opacity duration-150 group-data-busy/confirm:opacity-0 group-data-busy/confirm:delay-150\">{confirmLabel}</span>\n            <span className=\"col-start-1 row-start-1 opacity-0 transition-opacity duration-150 group-data-busy/confirm:opacity-100 group-data-busy/confirm:delay-150\">\n              <Loader size={14} className=\"animate-spin\" />\n            </span>\n          </button>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction Message({ title, detail, tone, reduce, children }: { title: string; detail?: string; tone?: \"danger\"; reduce: boolean; children?: React.ReactNode }) {\n  return (\n    <motion.div\n      initial={reduce ? { opacity: 0 } : { opacity: 0, y: 4 }}\n      animate={{ opacity: 1, y: 0 }}\n      exit={{ opacity: 0, transition: { duration: 0.1 } }}\n      transition={{ duration: 0.22, ease: ease.out }}\n      className=\"flex flex-col items-start gap-3 px-3 pt-1 pb-4\"\n    >\n      <div className=\"flex flex-col gap-0.5\">\n        <p className={cn(\"text-[13px] leading-5\", tone === \"danger\" ? \"text-danger\" : \"text-fg-2\")}>{title}</p>\n        {detail && <p className=\"text-[12px] leading-4 text-fg-3\">{detail}</p>}\n      </div>\n      {children}\n    </motion.div>\n  );\n}\n\nfunction Booked({ range, zone, reduce, onChange }: { range: string; zone: string | null; reduce: boolean; onChange: () => void }) {\n  const draw = reduce ? {} : { initial: { pathLength: 0 }, animate: { pathLength: 1 } };\n  return (\n    <motion.div\n      initial={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.97, filter: \"blur(2px)\" }}\n      animate={{ opacity: 1, scale: 1, filter: \"blur(0px)\" }}\n      exit={{ opacity: 0, transition: { duration: 0.1 } }}\n      transition={{ duration: 0.26, ease: ease.out }}\n      className=\"flex flex-col items-start gap-3 px-3 pt-1 pb-4\"\n    >\n      <span className=\"grid size-8 place-items-center rounded-full bg-success-soft text-success\">\n        <svg width={16} height={16} viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth={1.6} strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n          <motion.path d=\"M3.5 8.5 6.5 11.5 12.5 4.5\" {...draw} transition={{ duration: 0.34, ease: ease.out, delay: 0.12 }} />\n        </svg>\n      </span>\n      <div className=\"flex min-w-0 flex-col gap-0.5\">\n        <p className=\"text-[13px] font-medium leading-5\">Booked</p>\n        <p className=\"text-[12.5px] leading-[18px] text-fg-2 tabular\">\n          {range}\n          {zone && <span className=\"text-fg-3\"> · {zone}</span>}\n        </p>\n      </div>\n      <button type=\"button\" onClick={onChange} className={secondaryButton}>\n        Change time\n      </button>\n    </motion.div>\n  );\n}\n\n/** 12h / 24h, with one pill that slides between the two. */\nfunction HourToggle({ hour12, onChange, reduce }: { hour12: boolean; onChange: (v: boolean) => void; reduce: boolean }) {\n  const id = useId();\n  return (\n    <ToggleGroup\n      value={[hour12 ? \"12\" : \"24\"]}\n      onValueChange={(v) => v[0] && onChange(v[0] === \"12\")}\n      aria-label=\"Clock format\"\n      className=\"relative flex h-7 shrink-0 items-center rounded-md bg-fg/[0.05] p-0.5\"\n    >\n      {([\"12\", \"24\"] as const).map((v) => {\n        const on = (v === \"12\") === hour12;\n        return (\n          <Toggle\n            key={v}\n            value={v}\n            aria-label={v === \"12\" ? \"12-hour clock\" : \"24-hour clock\"}\n            className={cn(\n              \"relative h-6 rounded-[5px] px-2 font-mono text-2xs outline-none\",\n              \"transition-[color,scale] duration-150 active:scale-[0.94] active:duration-75\",\n              \"focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-1 focus-visible:outline-fg-3\",\n              \"pointer-coarse:after:absolute pointer-coarse:after:-inset-2 pointer-coarse:after:content-['']\",\n              on ? \"text-fg\" : \"text-fg-3 hover:text-fg-2\",\n            )}\n          >\n            {on && (\n              <motion.span\n                layoutId={`${id}-pill`}\n                aria-hidden\n                className=\"absolute inset-0 rounded-[5px] border border-line-2 bg-raised shadow-[var(--shadow)]\"\n                transition={reduce ? { duration: 0 } : spring.snappy}\n              />\n            )}\n            <span className=\"relative\">{v}h</span>\n          </Toggle>\n        );\n      })}\n    </ToggleGroup>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "https://www.stealth.pm/ui/booking-slots"
}
