{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "seatmap-1",
  "title": "Seat map 1: choose seats",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A seat map for a cinema or venue: rows with walkways, price tiers, taken and wheelchair seats marked in words, a limit and running total, a best-seats-together helper, keyboard arrows, and a hold that survives a lost seat.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/button.json",
    "https://ui.ballmac.com/r/skeleton.json",
    "https://ui.ballmac.com/r/state-view.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/seatmap-1/seatmap-1.tsx",
      "content": "// Ballmac UI: Seat map 1. https://ui.ballmac.com/blocks/seatmap-1\n\"use client\"\n\nimport * as React from \"react\"\nimport { Accessibility, CircleAlert, Loader2, Armchair, Sparkles, Ticket, X } from \"lucide-react\"\n\nimport { Button } from \"@/components/ballmac/button\"\nimport { Skeleton } from \"@/components/ballmac/skeleton\"\nimport { frameProps, StateView, type StateProps } from \"@/components/ballmac/state-view\"\nimport { cn } from \"@/lib/utils\"\n\ntype Seatmap1Tier = { id: string; label: string; price: number }\ntype Seatmap1Seat = { id: string; row: string; number: number; tier: string; taken?: boolean; accessible?: boolean }\ntype Seatmap1Row = { row: string; tier: string; /** Seat numbers to leave empty, such as a walkway. */ gaps?: number[]; seats: number }\n\ntype Seatmap1Props = Omit<React.ComponentProps<\"section\">, \"title\"> &\n  StateProps & {\n    title?: string\n    /** \"Dune: Part Three · Sat 24 Oct, 20:15 · Screen 2\". */\n    show?: string\n    tiers?: Seatmap1Tier[]\n    rows?: Seatmap1Row[]\n    /** Ids like \"C7\" that are already taken. */\n    taken?: string[]\n    /** Ids of seats set aside for wheelchair users. */\n    accessible?: string[]\n    maxSeats?: number\n    currency?: string\n    locale?: string\n    defaultSelected?: string[]\n    /** Called to hold the seats. Return a promise; reject, and put `taken: string[]` on the error if someone else got a seat first. */\n    onContinue?: (seats: Seatmap1Seat[], total: number) => void | Promise<void>\n  }\n\nconst tiers0: Seatmap1Tier[] = [{ id: \"std\", label: \"Standard\", price: 12 }, { id: \"prem\", label: \"Premium\", price: 16 }, { id: \"rec\", label: \"Recliner\", price: 22 }]\nconst rows0: Seatmap1Row[] = \"ABCDEFGHI\".split(\"\").map((r, i) => ({ row: r, tier: i < 3 ? \"std\" : i < 7 ? \"prem\" : \"rec\", seats: i < 3 ? 12 : 14, gaps: i < 3 ? [6] : [7] }))\nconst taken0 = [\"B4\", \"B5\", \"C9\", \"D6\", \"D7\", \"D8\", \"E3\", \"E10\", \"E11\", \"F7\", \"F8\", \"G5\", \"G12\", \"H2\", \"H3\", \"H9\", \"I6\", \"I7\", \"A3\", \"A10\"]\n\nfunction Skel() {\n  return (\n    <div aria-hidden=\"true\" className=\"grid gap-6 lg:grid-cols-[minmax(0,1fr)_20rem]\">\n      <Skeleton className=\"h-[30rem] rounded-3xl\" />\n      <Skeleton className=\"h-64 rounded-3xl\" />\n    </div>\n  )\n}\n\n/**\n * A seat map for a cinema, theatre or venue: a screen at the front, rows of seats with walkways, three price tiers, taken and wheelchair seats marked in words\n * as well as color, up to a limit chosen seats with their prices and a running total, and a helper that picks the best seats together for a party. It works\n * from the keyboard with the arrow keys, and if someone else takes a seat first the hold fails, those seats are marked taken and your others stay chosen.\n */\nfunction Seatmap1({\n  title = \"Choose your seats\",\n  show = \"Dune: Part Three · Sat 24 Oct, 20:15 · Screen 2\",\n  tiers = tiers0,\n  rows = rows0,\n  taken: takenProp = taken0,\n  accessible = [\"A1\", \"A12\"],\n  maxSeats = 6,\n  currency = \"USD\",\n  locale = \"en\",\n  defaultSelected = [],\n  onContinue,\n  state = \"ready\",\n  onRetry,\n  emptyState,\n  errorState,\n  className,\n  ...props\n}: Seatmap1Props) {\n  const id = React.useId()\n  const [takenIds, setTakenIds] = React.useState<string[]>(takenProp)\n  React.useEffect(() => setTakenIds(takenProp), [takenProp])\n  const [selected, setSelected] = React.useState<string[]>(defaultSelected)\n  const [party, setParty] = React.useState(2)\n  const [focusId, setFocusId] = React.useState<string>(() => rows[0] ? `${rows[0].row}1` : \"\")\n  const [sending, setSending] = React.useState(false)\n  const [error, setError] = React.useState<string>()\n  const [done, setDone] = React.useState(false)\n  const [announce, setAnnounce] = React.useState(\"\")\n  const grid = React.useRef<HTMLDivElement>(null)\n  const money = (n: number) => new Intl.NumberFormat(locale, { style: \"currency\", currency }).format(n)\n  const tier = (t: string) => tiers.find((x) => x.id === t) ?? tiers[0]!\n\n  const seats = React.useMemo(() => rows.flatMap((r) => Array.from({ length: r.seats }, (_, i) => i + 1).filter((n) => !(r.gaps ?? []).includes(n)).map((n): Seatmap1Seat => ({ id: `${r.row}${n}`, row: r.row, number: n, tier: r.tier, accessible: accessible.includes(`${r.row}${n}`) }))), [rows, accessible])\n  const byId = React.useMemo(() => new Map(seats.map((s) => [s.id, s])), [seats])\n  const isTaken = (s: Seatmap1Seat) => takenIds.includes(s.id)\n  const chosen = selected.map((i) => byId.get(i)).filter((s): s is Seatmap1Seat => !!s)\n  const total = chosen.reduce((n, s) => n + tier(s.tier).price, 0)\n  const empty = state === \"empty\"\n\n  const toggle = (s: Seatmap1Seat) => {\n    if (isTaken(s) || done) return\n    setError(undefined)\n    setSelected((cur) => {\n      if (cur.includes(s.id)) { setAnnounce(`Row ${s.row}, seat ${s.number} removed`); return cur.filter((x) => x !== s.id) }\n      if (cur.length >= maxSeats) { setError(`You can choose up to ${maxSeats} seats. Remove one to pick another.`); return cur }\n      setAnnounce(`Row ${s.row}, seat ${s.number} added, ${tier(s.tier).label}, ${money(tier(s.tier).price)}`)\n      return [...cur, s.id]\n    })\n  }\n\n  // Best seats: the middle of the best-priced middle rows, as a block of neighbours with no taken seat between them.\n  function best() {\n    setError(undefined)\n    const order = [...rows].sort((a, b) => Math.abs(rows.indexOf(a) - rows.length * 0.55) - Math.abs(rows.indexOf(b) - rows.length * 0.55))\n    for (const r of order) {\n      const rowSeats = seats.filter((s) => s.row === r.row)\n      const mid = (r.seats + 1) / 2\n      let pick: Seatmap1Seat[] | null = null\n      let score = Infinity\n      for (let i = 0; i + party <= rowSeats.length; i++) {\n        const block = rowSeats.slice(i, i + party)\n        const contiguous = block.every((s, k) => k === 0 || s.number === block[k - 1]!.number + 1)\n        if (!contiguous || block.some((s) => isTaken(s) || s.accessible)) continue\n        const d = Math.abs(block.reduce((n, s) => n + s.number, 0) / party - mid)\n        if (d < score) { score = d; pick = block }\n      }\n      if (pick) {\n        setSelected(pick.map((s) => s.id))\n        setFocusId(pick[0]!.id)\n        setAnnounce(`${party} seats together chosen in row ${r.row}`)\n        return\n      }\n    }\n    setError(`There are no ${party} seats together left. Try a smaller group.`)\n  }\n\n  function move(e: React.KeyboardEvent, s: Seatmap1Seat) {\n    const ri = rows.findIndex((r) => r.row === s.row)\n    const rowSeats = seats.filter((x) => x.row === s.row)\n    const si = rowSeats.indexOf(s)\n    let next: Seatmap1Seat | undefined\n    if (e.key === \"ArrowRight\") next = rowSeats[Math.min(rowSeats.length - 1, si + 1)]\n    else if (e.key === \"ArrowLeft\") next = rowSeats[Math.max(0, si - 1)]\n    else if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n      const tr = rows[ri + (e.key === \"ArrowDown\" ? 1 : -1)]\n      if (tr) { const rs = seats.filter((x) => x.row === tr.row); next = rs.reduce((a, b) => (Math.abs(b.number / tr.seats - s.number / rows[ri]!.seats) < Math.abs(a.number / tr.seats - s.number / rows[ri]!.seats) ? b : a)) }\n    } else if (e.key === \"Home\") next = rowSeats[0]\n    else if (e.key === \"End\") next = rowSeats.at(-1)\n    else return\n    e.preventDefault()\n    if (next) { setFocusId(next.id); requestAnimationFrame(() => grid.current?.querySelector<HTMLElement>(`[data-seat=\"${next!.id}\"]`)?.focus()) }\n  }\n\n  async function hold() {\n    setError(undefined)\n    setSending(true)\n    try {\n      await onContinue?.(chosen, total)\n      setDone(true)\n      setAnnounce(\"Your seats are held\")\n    } catch (e) {\n      const lost = (e as { taken?: string[] }).taken\n      if (lost?.length) {\n        setTakenIds((t) => [...new Set([...t, ...lost])])\n        setSelected((cur) => cur.filter((x) => !lost.includes(x)))\n        setError(`${lost.map((l) => `${byId.get(l)?.row ?? \"\"}${byId.get(l)?.number ?? l}`).join(\", \")} ${lost.length === 1 ? \"was\" : \"were\"} just taken by someone else. Your other seats are still chosen.`)\n      } else setError(e instanceof Error ? e.message : \"We could not hold your seats. Nothing was charged, so try again.\")\n    } finally {\n      setSending(false)\n    }\n  }\n\n  const content = empty ? (\n    <div className=\"bg-card rounded-3xl border px-6 py-16 text-center\">\n      <span aria-hidden=\"true\" className=\"bg-muted mx-auto flex size-12 items-center justify-center rounded-full\">\n        <Ticket className=\"size-6\" />\n      </span>\n      <h2 className=\"mt-5 text-lg font-semibold tracking-tight\">{emptyState?.title ?? \"No showing selected\"}</h2>\n      <p className=\"text-muted-foreground mx-auto mt-1.5 max-w-xs text-sm text-pretty\">{emptyState?.description ?? \"Choose a date and time first, then the seats for it appear here.\"}</p>\n      {emptyState?.action}\n    </div>\n  ) : done ? (\n    <div role=\"status\" className=\"bg-card rounded-3xl border px-6 py-16 text-center\">\n      <span aria-hidden=\"true\" className=\"bg-chart-2/15 mx-auto flex size-14 items-center justify-center rounded-full\">\n        <Ticket className=\"size-7\" />\n      </span>\n      <h2 className=\"mt-5 text-2xl font-semibold tracking-tight\">Seats held for you</h2>\n      <p className=\"text-muted-foreground mx-auto mt-2 max-w-sm text-pretty\">\n        {chosen.map((s) => `${s.row}${s.number}`).join(\", \")} for {show.split(\"·\")[0]!.trim()}. Pay within 10 minutes to keep them. Total {money(total)}.\n      </p>\n    </div>\n  ) : (\n    <div className=\"grid items-start gap-6 lg:grid-cols-[minmax(0,1fr)_20rem]\">\n      <div className=\"bg-card min-w-0 rounded-3xl border p-4 sm:p-7\">\n        <div aria-hidden=\"true\" className=\"mx-auto mb-8 max-w-md\">\n          <div className=\"from-foreground/25 mx-auto h-1.5 rounded-[50%] bg-gradient-to-b to-transparent\" style={{ boxShadow: \"0 14px 22px -8px color-mix(in oklab, var(--foreground) 22%, transparent)\" }} />\n          <p className=\"text-muted-foreground mt-2 text-center text-xs tracking-widest uppercase\">Screen</p>\n        </div>\n        <div role=\"region\" tabIndex={0} aria-label=\"Seat map\" className=\"focus-visible:ring-ring/50 overflow-x-auto rounded-xl pb-2 outline-none focus-visible:ring-[3px]\">\n          <div ref={grid} role=\"grid\" aria-label={`Seats, rows ${rows[0]?.row} to ${rows.at(-1)?.row}`} dir=\"ltr\" className=\"mx-auto grid w-max gap-1.5\">\n            {rows.map((r) => (\n              <div key={r.row} role=\"row\" className=\"flex items-center gap-1.5\">\n                <span aria-hidden=\"true\" className=\"text-muted-foreground w-5 text-center text-xs font-medium\">\n                  {r.row}\n                </span>\n                {Array.from({ length: r.seats }, (_, i) => i + 1).map((n) => {\n                  if ((r.gaps ?? []).includes(n)) return <span key={n} role=\"presentation\" className=\"w-4\" />\n                  const s = byId.get(`${r.row}${n}`)!\n                  const t = tier(s.tier)\n                  const tk = isTaken(s)\n                  const on = selected.includes(s.id)\n                  return (\n                    <button\n                      key={n}\n                      type=\"button\"\n                      role=\"gridcell\"\n                      data-seat={s.id}\n                      aria-selected={on}\n                      aria-disabled={tk || undefined}\n                      tabIndex={focusId === s.id ? 0 : -1}\n                      aria-label={`Row ${s.row}, seat ${s.number}, ${t.label}, ${money(t.price)}${s.accessible ? \", wheelchair space\" : \"\"}, ${tk ? \"taken\" : on ? \"selected\" : \"available\"}`}\n                      onFocus={() => setFocusId(s.id)}\n                      onKeyDown={(e) => move(e, s)}\n                      onClick={() => toggle(s)}\n                      className={cn(\n                        \"focus-visible:ring-ring/60 relative flex size-7 items-center justify-center rounded-t-lg rounded-b-sm border text-[10px] font-medium tabular-nums outline-none transition-[transform,background-color] focus-visible:ring-[3px] motion-reduce:transition-none sm:size-8\",\n                        tk ? \"bg-muted text-muted-foreground cursor-not-allowed border-transparent\" : on ? \"bg-foreground text-background border-foreground scale-110\" : s.tier === \"rec\" ? \"bg-chart-5/20 border-chart-5/50 hover:bg-chart-5/35\" : s.tier === \"prem\" ? \"bg-chart-1/15 border-chart-1/40 hover:bg-chart-1/30\" : \"bg-background hover:bg-accent\"\n                      )}\n                    >\n                      {tk ? <X aria-hidden=\"true\" className=\"size-3.5\" /> : s.accessible ? <Accessibility aria-hidden=\"true\" className=\"size-4\" /> : on ? <Armchair aria-hidden=\"true\" className=\"size-4\" /> : n}\n                    </button>\n                  )\n                })}\n                <span aria-hidden=\"true\" className=\"text-muted-foreground w-5 text-center text-xs font-medium\">\n                  {r.row}\n                </span>\n              </div>\n            ))}\n          </div>\n        </div>\n        <ul className=\"text-muted-foreground mt-6 flex flex-wrap justify-center gap-x-5 gap-y-2 text-xs\">\n          {tiers.map((t) => (\n            <li key={t.id} className=\"flex items-center gap-2\">\n              <span aria-hidden=\"true\" className={cn(\"size-4 rounded-t-md rounded-b-sm border\", t.id === \"rec\" ? \"bg-chart-5/20 border-chart-5/50\" : t.id === \"prem\" ? \"bg-chart-1/15 border-chart-1/40\" : \"bg-background\")} />\n              {t.label} {money(t.price)}\n            </li>\n          ))}\n          <li className=\"flex items-center gap-2\">\n            <span aria-hidden=\"true\" className=\"bg-foreground size-4 rounded-t-md rounded-b-sm\" />\n            Selected\n          </li>\n          <li className=\"flex items-center gap-2\">\n            <span aria-hidden=\"true\" className=\"bg-muted flex size-4 items-center justify-center rounded-t-md rounded-b-sm\">\n              <X className=\"size-3\" />\n            </span>\n            Taken\n          </li>\n          <li className=\"flex items-center gap-2\">\n            <Accessibility aria-hidden=\"true\" className=\"size-4\" />\n            Wheelchair space\n          </li>\n        </ul>\n      </div>\n\n      <aside aria-labelledby={`${id}-sum`} className=\"bg-card rounded-3xl border p-5 sm:p-6 lg:sticky lg:top-6\">\n        <h2 id={`${id}-sum`} className=\"font-semibold tracking-tight\">\n          Your seats <span className=\"text-muted-foreground font-normal tabular-nums\">· {chosen.length} of {maxSeats}</span>\n        </h2>\n        <div className=\"bg-muted/50 mt-4 rounded-2xl p-4\">\n          <p className=\"flex items-center gap-2 text-sm font-medium\">\n            <Sparkles aria-hidden=\"true\" className=\"size-4\" />\n            Best seats together\n          </p>\n          <div className=\"mt-3 flex items-center gap-2\">\n            <label htmlFor={`${id}-p`} className=\"text-muted-foreground text-sm\">\n              For\n            </label>\n            <select id={`${id}-p`} value={party} onChange={(e) => setParty(Number(e.target.value))} className=\"bg-background focus-visible:ring-ring/50 h-9 rounded-md border px-2 text-sm outline-none focus-visible:ring-[3px]\">\n              {Array.from({ length: maxSeats }, (_, i) => i + 1).map((n) => (\n                <option key={n} value={n}>\n                  {n} {n === 1 ? \"person\" : \"people\"}\n                </option>\n              ))}\n            </select>\n            <Button size=\"sm\" variant=\"outline\" onClick={best} className=\"ms-auto\">\n              Pick for me\n            </Button>\n          </div>\n        </div>\n\n        {chosen.length ? (\n          <ul className=\"mt-4 grid gap-2\">\n            {chosen.map((s) => (\n              <li key={s.id} className=\"flex items-center justify-between gap-3 text-sm\">\n                <span>\n                  <span className=\"font-semibold\">\n                    Row {s.row}, seat {s.number}\n                  </span>\n                  <span className=\"text-muted-foreground block text-xs\">{tier(s.tier).label}</span>\n                </span>\n                <span className=\"flex items-center gap-1 tabular-nums\">\n                  {money(tier(s.tier).price)}\n                  <Button variant=\"ghost\" size=\"icon\" aria-label={`Remove row ${s.row}, seat ${s.number}`} onClick={() => toggle(s)}>\n                    <X aria-hidden=\"true\" />\n                  </Button>\n                </span>\n              </li>\n            ))}\n          </ul>\n        ) : (\n          <p className=\"text-muted-foreground mt-4 text-sm text-pretty\">Press a seat to choose it, or let us pick the best ones together.</p>\n        )}\n\n        <div className=\"mt-5 flex items-baseline justify-between border-t pt-4\">\n          <p className=\"text-muted-foreground text-sm\">Total</p>\n          <p className=\"text-2xl font-semibold tracking-tight tabular-nums\">{money(total)}</p>\n        </div>\n        {error ? (\n          <p role=\"alert\" className=\"text-destructive mt-3 flex items-start gap-2 text-sm\">\n            <CircleAlert aria-hidden=\"true\" className=\"mt-0.5 size-4 shrink-0\" />\n            {error}\n          </p>\n        ) : null}\n        <Button className=\"mt-4 w-full\" size=\"lg\" disabled={!chosen.length || sending} onClick={() => void hold()}>\n          {sending ? <Loader2 aria-hidden=\"true\" className=\"animate-spin motion-reduce:animate-none\" /> : <Ticket aria-hidden=\"true\" />}\n          {sending ? \"Holding\" : chosen.length ? `Hold ${chosen.length} ${chosen.length === 1 ? \"seat\" : \"seats\"}` : \"Choose seats\"}\n        </Button>\n      </aside>\n    </div>\n  )\n\n  return (\n    <section data-slot=\"seatmap-1\" data-state={state} aria-labelledby={`${id}-title`} className={cn(\"mx-auto w-full max-w-5xl px-4 py-10 sm:px-6\", className)} {...frameProps(props)}>\n      <header className=\"mb-8\">\n        <h1 id={`${id}-title`} className=\"text-3xl font-semibold tracking-[-0.035em] text-balance\">\n          {title}\n        </h1>\n        <p className=\"text-muted-foreground mt-2 text-pretty\">{show}</p>\n      </header>\n      <StateView state={state === \"loading\" || state === \"error\" ? state : \"ready\"} onRetry={onRetry} label=\"Loading the seats\" loading={<Skel />} error={{ title: errorState?.title ?? \"The seats could not be loaded\", description: errorState?.description ?? \"Nothing was held. Check your connection and try again.\", ...errorState }}>\n        {content}\n      </StateView>\n      <p aria-live=\"polite\" className=\"sr-only\">\n        {announce}\n      </p>\n    </section>\n  )\n}\n\nexport { Seatmap1, type Seatmap1Props, type Seatmap1Seat, type Seatmap1Tier, type Seatmap1Row }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/seatmap-1/seatmap-1.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "seat map",
      "seat picker",
      "cinema",
      "theatre",
      "tickets",
      "booking",
      "venue",
      "seating chart",
      "event",
      "reservation"
    ],
    "version": "1.0.0",
    "updated": "2026-10-18",
    "tests": true,
    "ai": {
      "summary": "A seat picker. <Seatmap1 show tiers={[{ id, label, price }]} rows={[{ row: 'A', tier: 'std', seats: 12, gaps: [6] }]} taken={['B4']} accessible={['A1']} maxSeats={6} currency locale defaultSelected={['C5']} onContinue={(seats, total) => Promise} />. Seat ids are the row letter plus the number ('C7'); gaps are numbers left empty for walkways. Pick for me chooses the best block of neighbouring seats for the party size. Reject onContinue with an error that has taken: ['C7'] and those seats become taken while the rest stay chosen; any other rejection shows its message.",
      "whenToUse": [
        "Cinema, theatre, concert and event seat selection",
        "Any fixed-seat booking such as trains or classrooms"
      ],
      "whenNotToUse": [
        "General admission tickets (use event-1)",
        "A table or room booking by time (use a calendar)"
      ],
      "composesWith": [
        "event-1",
        "invoice-1",
        "locations-1",
        "app-shell-1"
      ],
      "a11y": [
        {
          "keys": "Tab, then arrows, Home and End",
          "action": "The hall is a grid with one tab stop; Left and Right move along a row, Up and Down to the nearest seat in the next row; Enter or Space chooses"
        },
        {
          "keys": "Each seat",
          "action": "Reads 'Row C, seat 7, Premium, $16.00, available' (or selected, taken, wheelchair space); taken seats are marked with an X and the word, not only grey"
        },
        {
          "keys": "Choices",
          "action": "Adding, removing and picking seats for you are announced politely; reaching the limit says so in an alert"
        }
      ],
      "customization": [
        "rows and tiers are plain data, so any hall shape works",
        "The hall keeps seat 1 on the left in every language, like a floor plan",
        "A hold that fails never clears seats that are still free"
      ]
    },
    "states": [
      {
        "id": "loading",
        "title": "Loading",
        "example": "seatmap-1-loading",
        "trigger": "state=\"loading\"",
        "note": "A skeleton hall and summary under the real heading, announced as loading."
      },
      {
        "id": "empty",
        "title": "No showing chosen",
        "example": "seatmap-1-empty",
        "trigger": "state=\"empty\"",
        "note": "Says to choose a date and time first; emptyState sets the copy and can add a button."
      },
      {
        "id": "error",
        "title": "Could not load",
        "example": "seatmap-1-error",
        "trigger": "state=\"error\"",
        "note": "An alert saying nothing was held, with Try again (onRetry). A hold that fails says nothing was charged; a lost seat is marked taken and the others stay chosen."
      }
    ],
    "examples": [
      "seatmap-1-demo",
      "seatmap-1-chosen",
      "seatmap-1-conflict",
      "seatmap-1-loading",
      "seatmap-1-empty",
      "seatmap-1-error"
    ],
    "url": "https://ui.ballmac.com/components/seatmap-1"
  },
  "categories": [
    "blocks",
    "seatmap"
  ],
  "type": "registry:block"
}