{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "booking-card",
  "title": "Booking Card",
  "author": "AlgorizeTech",
  "description": "A booking panel for any card: a day strip paged a week at a time, time slots in groups, a button that reads back the choice, and a confirmation that draws itself in. Use it wherever a page ends in an appointment.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "https://avaraui.com/r/glass-panel.json",
    "utils"
  ],
  "files": [
    {
      "path": "src/components/custom/blocks/BookingCardBlock.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { ChevronLeft, ChevronRight } from \"lucide-react\";\nimport React, {\n  createContext,\n  useContext,\n  useEffect,\n  useMemo,\n  useState,\n  useSyncExternalStore,\n} from \"react\";\nimport { useSurface } from \"./glass-panel-context\";\n\n// ==========================================\n// BOOKING CARD — days, slots, confirm\n// ==========================================\nexport interface BookingSlot {\n  /** As shown, e.g. \"9:00 AM\". */\n  label: string;\n  /** Already taken — shown, not selectable. */\n  taken?: boolean;\n}\n\nexport interface Booking {\n  day: Date;\n  slot: string;\n}\n\ntype BookingCardStatus = \"idle\" | \"booking\" | \"booked\";\n\ninterface BookingCardContextValue {\n  day: Date | null;\n  slot: string | null;\n  status: BookingCardStatus;\n  setDay: (d: Date) => void;\n  setSlot: (s: string) => void;\n  submit: () => Promise<void>;\n  reset: () => void;\n}\n\nconst BookingCardContext = createContext<BookingCardContextValue | null>(null);\n\nfunction useBookingCard(part: string) {\n  const ctx = useContext(BookingCardContext);\n  if (!ctx) throw new Error(`<BookingCard.${part}> must be inside <BookingCard>`);\n  return ctx;\n}\n\nexport interface BookingCardProps {\n  /**\n   * Called when the person confirms. Return a promise to hold the button in\n   * its busy state until the booking is really made.\n   */\n  onBook?: (booking: Booking) => void | Promise<void>;\n  children: React.ReactNode;\n  className?: string;\n}\n\n/**\n * Holds the chosen day and slot and runs the confirm. Put `BookingCard.Days`,\n * `BookingCard.Slots`, `BookingCard.Submit` and `BookingCard.Confirmation`\n * inside it in any layout you like.\n *\n * The card is surface-neutral: it draws no background of its own, so it can\n * sit inside any panel — a glass slab, a plain bordered card, a modal. The\n * confirmation covers exactly this element, so make the BookingCard the padded\n * panel itself (heading included) rather than a child of it — otherwise the\n * cover stops short of the panel's edges. Its corners follow the panel's.\n */\nexport function BookingCard({ onBook, children, className }: BookingCardProps) {\n  const [day, setDayState] = useState<Date | null>(null);\n  const [slot, setSlot] = useState<string | null>(null);\n  const [status, setStatus] = useState<BookingCardStatus>(\"idle\");\n\n  const setDay = (d: Date) => {\n    setDayState(d);\n    setSlot(null); // a new day means new availability\n  };\n\n  const submit = async () => {\n    if (!day || !slot || status !== \"idle\") return;\n    setStatus(\"booking\");\n    try {\n      await onBook?.({ day, slot });\n      setStatus(\"booked\");\n    } catch {\n      setStatus(\"idle\");\n    }\n  };\n\n  const reset = () => {\n    setDayState(null);\n    setSlot(null);\n    setStatus(\"idle\");\n  };\n\n  return (\n    <BookingCardContext.Provider value={{ day, slot, status, setDay, setSlot, submit, reset }}>\n      <div className={cn(\"relative rounded-[inherit]\", className)}>{children}</div>\n    </BookingCardContext.Provider>\n  );\n}\n\nconst sameDay = (a: Date, b: Date) =>\n  a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();\n\nconst WEEKDAY = new Intl.DateTimeFormat(\"en-US\", { weekday: \"short\" });\nconst MONTH = new Intl.DateTimeFormat(\"en-US\", { month: \"long\" });\nconst LONG_DATE = new Intl.DateTimeFormat(\"en-US\", { weekday: \"long\", month: \"long\", day: \"numeric\" });\n\nexport interface BookingCardDaysProps {\n  /**\n   * Days on offer, in order, paged `count` at a time. Defaults to every day\n   * from today up to `horizon` days out.\n   */\n  days?: Date[];\n  /** Days shown per page. */\n  count?: number;\n  /** How far ahead the default days run, in days from today. */\n  horizon?: number;\n  /** Days with nothing free — shown, not selectable. */\n  unavailable?: (day: Date) => boolean;\n  className?: string;\n}\n\nconst daysFromToday = (length: number) => {\n  const today = new Date();\n  today.setHours(0, 0, 0, 0);\n  return Array.from({ length }, (_, i) => {\n    const d = new Date(today);\n    d.setDate(today.getDate() + i);\n    return d;\n  });\n};\n\n/** False on the server and during hydration, true once the client has taken over. */\nconst subscribeToNothing = () => () => {};\nconst useIsMounted = () =>\n  useSyncExternalStore(\n    subscribeToNothing,\n    () => true,\n    () => false,\n  );\n\n/** \"September\", or \"September – October\" when the page straddles a month. */\nconst monthRange = (first?: Date, last?: Date) => {\n  if (!first) return \"\";\n  const a = MONTH.format(first);\n  const b = last ? MONTH.format(last) : a;\n  return a === b ? a : `${a} – ${b}`;\n};\n\nfunction BookingCardDays({ days: daysProp, count = 7, horizon = 90, unavailable, className }: BookingCardDaysProps) {\n  const { day, setDay } = useBookingCard(\"Days\");\n  const surface = useSurface();\n  const isLiquid = surface === \"liquid-glass\";\n  // Built on the client only — a server render has no idea what \"today\" is.\n  const mounted = useIsMounted();\n  const defaultDays = useMemo(\n    () => (mounted ? daysFromToday(horizon) : []),\n    [mounted, horizon],\n  );\n  const days = daysProp ?? defaultDays;\n\n  // The list starts today (or at the first day handed in), so page 0 is the\n  // earliest anyone can go — there is no way to page back into the past.\n  const [page, setPage] = useState(0);\n  const pageCount = Math.max(1, Math.ceil(days.length / count));\n  const visible = days.slice(page * count, page * count + count);\n  const atStart = page === 0;\n  const atEnd = page >= pageCount - 1;\n\n  return (\n    <div className={className}>\n      <div className=\"flex items-center justify-between gap-2\">\n        <p className=\"text-sm font-semibold\">{monthRange(visible[0], visible[visible.length - 1])}</p>\n        <div className=\"flex gap-1\">\n          {[\n            { label: \"Previous days\", Icon: ChevronLeft, disabled: atStart, step: -1 },\n            { label: \"Next days\", Icon: ChevronRight, disabled: atEnd, step: 1 },\n          ].map(({ label, Icon, disabled, step }) => (\n            <button\n              key={label}\n              type=\"button\"\n              aria-label={label}\n              disabled={disabled}\n              // Clamped, not just disabled: rapid clicks queue up before the\n              // disabled state re-renders and would otherwise run off the end.\n              onClick={() => setPage((p) => Math.min(Math.max(p + step, 0), pageCount - 1))}\n              className={cn(\n                \"grid size-7 place-items-center rounded-full transition-colors\",\n                isLiquid\n                  ? \"hover:bg-white/20 text-white disabled:hover:bg-transparent disabled:opacity-30\"\n                  : \"hover:bg-foreground/5 disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent\",\n                \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary\",\n              )}\n            >\n              <Icon className=\"size-4\" aria-hidden />\n            </button>\n          ))}\n        </div>\n      </div>\n      <div role=\"radiogroup\" aria-label=\"Day\" className=\"mt-3 grid grid-cols-7 gap-1.5\">\n        {visible.map((d) => {\n          const selected = day !== null && sameDay(d, day);\n          const off = unavailable?.(d) ?? false;\n          return (\n            <button\n              key={d.toISOString()}\n              type=\"button\"\n              role=\"radio\"\n              aria-checked={selected}\n              aria-label={LONG_DATE.format(d)}\n              disabled={off}\n              onClick={() => setDay(d)}\n              className={cn(\n                \"flex flex-col items-center gap-1 rounded-[var(--radius-xl,1rem)] py-2 text-xs transition-colors\",\n                \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary\",\n                selected\n                  ? isLiquid\n                    ? \"bg-white text-black font-semibold shadow-sm\"\n                    : \"bg-primary text-primary-foreground\"\n                  : off\n                    ? isLiquid\n                      ? \"text-white/30 line-through\"\n                      : \"text-foreground/30 line-through\"\n                    : isLiquid\n                      ? \"text-white hover:bg-white/20\"\n                      : \"hover:bg-foreground/5\",\n              )}\n            >\n              <span className={cn(\"text-[10px] uppercase\", selected ? (isLiquid ? \"opacity-90\" : \"opacity-70\") : (isLiquid ? \"text-white/70\" : \"text-foreground/50\"))}>\n                {WEEKDAY.format(d).slice(0, 3)}\n              </span>\n              <span className=\"text-sm font-semibold tabular-nums\">{d.getDate()}</span>\n            </button>\n          );\n        })}\n      </div>\n    </div>\n  );\n}\n\nexport interface BookingCardSlotsProps {\n  /** Group title, e.g. \"Morning\". */\n  label: string;\n  slots: BookingSlot[];\n  className?: string;\n}\n\nfunction BookingCardSlots({ label, slots, className }: BookingCardSlotsProps) {\n  const { day, slot, setSlot } = useBookingCard(\"Slots\");\n  const surface = useSurface();\n  const isLiquid = surface === \"liquid-glass\";\n\n  return (\n    <div className={className}>\n      <p className={cn(\"text-xs\", isLiquid ? \"text-white/70\" : \"text-foreground/50\")}>{label}</p>\n      <div role=\"radiogroup\" aria-label={`${label} times`} className=\"mt-2 flex flex-wrap gap-2\">\n        {slots.map((s) => {\n          const selected = slot === s.label;\n          return (\n            <button\n              key={s.label}\n              type=\"button\"\n              role=\"radio\"\n              aria-checked={selected}\n              disabled={s.taken || day === null}\n              onClick={() => setSlot(s.label)}\n              className={cn(\n                \"h-9 rounded-[var(--radius-xl,1rem)] border px-3.5 text-xs font-medium tabular-nums transition-colors\",\n                \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary\",\n                selected\n                  ? isLiquid\n                    ? \"border-transparent bg-white text-black font-semibold shadow-sm\"\n                    : \"border-transparent bg-primary text-primary-foreground\"\n                  : s.taken\n                    ? isLiquid\n                      ? \"border-transparent text-white/30 line-through\"\n                      : \"border-transparent text-foreground/30 line-through\"\n                    : isLiquid\n                      ? \"border-white/30 bg-white/10 text-white hover:bg-white/25 disabled:opacity-40\"\n                      : \"border-foreground/10 bg-foreground/[0.03] hover:bg-foreground/[0.06] disabled:opacity-50\",\n              )}\n            >\n              {s.label}\n            </button>\n          );\n        })}\n      </div>\n    </div>\n  );\n}\n\nexport interface BookingCardSubmitProps {\n  children?: React.ReactNode;\n  className?: string;\n}\n\nfunction BookingCardSubmit({ children = \"Book now\", className }: BookingCardSubmitProps) {\n  const { day, slot, status, submit } = useBookingCard(\"Submit\");\n  const surface = useSurface();\n  const isLiquid = surface === \"liquid-glass\";\n  const ready = day !== null && slot !== null;\n  const busy = status === \"booking\";\n  return (\n    <button\n      type=\"button\"\n      disabled={!ready || busy}\n      aria-busy={busy}\n      onClick={submit}\n      className={cn(\n        \"inline-flex h-11 w-full items-center justify-center gap-2 rounded-[var(--radius-xl,1rem)] px-5 text-sm font-medium transition-[background-color,transform] active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100\",\n        isLiquid\n          ? \"bg-white text-black hover:bg-white/90 font-semibold shadow-sm\"\n          : \"bg-primary text-primary-foreground hover:bg-primary/90\",\n        \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n        className,\n      )}\n    >\n      {busy ? (\n        <>\n          <span className={cn(\"size-4 animate-spin rounded-full border-2 motion-reduce:animate-none\", isLiquid ? \"border-black/30 border-t-black\" : \"border-primary-foreground/40 border-t-primary-foreground\")} />\n          Booking\n        </>\n      ) : ready && day ? (\n        `${children}, ${WEEKDAY.format(day)} ${day.getDate()} at ${slot}`\n      ) : (\n        children\n      )}\n    </button>\n  );\n}\n\nexport interface BookingCardConfirmationProps {\n  /** Who the appointment is with, e.g. \"Dr. Priya Nair\". */\n  withName: string;\n  /** Where, e.g. \"Riverside Heart & Medical Group\". */\n  place?: string;\n  className?: string;\n}\n\n// The cover fades in and out over 0.25s. Inside it the ring draws (0.5s), the\n// check follows (0.35s from 0.45s), the details rise 12px (0.35s from 0.7s)\n// and the actions fade in (0.3s from 1s) — all ease-out. The strokes draw the\n// way Motion's `pathLength` does: `pathLength=\"1\"` on the shape and\n// a dash array growing from \"0 1\" to \"1 1\".\nconst CONFIRMATION_CSS = `\n.booking-card-confirmation { transition: opacity 0.25s cubic-bezier(0, 0, 0.58, 1); }\n.booking-card-confirmation[data-state=\"closed\"] { opacity: 0; }\n@starting-style {\n  .booking-card-confirmation[data-state=\"open\"] { opacity: 0; }\n}\n@keyframes booking-card-draw {\n  from { stroke-dasharray: 0px 1px; }\n  to { stroke-dasharray: 1px 1px; }\n}\n@keyframes booking-card-rise { from { opacity: 0; translate: 0 12px; } }\n@keyframes booking-card-fade { from { opacity: 0; } }\n.booking-card-ring { animation: booking-card-draw 0.5s cubic-bezier(0, 0, 0.58, 1) both; }\n.booking-card-check { animation: booking-card-draw 0.35s cubic-bezier(0, 0, 0.58, 1) 0.45s both; }\n.booking-card-details { animation: booking-card-rise 0.35s cubic-bezier(0, 0, 0.58, 1) 0.7s backwards; }\n.booking-card-actions { animation: booking-card-fade 0.3s cubic-bezier(0, 0, 0.58, 1) 1s backwards; }\n`;\n\n/**\n * Covers the card once a booking is made: the check draws itself, then the\n * details rise in. \"Book another\" clears it.\n */\nfunction BookingCardConfirmation({ withName, place, className }: BookingCardConfirmationProps) {\n  const { day, slot, status, reset } = useBookingCard(\"Confirmation\");\n  const surface = useSurface();\n  const isLiquid = surface === \"liquid-glass\";\n  const open = status === \"booked\" && day !== null && slot !== null;\n\n  // \"Book another\" clears the day and slot straight away, but the cover is\n  // still fading out: it keeps showing the booking it opened with.\n  const [booked, setBooked] = useState<Booking | null>(null);\n  if (open && (booked?.day !== day || booked?.slot !== slot)) setBooked({ day, slot });\n  const [mounted, setMounted] = useState(open);\n  if (open && !mounted) setMounted(true);\n\n  useEffect(() => {\n    if (open || !mounted) return;\n    // Backstop for when no transitionend arrives (e.g. a hidden tab).\n    const timer = setTimeout(() => setMounted(false), 300);\n    return () => clearTimeout(timer);\n  }, [open, mounted]);\n\n  const shown = open ? { day, slot } : booked;\n\n  return (\n    <>\n      {mounted && shown && (\n        <div\n          role=\"status\"\n          data-state={open ? \"open\" : \"closed\"}\n          onTransitionEnd={(e) => {\n            if (e.target === e.currentTarget && e.propertyName === \"opacity\" && !open) {\n              setMounted(false);\n            }\n          }}\n          className={cn(\n            \"booking-card-confirmation absolute inset-0 z-10 flex flex-col items-center justify-center gap-4 rounded-[inherit] p-6 text-center backdrop-blur-2xl transition-colors duration-200\",\n            isLiquid\n              ? \"bg-black/85 text-white border border-white/30 shadow-2xl\"\n              : \"bg-card/85 text-card-foreground backdrop-blur-xl\",\n            className,\n          )}\n        >\n          <style>{CONFIRMATION_CSS}</style>\n          <svg viewBox=\"0 0 64 64\" className=\"size-20\" aria-hidden>\n            <circle\n              cx=\"32\"\n              cy=\"32\"\n              r=\"28\"\n              fill=\"none\"\n              strokeWidth=\"3\"\n              pathLength={1}\n              className={cn(\"booking-card-ring\", isLiquid ? \"stroke-white\" : \"stroke-primary\")}\n            />\n            <path\n              d=\"M20 33 L28 41 L44 24\"\n              fill=\"none\"\n              strokeWidth=\"3.5\"\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              pathLength={1}\n              className={cn(\"booking-card-check\", isLiquid ? \"stroke-white\" : \"stroke-primary\")}\n            />\n          </svg>\n\n          <div className=\"booking-card-details\">\n            <p className=\"text-lg font-semibold\">Booked</p>\n            <p className={cn(\"mt-1 text-sm\", isLiquid ? \"text-white/80\" : \"text-foreground/70\")}>\n              {LONG_DATE.format(shown.day)} at {shown.slot}\n            </p>\n            <p className={cn(\"mt-0.5 text-sm\", isLiquid ? \"text-white/70\" : \"text-foreground/60\")}>\n              with {withName}\n              {place ? `, ${place}` : \"\"}\n            </p>\n          </div>\n\n          <div className=\"booking-card-actions flex w-full max-w-[240px] flex-col gap-2\">\n            <button\n              type=\"button\"\n              className={cn(\n                \"h-10 rounded-full px-4 text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary\",\n                isLiquid\n                  ? \"bg-white text-black font-semibold hover:bg-white/90 shadow-sm\"\n                  : \"bg-primary text-primary-foreground hover:bg-primary/90\",\n              )}\n            >\n              Add to calendar\n            </button>\n            <button\n              type=\"button\"\n              onClick={reset}\n              className={cn(\n                \"h-10 rounded-full border px-4 text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary\",\n                isLiquid\n                  ? \"border-white/30 text-white hover:bg-white/20\"\n                  : \"border-foreground/10 hover:bg-foreground/5\",\n              )}\n            >\n              Book another\n            </button>\n          </div>\n        </div>\n      )}\n    </>\n  );\n}\n\nBookingCard.Days = BookingCardDays;\nBookingCard.Slots = BookingCardSlots;\nBookingCard.Submit = BookingCardSubmit;\nBookingCard.Confirmation = BookingCardConfirmation;\n",
      "type": "registry:component",
      "target": "components/custom/blocks/BookingCardBlock.tsx"
    },
    {
      "path": "src/components/custom/blocks/glass-panel-context.ts",
      "content": "\"use client\";\n\nimport { createContext, useContext } from \"react\";\n\n/**\n * The page's surface treatment. `glass` is frosted panels over an aurora;\n * `solid` is plain theme cards with an ordinary shadow on a flat ground.\n */\nexport type SurfaceVariant = \"glass\" | \"solid\" | \"liquid-glass\";\n\n/** Set by `GlassBackdrop` for every `GlassPanel` inside it. */\nexport const SurfaceContext = createContext<SurfaceVariant>(\"glass\");\n\n/** The surface variant of the enclosing `GlassBackdrop`. */\nexport const useSurface = () => useContext(SurfaceContext);\n",
      "type": "registry:component",
      "target": "components/custom/blocks/glass-panel-context.ts"
    }
  ],
  "docs": "Named export: BookingCard (plus the BookingSlot and Booking types). `BookingCard` is a compound component — put `BookingCard.Days`, `BookingCard.Slots`, `BookingCard.Submit` and `BookingCard.Confirmation` inside it in any layout; it holds the chosen day and slot and calls `onBook`. It draws no surface of its own: make it the padded inside of whatever panel it sits in so the confirmation reaches the edges. The selected day and slot, the submit and 'Add to calendar' buttons, the check mark and the focus rings use the `--primary` token.",
  "categories": [
    "forms",
    "dates"
  ],
  "type": "registry:component"
}
