{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "booking29",
  "title": "Appointment Intake Match",
  "description": "Three intake questions that land on a real appointment type instead of a drop-down: the answers pick a route with its length, its facts, and the clinician you will sit with, and the image tile beside them swaps its floated availability card to match.",
  "dependencies": [
    "clsx",
    "tailwind-merge",
    "lucide-react"
  ],
  "registryDependencies": [
    "questionnaire",
    "button"
  ],
  "files": [
    {
      "path": "registry/booking29/booking29.tsx",
      "content": "\"use client\";\n\nimport { type FormEvent, type ReactNode, useState } from \"react\";\nimport Link from \"next/link\";\nimport { Badge23 } from \"@/components/beste/component/badge23\";\nimport { Button21 } from \"@/components/beste/component/button21\";\nimport { Card31 } from \"@/components/beste/piece/card31\";\nimport {\n  Questionnaire,\n  QuestionnaireActions,\n  QuestionnaireChoice,\n  QuestionnaireChoiceDescription,\n  QuestionnaireChoices,\n  QuestionnaireDescription,\n  QuestionnaireError,\n  QuestionnaireItem,\n  QuestionnaireNext,\n  QuestionnairePrevious,\n  QuestionnaireProgress,\n  QuestionnaireSubmit,\n  QuestionnaireTitle,\n} from \"@/components/ui/questionnaire\";\nimport { cn } from \"@/lib/utils\";\n\ninterface Badge {\n  label: string;\n}\n\ninterface ActionLink {\n  label: string;\n  href: string;\n}\n\ninterface TileImage {\n  src: string;\n  alt: string;\n}\n\ninterface Fact {\n  label: string;\n  value: string;\n}\n\ninterface IntakeChoice {\n  value: string;\n  label: string;\n  description?: string;\n}\n\ninterface IntakeQuestion {\n  /** Form field name. Must be unique across the intake. */\n  name: string;\n  title: string;\n  description?: string;\n  choices?: IntakeChoice[];\n}\n\ninterface Route {\n  /**\n   * Field name to answer value. A route is only matched when every pair fits.\n   * Leave it out on the last route, which is the catch-all.\n   */\n  match?: Record<string, string>;\n  title: string;\n  summary: string;\n  facts?: Fact[];\n  /** Piece floated over the image tile once this route is matched */\n  media?: ReactNode;\n}\n\ninterface Booking29Labels {\n  previous?: string;\n  next?: string;\n  submit?: string;\n  restart?: string;\n  matchTitle?: string;\n  waitingTitle?: string;\n}\n\ninterface Booking29Props {\n  badge?: Badge;\n  heading?: string;\n  description?: string;\n  questions?: IntakeQuestion[];\n  /** Prints a scoped shortcut on every choice and binds it while the question is active */\n  shortcuts?: \"letters\" | \"numbers\";\n  /** Checked in order; the first one whose `match` fits the answers wins. */\n  routes?: Route[];\n  /** Backdrop behind the floated piece */\n  image?: TileImage;\n  /** Piece shown on the tile before anything has been answered */\n  media?: ReactNode;\n  /** Copy under the tile while the questions are still open */\n  waitingBody?: string;\n  button?: ActionLink;\n  labels?: Booking29Labels;\n  className?: string;\n}\n\nexport const booking29Demo: Booking29Props = {\n  badge: { label: \"Before you book\" },\n  heading: \"Three questions decide the appointment, not a drop-down\",\n  description:\n    \"Every combination lands on a real appointment type with a named clinician and a length, so nobody arrives for twenty minutes that needed an hour.\",\n  shortcuts: \"numbers\",\n  labels: {\n    submit: \"Match me\",\n    restart: \"Change my answers\",\n    matchTitle: \"Your appointment\",\n    waitingTitle: \"On duty today\",\n  },\n  waitingBody:\n    \"Whoever you end up with, the notes follow you. Nobody has to hear the same history twice.\",\n  questions: [\n    {\n      name: \"reason\",\n      title: \"What brings you in?\",\n      description: \"The closest one. It only sets the length and who you sit with.\",\n      choices: [\n        {\n          value: \"assessment\",\n          label: \"Something new\",\n          description: \"A symptom or a concern nobody here has looked at yet.\",\n        },\n        {\n          value: \"review\",\n          label: \"A follow-up\",\n          description: \"Checking in on something already being treated.\",\n        },\n        {\n          value: \"advice\",\n          label: \"A question, not a problem\",\n          description: \"Medication, a letter, or a second opinion.\",\n        },\n      ],\n    },\n    {\n      name: \"history\",\n      title: \"Have you been seen here before?\",\n      choices: [\n        { value: \"new\", label: \"First time\" },\n        { value: \"returning\", label: \"I am already registered\" },\n      ],\n    },\n    {\n      name: \"when\",\n      title: \"When would you rather come in?\",\n      choices: [\n        { value: \"soon\", label: \"The soonest slot going\" },\n        { value: \"evening\", label: \"After work, if possible\" },\n        { value: \"any\", label: \"Whenever suits the clinic\" },\n      ],\n    },\n  ],\n  routes: [\n    {\n      match: { reason: \"assessment\", history: \"new\" },\n      title: \"First assessment, 60 minutes\",\n      summary:\n        \"A full first appointment with the assessment lead. Longer than a standard slot because the history is taken from the beginning.\",\n      facts: [\n        { label: \"With\", value: \"Dr Amelia Frost\" },\n        { label: \"Length\", value: \"60 minutes\" },\n        { label: \"Bring\", value: \"Any current medication\" },\n      ],\n      media: (\n        <Card31\n          avatar={{\n            src: \"https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=160&h=160&fit=crop\",\n            alt: \"Portrait of Dr Amelia Frost\",\n          }}\n          name=\"Dr Amelia Frost\"\n          role=\"Assessment lead\"\n          status=\"Next free Wednesday 09:00\"\n          availability=\"soon\"\n          rows={[\n            { label: \"Appointment\", value: \"60 minutes\" },\n            { label: \"Site\", value: \"Bramble Health\" },\n            { label: \"Waiting\", value: \"6 days on median\" },\n          ]}\n        />\n      ),\n    },\n    {\n      match: { reason: \"review\" },\n      title: \"Follow-up review, 30 minutes\",\n      summary:\n        \"Back with the clinician who is already holding your plan, so the appointment starts where the last one finished.\",\n      facts: [\n        { label: \"With\", value: \"Your named clinician\" },\n        { label: \"Length\", value: \"30 minutes\" },\n        { label: \"Bring\", value: \"Nothing, we have the notes\" },\n      ],\n      media: (\n        <Card31\n          avatar={{\n            src: \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=160&h=160&fit=crop\",\n            alt: \"Portrait of Dr Owen Meade\",\n          }}\n          name=\"Dr Owen Meade\"\n          role=\"Your named clinician\"\n          status=\"Next free tomorrow 11:30\"\n          availability=\"free\"\n          rows={[\n            { label: \"Appointment\", value: \"30 minutes\" },\n            { label: \"Site\", value: \"Kingsway Clinic\" },\n            { label: \"Waiting\", value: \"2 days on median\" },\n          ]}\n        />\n      ),\n    },\n    {\n      title: \"Advice call, 15 minutes\",\n      summary:\n        \"A short call with whoever is on duty. If it turns out to need a room and an hour, they book that for you before hanging up.\",\n      facts: [\n        { label: \"With\", value: \"The duty practitioner\" },\n        { label: \"Length\", value: \"15 minutes, by phone\" },\n        { label: \"Bring\", value: \"The question, written down\" },\n      ],\n      media: (\n        <Card31\n          avatar={{\n            src: \"https://images.unsplash.com/photo-1594744803329-e58b31de8bf5?w=160&h=160&fit=crop\",\n            alt: \"Portrait of Sara Okonjo\",\n          }}\n          name=\"Sara Okonjo\"\n          role=\"Duty practitioner\"\n          status=\"Calling back within the hour\"\n          availability=\"free\"\n          rows={[\n            { label: \"Appointment\", value: \"15 minutes\" },\n            { label: \"Site\", value: \"By phone\" },\n            { label: \"Waiting\", value: \"Same day\" },\n          ]}\n        />\n      ),\n    },\n  ],\n  media: (\n    <Card31\n      avatar={{\n        src: \"https://images.unsplash.com/photo-1594744803329-e58b31de8bf5?w=160&h=160&fit=crop\",\n        alt: \"Portrait of Sara Okonjo\",\n      }}\n      name=\"Sara Okonjo\"\n      role=\"Duty practitioner\"\n      status=\"Free from 09:30 today\"\n      availability=\"free\"\n      rows={[\n        { label: \"Covering\", value: \"All four sites\" },\n        { label: \"Answering\", value: \"Calls and messages\" },\n        { label: \"Until\", value: \"18:00\" },\n      ]}\n    />\n  ),\n  image: {\n    src: \"https://images.unsplash.com/photo-1750918619871-dc74c9a57394?q=80&w=2222&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\n    alt: \"Soft blue gradient backdrop\",\n  },\n  button: { label: \"Choose a time\", href: \"https://beste.co\" },\n};\n\nexport function Booking29({\n  badge,\n  heading,\n  description,\n  questions = [],\n  shortcuts,\n  routes = [],\n  image,\n  media,\n  waitingBody,\n  button,\n  labels = {},\n  className,\n}: Booking29Props) {\n  const {\n    previous: previousLabel,\n    next: nextLabel,\n    submit: submitLabel,\n    restart: restartLabel,\n    matchTitle,\n    waitingTitle,\n  } = labels;\n\n  const [route, setRoute] = useState<Route | null>(null);\n\n  const handleSubmit = (event: FormEvent<HTMLFormElement>) => {\n    event.preventDefault();\n    const data = new FormData(event.currentTarget);\n    const given: Record<string, string> = {};\n    for (const question of questions) {\n      given[question.name] = String(data.get(question.name) ?? \"\");\n    }\n\n    // The last route stands in when nothing fits, so a submit never dead-ends.\n    const matched = routes.find((entry) =>\n      Object.entries(entry.match ?? {}).every(([name, value]) => given[name] === value)\n    );\n    setRoute(matched ?? routes[routes.length - 1] ?? null);\n  };\n\n  const restart = () => setRoute(null);\n\n  return (\n    <section className={cn(\"w-full bg-background py-16 md:py-24\", className)}>\n      <div className=\"mx-auto max-w-7xl px-4 md:px-6\">\n        {badge && <Badge23 label={badge.label} />}\n\n        <div className=\"mt-6 border-t border-border pt-8 md:pt-10\">\n          <div className=\"grid gap-6 md:grid-cols-2 md:gap-12\">\n            {heading && (\n              <h2 className=\"text-3xl font-light leading-[1.1] tracking-tight text-foreground md:text-5xl\">\n                {heading}\n              </h2>\n            )}\n            {description && (\n              <div className=\"flex md:justify-end\">\n                <p className=\"max-w-md text-base leading-relaxed text-muted-foreground\">\n                  {description}\n                </p>\n              </div>\n            )}\n          </div>\n        </div>\n\n        <div className=\"mt-12 grid gap-8 md:mt-16 lg:grid-cols-[minmax(0,1fr)_minmax(0,1.1fr)] lg:gap-12\">\n          <div>\n            {route ? (\n              <>\n                {matchTitle && <p className=\"text-sm text-muted-foreground\">{matchTitle}</p>}\n                <p className=\"mt-3 text-2xl font-light tracking-tight text-foreground md:text-3xl\">\n                  {route.title}\n                </p>\n                <p className=\"mt-4 text-base leading-relaxed text-muted-foreground\">\n                  {route.summary}\n                </p>\n\n                {route.facts && route.facts.length > 0 && (\n                  <dl className=\"mt-8\">\n                    {route.facts.map((fact, index) => (\n                      <div\n                        key={index}\n                        className=\"grid gap-1 border-b border-border py-4 first:border-t sm:grid-cols-[minmax(0,8rem)_minmax(0,1fr)] sm:gap-4\"\n                      >\n                        <dt className=\"text-sm text-muted-foreground\">{fact.label}</dt>\n                        <dd className=\"text-base text-foreground\">{fact.value}</dd>\n                      </div>\n                    ))}\n                  </dl>\n                )}\n\n                <div className=\"mt-8 flex flex-wrap items-center gap-3\">\n                  {button && (\n                    <Button21 asChild label={button.label}>\n                      <Link href={button.href} />\n                    </Button21>\n                  )}\n                  <Button21\n                    label={restartLabel ?? \"Change my answers\"}\n                    tone=\"outline\"\n                    onClick={restart}\n                  />\n                </div>\n              </>\n            ) : (\n              <Questionnaire\n                defaultItem={questions[0]?.name}\n                shortcuts={shortcuts}\n                onSubmit={handleSubmit}\n                className=\"gap-6\"\n              >\n                {questions.length > 0 && (\n                  <QuestionnaireProgress className=\"text-sm font-normal tracking-normal\" />\n                )}\n\n                {questions.map((question, index) => (\n                  <QuestionnaireItem key={index} name={question.name} required>\n                    <QuestionnaireTitle className=\"text-xl font-medium text-foreground\">\n                      {question.title}\n                    </QuestionnaireTitle>\n                    {question.description && (\n                      <QuestionnaireDescription className=\"text-base leading-relaxed\">\n                        {question.description}\n                      </QuestionnaireDescription>\n                    )}\n                    <QuestionnaireChoices>\n                      {question.choices?.map((choice, choiceIndex) => (\n                        <QuestionnaireChoice\n                          key={choiceIndex}\n                          value={choice.value}\n                          className=\"rounded-md border-border p-4 text-base\"\n                        >\n                          <span className=\"font-medium text-foreground\">{choice.label}</span>\n                          {choice.description && (\n                            <QuestionnaireChoiceDescription className=\"text-sm\">\n                              {choice.description}\n                            </QuestionnaireChoiceDescription>\n                          )}\n                        </QuestionnaireChoice>\n                      ))}\n                    </QuestionnaireChoices>\n                    <QuestionnaireError className=\"text-sm\" />\n                  </QuestionnaireItem>\n                ))}\n\n                <QuestionnaireActions>\n                  <QuestionnairePrevious className=\"cursor-pointer rounded-md border-border text-sm font-medium\">\n                    {previousLabel}\n                  </QuestionnairePrevious>\n                  <QuestionnaireNext className=\"cursor-pointer rounded-md bg-primary text-sm font-medium text-primary-foreground hover:bg-primary/90\">\n                    {nextLabel}\n                  </QuestionnaireNext>\n                  <QuestionnaireSubmit className=\"cursor-pointer rounded-md bg-primary text-sm font-medium text-primary-foreground hover:bg-primary/90\">\n                    {submitLabel}\n                  </QuestionnaireSubmit>\n                </QuestionnaireActions>\n              </Questionnaire>\n            )}\n          </div>\n\n          <div>\n            <div className=\"relative flex h-80 items-center justify-center overflow-hidden rounded-md bg-muted md:h-[26rem]\">\n              {image && (\n                <img\n                  className=\"absolute inset-0 size-full object-cover\"\n                  src={image.src}\n                  alt={image.alt}\n                />\n              )}\n              <div className=\"relative z-10 size-full\">{route?.media ?? media}</div>\n            </div>\n\n            <div className=\"mt-6 border-t border-border pt-5\">\n              <p className=\"text-lg font-medium text-foreground\">\n                {route ? route.title : (waitingTitle ?? \"On duty today\")}\n              </p>\n              {waitingBody && (\n                <p className=\"mt-2 max-w-xl text-base leading-relaxed text-muted-foreground\">\n                  {waitingBody}\n                </p>\n              )}\n            </div>\n          </div>\n        </div>\n      </div>\n    </section>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/beste/block/booking29.tsx"
    },
    {
      "path": "registry-pieces/card31/card31.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\n\ntype Surface = \"card\" | \"glass\";\n\ntype Availability = \"free\" | \"soon\" | \"busy\";\n\ninterface MetaRow {\n  label: string;\n  value: string;\n}\n\ninterface Avatar {\n  src: string;\n  alt: string;\n}\n\ninterface Card31Props {\n  avatar?: Avatar;\n  name?: string;\n  role?: string;\n  status?: string;\n  availability?: Availability;\n  rows?: MetaRow[];\n  surface?: Surface;\n  bordered?: boolean;\n  inverted?: boolean;\n  className?: string;\n}\n\nconst dotStyles: Record<Availability, string> = {\n  free: \"bg-emerald-500\",\n  soon: \"bg-amber-500\",\n  busy: \"bg-rose-500\",\n};\n\n\n/* The card sets the colour and everything inside it is drawn in `current`, so\n   inverting is two classes rather than a condition on every element.\n   `glass` is a deliberate exception to the solid-surface rule: these pieces sit\n   over section background images, and a frosted panel is the point of it. */\nconst surfaceClasses: Record<Surface, { plain: string; inverted: string }> = {\n  card: {\n    plain: \"bg-card text-card-foreground\",\n    inverted: \"bg-foreground text-background\",\n  },\n  glass: {\n    plain: \"bg-card/60 text-card-foreground backdrop-blur-md\",\n    inverted: \"bg-foreground/60 text-background backdrop-blur-md\",\n  },\n};\n\nexport const card31Demo: Card31Props = {\n  surface: \"card\",\n  bordered: true,\n  inverted: false,\n  avatar: {\n    src: \"https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=160&h=160&fit=crop\",\n    alt: \"Portrait of Priya Nandan\",\n  },\n  name: \"Priya Nandan\",\n  role: \"Migration engineer\",\n  status: \"Free from 09:30 today\",\n  availability: \"free\",\n  rows: [\n    { label: \"Call length\", value: \"30 minutes\" },\n    { label: \"Based in\", value: \"Bristol, UK\" },\n    { label: \"Has migrated\", value: \"40+ practices\" },\n  ],\n};\n\nexport function Card31({\n  avatar,\n  name,\n  role,\n  status,\n  availability = \"free\",\n  rows = [],\n  surface = \"card\",\n  bordered = true,\n  inverted = false,\n  className,\n}: Card31Props) {\n  const surfaceTone = surfaceClasses[surface][inverted ? \"inverted\" : \"plain\"];\n\n  return (\n    <div className={cn(\"relative flex size-full items-center justify-center p-4\", className)}>\n      <div className={cn(\"w-full max-w-80 rounded-md p-5 shadow-xl\", surfaceTone, bordered && \"border border-current/15\")}>\n        <div className=\"flex items-center gap-3\">\n          {avatar && (\n            <img\n              className=\"size-12 shrink-0 rounded-full object-cover\"\n              src={avatar.src}\n              alt={avatar.alt}\n            />\n          )}\n          <div className=\"min-w-0 flex-1\">\n            {name && (\n              <p className=\"truncate text-sm font-semibold\">{name}</p>\n            )}\n            {role && <p className=\"truncate text-sm text-current/60\">{role}</p>}\n          </div>\n        </div>\n\n        {status && (\n          <p className=\"mt-3 flex items-center gap-2 text-sm\">\n            <span\n              className={cn(\"size-1.5 shrink-0 rounded-full\", dotStyles[availability])}\n              aria-hidden=\"true\"\n            />\n            {status}\n          </p>\n        )}\n\n        {rows.length > 0 && (\n          <div className=\"mt-3 flex flex-col gap-2 border-t border-current/15 pt-3\">\n            {rows.map((row, index) => (\n              <div key={index} className=\"flex items-baseline justify-between gap-3\">\n                <span className=\"shrink-0 text-sm text-current/60\">{row.label}</span>\n                <span className=\"truncate text-sm\">{row.value}</span>\n              </div>\n            ))}\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/beste/piece/card31.tsx"
    },
    {
      "path": "registry-components/badge23/badge23.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\n\ntype Tone = \"muted\" | \"foreground\" | \"primary\";\n\ninterface Badge23Props {\n  /** Eyebrow label (rendered uppercase) */\n  label: string;\n  /** Text/border tone */\n  tone?: Tone;\n  /** Additional classes merged onto the root */\n  className?: string;\n}\n\nconst toneStyles: Record<Tone, string> = {\n  muted: \"border-border text-muted-foreground\",\n  foreground: \"border-foreground/30 text-foreground\",\n  primary: \"border-primary/40 text-primary\",\n};\n\nexport const badge23Demo: Badge23Props = {\n  label: \"Product\",\n};\n\nexport function Badge23({ label, tone = \"muted\", className }: Badge23Props) {\n  return (\n    <span\n      className={cn(\n        \"inline-flex items-center rounded-md border bg-transparent px-2.5 py-1 font-mono text-sm uppercase leading-none tracking-widest\",\n        toneStyles[tone],\n        className\n      )}\n    >\n      {label}\n    </span>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/beste/component/badge23.tsx"
    },
    {
      "path": "registry-components/button21/button21.tsx",
      "content": "\"use client\";\n\nimport type { LucideIcon } from \"lucide-react\";\nimport * as React from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\ntype Tone = \"primary\" | \"neutral\" | \"outline\";\n\ninterface Button21Props {\n  /** Button label */\n  label: string;\n  /**\n   * Compose the rendered element shadcn-style (radix asChild): your element\n   * (e.g. a router Link) becomes the root and the button content is injected\n   * as its children. Pass the element without children of its own.\n   */\n  asChild?: boolean;\n  /** The element to render when `asChild` is set (e.g. your framework's Link) */\n  children?: React.ReactElement;\n  /**\n   * Fallback link: renders a plain `<a>`. Prefer `asChild` with your\n   * framework's Link component for client-side navigation.\n   */\n  href?: string;\n  /** Optional trailing icon */\n  icon?: LucideIcon;\n  /** Surface tone: solid accent (default), soft neutral pill, or hairline outline */\n  tone?: Tone;\n  /** Click handler (used when there is no href and no asChild) */\n  onClick?: React.MouseEventHandler<HTMLButtonElement>;\n  /** Additional classes merged onto the button */\n  className?: string;\n}\n\nconst toneStyles: Record<Tone, string> = {\n  primary: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n  neutral: \"bg-muted text-foreground hover:bg-muted/70\",\n  outline: \"border border-border bg-transparent text-foreground hover:bg-muted\",\n};\n\nexport const button21Demo: Button21Props = {\n  label: \"Book a demo\",\n};\n\nexport function Button21({\n  label,\n  asChild = false,\n  children,\n  href,\n  icon: Icon,\n  tone = \"primary\",\n  onClick,\n  className,\n}: Button21Props) {\n  const classes = cn(\n    \"group/button21 h-auto w-fit gap-2 rounded-md px-5 py-2.5 text-sm font-medium tracking-tight transition-colors\",\n    toneStyles[tone],\n    className\n  );\n\n  const inner = (\n    <>\n      {label}\n      {Icon && (\n        <Icon className=\"size-4 transition-transform motion-safe:group-hover/button21:translate-x-0.5\" />\n      )}\n    </>\n  );\n\n  if (asChild && React.isValidElement(children)) {\n    return (\n      <Button asChild className={classes}>\n        {React.cloneElement(children, undefined, inner)}\n      </Button>\n    );\n  }\n\n  if (href) {\n    return (\n      <Button asChild className={classes}>\n        <a href={href}>{inner}</a>\n      </Button>\n    );\n  }\n\n  return (\n    <Button type=\"button\" className={classes} onClick={onClick}>\n      {inner}\n    </Button>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/beste/component/button21.tsx"
    }
  ],
  "type": "registry:block"
}