{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "questionnaire",
  "title": "Questionnaire",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A multi-step questionnaire with single-choice, multiple-choice, freeform and skippable questions, answer shortcuts, validation and native form data, built on shadcn's headless Questionnaire so the API is the same.",
  "dependencies": [
    "@shadcn/react@^0.3",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/button.json",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/questionnaire.tsx",
      "content": "// Ballmac UI: Questionnaire. https://ui.ballmac.com/components/questionnaire\n// Based on shadcn/ui's Questionnaire (MIT, Copyright (c) 2023 shadcn) on @shadcn/react (MIT), the same parts and props, restyled with tokens and translated action labels.\n\"use client\"\n\nimport * as React from \"react\"\nimport { Questionnaire as Primitive } from \"@shadcn/react/questionnaire\"\nimport { Check } from \"lucide-react\"\n\nimport { buttonVariants, type ButtonProps } from \"@/components/ballmac/button\"\nimport { useMessages } from \"@/lib/ballmac/i18n\"\nimport { cn } from \"@/lib/utils\"\n\n/** The form that owns the items, the active one, the answers, validation and progress. Pass `items` to render the right item and its shortcuts on the server. */\nfunction Questionnaire({ className, ...props }: React.ComponentProps<typeof Primitive.Root>) {\n  return <Primitive.Root data-slot=\"questionnaire\" className={cn(\"flex w-full min-w-0 flex-col gap-5\", className)} {...props} />\n}\n\n/** \"Question 2 of 5\", in the active item's place. It is a named progress bar for assistive technology. */\nfunction QuestionnaireProgress({ className, ...props }: React.ComponentProps<typeof Primitive.Progress>) {\n  return <Primitive.Progress data-slot=\"questionnaire-progress\" className={cn(\"min-h-[1lh] w-fit min-w-[14ch] text-sm font-medium tabular-nums text-muted-foreground\", className)} {...props} />\n}\n\n/** One question: a fieldset, so its title is the legend that names the group of answers. */\nfunction QuestionnaireItem({ className, ...props }: React.ComponentProps<typeof Primitive.Item>) {\n  return <Primitive.Item data-slot=\"questionnaire-item\" className={cn(\"min-w-0 border-0 p-0 outline-none\", className)} {...props} />\n}\n\nfunction QuestionnaireTitle({ className, ...props }: React.ComponentProps<typeof Primitive.Title>) {\n  return <Primitive.Title data-slot=\"questionnaire-title\" className={cn(\"text-pretty text-lg font-semibold leading-snug tracking-tight\", className)} {...props} />\n}\n\nfunction QuestionnaireDescription({ className, ...props }: React.ComponentProps<typeof Primitive.Description>) {\n  return <Primitive.Description data-slot=\"questionnaire-description\" className={cn(\"mt-1 text-pretty text-sm text-muted-foreground\", className)} {...props} />\n}\n\n/** The answers of one question. */\nfunction QuestionnaireChoices({ className, ...props }: React.ComponentProps<typeof Primitive.Choices>) {\n  return <Primitive.Choices data-slot=\"questionnaire-choices\" className={cn(\"group/questionnaire-choices mt-4 grid min-w-0 gap-2\", className)} {...props} />\n}\n\n/**\n * One fixed answer: a real radio button or checkbox that the label covers, so the keyboard, the arrow keys and screen readers behave as the browser\n * does. Put the answer's words in it, and a `QuestionnaireChoiceDescription` for a second line.\n */\nfunction QuestionnaireChoice({ children, className, ...props }: React.ComponentProps<typeof Primitive.Choice>) {\n  return (\n    <Primitive.Choice\n      data-slot=\"questionnaire-choice\"\n      className={cn(\n        \"group/questionnaire-choice relative flex min-h-11 cursor-pointer select-none items-start gap-3 rounded-lg border bg-background px-3 py-2.5 text-start text-sm shadow-xs outline-none transition-[border-color,background-color,box-shadow] duration-150 motion-reduce:transition-none\",\n        \"hover:bg-accent/50 has-[:focus-visible]:border-ring has-[:focus-visible]:ring-[3px] has-[:focus-visible]:ring-ring/50\",\n        \"data-checked:border-primary data-checked:bg-primary/5 data-invalid:border-destructive/60\",\n        \"data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-disabled:opacity-50\",\n        className\n      )}\n      {...props}\n    >\n      <Primitive.ChoiceInput data-slot=\"questionnaire-choice-input\" className=\"absolute inset-0 z-10 size-full cursor-pointer opacity-0\" />\n      <span\n        aria-hidden=\"true\"\n        data-slot=\"questionnaire-choice-indicator\"\n        className=\"pointer-events-none relative mt-0.5 flex size-4 shrink-0 items-center justify-center border border-input bg-background text-primary-foreground transition-colors group-data-[type=radio]/questionnaire-choice:rounded-full group-data-[type=checkbox]/questionnaire-choice:rounded-[4px] group-data-checked/questionnaire-choice:border-primary group-data-checked/questionnaire-choice:bg-primary motion-reduce:transition-none\"\n      >\n        <span className=\"hidden size-1.5 rounded-full bg-primary-foreground group-data-[type=radio]/questionnaire-choice:group-data-checked/questionnaire-choice:block\" />\n        <Check className=\"hidden size-3 group-data-[type=checkbox]/questionnaire-choice:group-data-checked/questionnaire-choice:block\" strokeWidth={3} />\n      </span>\n      <Primitive.ChoiceLabel data-slot=\"questionnaire-choice-label\" className=\"flex min-w-0 flex-1 flex-col gap-0.5 leading-snug\">\n        {children}\n      </Primitive.ChoiceLabel>\n      <Primitive.ChoiceShortcut\n        data-slot=\"questionnaire-choice-shortcut\"\n        className=\"pointer-events-none ms-auto hidden shrink-0 rounded border bg-muted px-1.5 font-mono text-[11px] uppercase text-muted-foreground group-data-[shortcut]/questionnaire-choice:inline-flex\"\n      />\n    </Primitive.Choice>\n  )\n}\n\n/** A second line inside a choice. */\nfunction QuestionnaireChoiceDescription({ className, ...props }: React.ComponentProps<\"span\">) {\n  return <span data-slot=\"questionnaire-choice-description\" className={cn(\"text-muted-foreground\", className)} {...props} />\n}\n\n/** A text answer next to the fixed ones. Always give it a name with a label, `aria-label` or `aria-labelledby`: a placeholder is not one. */\nfunction QuestionnaireInput({ className, ...props }: React.ComponentProps<typeof Primitive.Input>) {\n  return (\n    <div data-slot=\"questionnaire-input-wrapper\" className=\"group/questionnaire-input relative min-w-0\">\n      <Primitive.Input\n        data-slot=\"questionnaire-input\"\n        className={cn(\n          \"min-h-11 w-full min-w-0 rounded-lg border border-input bg-background px-3 py-2 text-sm shadow-xs outline-none transition-[color,border-color,box-shadow] duration-150 placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground\",\n          \"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 motion-reduce:transition-none sm:min-h-10\",\n          className\n        )}\n        {...props}\n      />\n    </div>\n  )\n}\n\n/** The message when an answer is required, or refused. It is tied to the item, and shown only while the item is invalid. */\nfunction QuestionnaireError({ className, ...props }: React.ComponentProps<typeof Primitive.Error>) {\n  return <Primitive.Error data-slot=\"questionnaire-error\" className={cn(\"mt-2 text-sm text-destructive\", className)} {...props} />\n}\n\n/** The row of buttons: Previous at the start, then Skip, then Next or Submit at the end. */\nfunction QuestionnaireActions({ className, ...props }: React.ComponentProps<\"div\">) {\n  return <div data-slot=\"questionnaire-actions\" className={cn(\"grid min-h-11 w-full grid-cols-[minmax(0,1fr)_auto_auto] items-center gap-2\", className)} {...props} />\n}\n\ntype ActionStyle = Pick<ButtonProps, \"size\" | \"variant\">\n\nfunction QuestionnairePrevious({ children, className, size = \"default\", variant = \"outline\", ...props }: React.ComponentProps<typeof Primitive.Previous> & ActionStyle) {\n  const msg = useMessages()\n  return (\n    <Primitive.Previous data-slot=\"questionnaire-previous\" data-size={size} data-variant={variant} className={cn(buttonVariants({ size, variant }), \"col-start-1 row-start-1 min-h-11 justify-self-start sm:min-h-0\", className)} {...props}>\n      {children ?? msg(\"questionnaire.previous\", \"Previous\")}\n    </Primitive.Previous>\n  )\n}\n\nfunction QuestionnaireSkip({ children, className, size = \"default\", variant = \"ghost\", ...props }: React.ComponentProps<typeof Primitive.Skip> & ActionStyle) {\n  const msg = useMessages()\n  return (\n    <Primitive.Skip data-slot=\"questionnaire-skip\" data-size={size} data-variant={variant} className={cn(buttonVariants({ size, variant }), \"col-start-2 row-start-1 min-h-11 justify-self-end sm:min-h-0\", className)} {...props}>\n      {children ?? msg(\"questionnaire.skip\", \"Skip\")}\n    </Primitive.Skip>\n  )\n}\n\nfunction QuestionnaireNext({ children, className, size = \"default\", variant = \"default\", ...props }: React.ComponentProps<typeof Primitive.Next> & ActionStyle) {\n  const msg = useMessages()\n  return (\n    <Primitive.Next data-slot=\"questionnaire-next\" data-size={size} data-variant={variant} className={cn(buttonVariants({ size, variant }), \"col-start-3 row-start-1 min-h-11 justify-self-end sm:min-h-0\", className)} {...props}>\n      {children ?? msg(\"questionnaire.next\", \"Next\")}\n    </Primitive.Next>\n  )\n}\n\nfunction QuestionnaireSubmit({ children, className, size = \"default\", variant = \"default\", ...props }: React.ComponentProps<typeof Primitive.Submit> & ActionStyle) {\n  const msg = useMessages()\n  return (\n    <Primitive.Submit data-slot=\"questionnaire-submit\" data-size={size} data-variant={variant} className={cn(buttonVariants({ size, variant }), \"col-start-3 row-start-1 min-h-11 justify-self-end sm:min-h-0\", className)} {...props}>\n      {children ?? msg(\"questionnaire.submit\", \"Submit\")}\n    </Primitive.Submit>\n  )\n}\n\nexport {\n  Questionnaire,\n  QuestionnaireActions,\n  QuestionnaireChoice,\n  QuestionnaireChoiceDescription,\n  QuestionnaireChoices,\n  QuestionnaireDescription,\n  QuestionnaireError,\n  QuestionnaireInput,\n  QuestionnaireItem,\n  QuestionnaireNext,\n  QuestionnairePrevious,\n  QuestionnaireProgress,\n  QuestionnaireSkip,\n  QuestionnaireSubmit,\n  QuestionnaireTitle,\n}\n",
      "type": "registry:ui",
      "target": "@components/ballmac/questionnaire.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "questionnaire",
      "form",
      "wizard",
      "agent",
      "choices",
      "shortcuts",
      "shadcn"
    ],
    "version": "1.0.0",
    "updated": "2026-10-19",
    "tests": true,
    "ai": {
      "summary": "<Questionnaire items onSubmit> with a <QuestionnaireItem name required> per question: Title, Description, Choices holding <QuestionnaireChoice value> and optionally <QuestionnaireInput aria-label>, and an Error. Add <QuestionnaireActions> with Previous, Skip, Next and Submit. Answers are native FormData.",
      "whenToUse": [
        "An AI agent asking the person to choose between options before it continues",
        "Onboarding and intake steps with a few questions",
        "Surveys where answers become form data"
      ],
      "whenNotToUse": [
        "One question inside a form; use radio-group or checkbox",
        "A long form with many field types; use form blocks",
        "Quizzes that score answers; compute that yourself from the form data"
      ],
      "composesWith": [
        "card",
        "dialog",
        "trace",
        "button"
      ],
      "a11y": [
        {
          "keys": "Tab / arrow keys",
          "action": "Native radio and checkbox behavior in each question"
        },
        {
          "keys": "Letter or number keys",
          "action": "With shortcuts set, choose the answer with that key; it does not advance"
        },
        {
          "keys": "Enter",
          "action": "Next on a question, Submit on the last"
        },
        {
          "keys": "Screen readers",
          "action": "Each question is a fieldset named by its legend; descriptions and errors are linked; progress is a named progress bar; inactive questions are inert"
        },
        {
          "keys": "After a failed step",
          "action": "Focus goes to an answer of the question that needs one, and its error is announced"
        }
      ],
      "customization": [
        "items to define the order, shortcuts, and server-rendered progress",
        "multiple on an item; QuestionnaireInput for a freeform answer",
        "item and onItemChange to control the active question",
        "invalid on an item with your own message in QuestionnaireError",
        "size and variant on the action buttons"
      ]
    },
    "statesNone": "It collects answers and hands them to onSubmit as form data; saving them, and any loading or failure around that, belongs to the page that owns the form.",
    "source": {
      "name": "shadcn/ui Questionnaire",
      "url": "https://github.com/shadcn-ui/ui",
      "license": "MIT",
      "copyright": "Copyright (c) 2023 shadcn",
      "modified": true
    },
    "examples": [
      "questionnaire-demo",
      "questionnaire-multiple",
      "questionnaire-shortcuts",
      "questionnaire-skip",
      "questionnaire-validation",
      "questionnaire-card"
    ],
    "url": "https://ui.ballmac.com/components/questionnaire"
  },
  "categories": [
    "ai"
  ],
  "type": "registry:ui"
}