{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "questionnaire",
  "title": "Questionnaire",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-react/components/questionnaire/questionnaire.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport { cn } from '@/lib/utils'\nimport { Kbd } from '@/components/ui/kbd'\nimport { questionnaireChoiceVariants, questionnaireInputVariants } from './questionnaire.variants'\nimport type { QuestionnaireAnswers, QuestionnaireItemDef, QuestionnaireShortcuts, QuestionnaireValue } from './types'\n\nexport interface QuestionnaireProps extends Omit<React.FormHTMLAttributes<HTMLFormElement>, 'onSubmit'> {\n  items?: QuestionnaireItemDef[]\n  shortcuts?: QuestionnaireShortcuts\n  showProgress?: boolean\n  requiredMessage?: string\n  value?: string\n  defaultValue?: string\n  onValueChange?: (name: string) => void\n  answers?: QuestionnaireAnswers\n  defaultAnswers?: QuestionnaireAnswers\n  onAnswersChange?: (answers: QuestionnaireAnswers) => void\n  onSubmit?: (answers: QuestionnaireAnswers) => void\n}\n\nfunction Questionnaire({\n  items = [],\n  shortcuts = false,\n  showProgress = true,\n  requiredMessage = 'Choose an answer to continue.',\n  value,\n  defaultValue,\n  onValueChange,\n  answers: answersProp,\n  defaultAnswers,\n  onAnswersChange,\n  onSubmit,\n  className,\n  ...props\n}: QuestionnaireProps) {\n  const [uncontrolledName, setUncontrolledName] = React.useState(defaultValue)\n  const [uncontrolledAnswers, setUncontrolledAnswers] = React.useState<QuestionnaireAnswers>(defaultAnswers ?? {})\n  const [showError, setShowError] = React.useState(false)\n\n  const names = items.map((i) => i.name)\n  const answers = answersProp ?? uncontrolledAnswers\n  const currentName = value ?? uncontrolledName ?? names[0]\n  const activeIndex = Math.max(0, names.indexOf(currentName ?? ''))\n  const current = items[activeIndex]\n  const isFirst = activeIndex <= 0\n  const isLast = names.length === 0 || activeIndex >= names.length - 1\n  const progress = { current: names.length ? activeIndex + 1 : 0, total: Math.max(names.length, 1) }\n\n  const setActive = (name: string) => {\n    onValueChange?.(name)\n    if (value === undefined) setUncontrolledName(name)\n    setShowError(false)\n  }\n\n  const writeAnswers = (next: QuestionnaireAnswers) => {\n    onAnswersChange?.(next)\n    if (answersProp === undefined) setUncontrolledAnswers(next)\n  }\n\n  const isAnswered = (name: string) => {\n    const v = answers[name]\n    if (Array.isArray(v)) return v.length > 0\n    return typeof v === 'string' && v.trim().length > 0\n  }\n\n  const setAnswer = (name: string, next: QuestionnaireValue | undefined) => {\n    writeAnswers({ ...answers, [name]: next })\n    setShowError(false)\n  }\n\n  const toggleChoice = (name: string, choice: string, multiple?: boolean) => {\n    if (multiple) {\n      const cur = Array.isArray(answers[name]) ? [...(answers[name] as string[])] : []\n      setAnswer(name, cur.includes(choice) ? cur.filter((v) => v !== choice) : [...cur, choice])\n      return\n    }\n    setAnswer(name, choice)\n  }\n\n  const shortcutFor = (index: number) => {\n    if (!shortcuts) return\n    if (shortcuts === 'letters' && index < 26) return String.fromCharCode(65 + index)\n    if (shortcuts === 'numbers' && index < 9) return String(index + 1)\n  }\n\n  const choiceChecked = (name: string, choice: string) => {\n    const v = answers[name]\n    if (Array.isArray(v)) return v.includes(choice)\n    return v === choice\n  }\n\n  const goTo = (index: number) => {\n    const name = names[index]\n    if (name) setActive(name)\n  }\n\n  const goNext = () => {\n    if (!current) return false\n    if (current.required && !isAnswered(current.name)) {\n      setShowError(true)\n      return false\n    }\n    setShowError(false)\n    if (isLast) {\n      onSubmit?.(answers)\n      return true\n    }\n    goTo(activeIndex + 1)\n    return true\n  }\n\n  const skip = () => {\n    if (!current || current.required) return\n    setShowError(false)\n    if (isLast) onSubmit?.(answers)\n    else goTo(activeIndex + 1)\n  }\n\n  return (\n    <form\n      data-uipkge=\"\"\n      data-slot=\"questionnaire\"\n      className={cn('flex w-full max-w-lg flex-col gap-4', className)}\n      onSubmit={(e) => {\n        e.preventDefault()\n        goNext()\n      }}\n      onKeyDown={(e) => {\n        if (e.defaultPrevented) return\n        const target = e.target as HTMLElement\n        const inField = target.closest('input, textarea, select')\n        if (inField) {\n          if (e.key === 'Enter' && target.tagName === 'INPUT') {\n            e.preventDefault()\n            goNext()\n          }\n          return\n        }\n        if (e.key === 'Enter') {\n          e.preventDefault()\n          goNext()\n          return\n        }\n        if (!shortcuts || e.metaKey || e.ctrlKey || e.altKey || !current) return\n        const values = current.choices?.map((c) => c.value) ?? []\n        let idx = -1\n        if (shortcuts === 'letters') idx = e.key.toLowerCase().charCodeAt(0) - 97\n        if (shortcuts === 'numbers') idx = Number(e.key) - 1\n        if (idx < 0 || idx >= values.length) return\n        e.preventDefault()\n        toggleChoice(current.name, values[idx], current.multiple)\n      }}\n      {...props}\n    >\n      {showProgress ? (\n        <div\n          data-slot=\"questionnaire-progress\"\n          role=\"progressbar\"\n          aria-valuemin={1}\n          aria-valuenow={progress.current}\n          aria-valuemax={progress.total}\n          aria-label={`Question ${progress.current} of ${progress.total}`}\n          className=\"flex flex-col gap-2\"\n        >\n          <div className=\"bg-muted h-1 w-full overflow-hidden rounded-full\">\n            <div\n              className=\"bg-primary h-full rounded-full transition-[width] duration-200\"\n              style={{ width: `${(progress.current / progress.total) * 100}%` }}\n            />\n          </div>\n          <p className=\"text-muted-foreground text-xs\">\n            Question {progress.current} of {progress.total}\n          </p>\n        </div>\n      ) : null}\n\n      {current ? (\n        <fieldset data-slot=\"questionnaire-item\" name={current.name} className=\"flex min-w-0 flex-col gap-3\">\n          <legend data-slot=\"questionnaire-title\" className=\"text-foreground text-base font-medium\">\n            {current.prompt}\n          </legend>\n          {current.description ? (\n            <p data-slot=\"questionnaire-description\" className=\"text-muted-foreground text-sm\">\n              {current.description}\n            </p>\n          ) : null}\n          {current.choices?.length ? (\n            <div data-slot=\"questionnaire-choices\" role=\"group\" className=\"flex flex-col gap-2\">\n              {current.choices.map((choice, index) => (\n                <label\n                  key={choice.value}\n                  data-slot=\"questionnaire-choice\"\n                  data-checked={choiceChecked(current.name, choice.value) ? '' : undefined}\n                  className={cn(questionnaireChoiceVariants(), choice.disabled && 'pointer-events-none opacity-50')}\n                >\n                  <input\n                    className=\"sr-only\"\n                    type={current.multiple ? 'checkbox' : 'radio'}\n                    name={current.name}\n                    value={choice.value}\n                    checked={choiceChecked(current.name, choice.value)}\n                    disabled={choice.disabled}\n                    onChange={() => toggleChoice(current.name, choice.value, current.multiple)}\n                  />\n                  <span className=\"flex min-w-0 flex-1 flex-col\">\n                    <span className=\"font-medium\">{choice.label}</span>\n                    {choice.description ? (\n                      <span className=\"text-muted-foreground text-xs\">{choice.description}</span>\n                    ) : null}\n                  </span>\n                  {shortcutFor(index) ? (\n                    <Kbd data-slot=\"questionnaire-choice-shortcut\" className=\"ml-auto\">\n                      {shortcutFor(index)}\n                    </Kbd>\n                  ) : null}\n                </label>\n              ))}\n            </div>\n          ) : null}\n          {current.input ? (\n            <input\n              data-slot=\"questionnaire-input\"\n              type=\"text\"\n              name={current.name}\n              placeholder={current.input.placeholder}\n              value={typeof answers[current.name] === 'string' ? (answers[current.name] as string) : ''}\n              className={cn(questionnaireInputVariants())}\n              onChange={(e) => setAnswer(current.name, e.target.value)}\n            />\n          ) : null}\n          {showError && current.required && !isAnswered(current.name) ? (\n            <p data-slot=\"questionnaire-error\" role=\"alert\" className=\"text-destructive text-sm\">\n              {requiredMessage}\n            </p>\n          ) : null}\n        </fieldset>\n      ) : null}\n\n      <div data-slot=\"questionnaire-actions\" className=\"flex flex-wrap items-center gap-2\">\n        <button\n          type=\"button\"\n          data-slot=\"questionnaire-previous\"\n          className=\"border-border bg-background hover:bg-accent inline-flex h-9 items-center rounded-md border px-4 text-sm font-medium disabled:opacity-50\"\n          disabled={isFirst}\n          onClick={() => {\n            if (!isFirst) goTo(activeIndex - 1)\n          }}\n        >\n          Previous\n        </button>\n        {current && !current.required ? (\n          <button\n            type=\"button\"\n            data-slot=\"questionnaire-skip\"\n            className=\"hover:bg-accent inline-flex h-9 items-center rounded-md px-4 text-sm font-medium\"\n            onClick={skip}\n          >\n            Skip\n          </button>\n        ) : null}\n        {!isLast ? (\n          <button\n            type=\"button\"\n            data-slot=\"questionnaire-next\"\n            className=\"bg-primary text-primary-foreground hover:bg-primary/90 inline-flex h-9 items-center rounded-md px-4 text-sm font-medium\"\n            onClick={goNext}\n          >\n            Next\n          </button>\n        ) : (\n          <button\n            type=\"submit\"\n            data-slot=\"questionnaire-submit\"\n            className=\"bg-primary text-primary-foreground hover:bg-primary/90 inline-flex h-9 items-center rounded-md px-4 text-sm font-medium\"\n          >\n            Submit\n          </button>\n        )}\n      </div>\n    </form>\n  )\n}\n\nexport { Questionnaire }\n",
      "type": "registry:ui",
      "target": "~/components/ui/questionnaire/questionnaire.tsx"
    },
    {
      "path": "packages/registry-react/components/questionnaire/types.ts",
      "content": "export type QuestionnaireValue = string | string[]\n\nexport type QuestionnaireAnswers = Record<string, QuestionnaireValue | undefined>\n\nexport interface QuestionnaireChoiceDef {\n  value: string\n  label: string\n  description?: string\n  disabled?: boolean\n}\n\nexport interface QuestionnaireItemDef {\n  name: string\n  prompt: string\n  description?: string\n  required?: boolean\n  multiple?: boolean\n  choices?: QuestionnaireChoiceDef[]\n  input?: { label?: string; placeholder?: string }\n}\n\nexport type QuestionnaireShortcuts = 'letters' | 'numbers' | false\n",
      "type": "registry:ui",
      "target": "~/components/ui/questionnaire/types.ts"
    },
    {
      "path": "packages/registry-react/components/questionnaire/questionnaire.variants.ts",
      "content": "import type { VariantProps } from 'class-variance-authority'\nimport { cva } from 'class-variance-authority'\n\n/**\n * Variant definitions live in their own file (rather than the package\n * `index.ts`) so consuming Vue SFCs can import without creating a circular\n * dependency through the index. See card.variants.ts for the canonical\n * example + the SSR symptom that motivated the split.\n */\n\nexport const questionnaireChoiceVariants = cva(\n  'border-border bg-card hover:bg-accent/50 has-[:focus-visible]:border-ring has-[:focus-visible]:ring-ring/50 has-[:checked]:border-primary has-[:checked]:bg-accent flex cursor-pointer items-start gap-3 rounded-lg border p-3 text-left text-sm transition-colors has-[:disabled]:pointer-events-none has-[:disabled]:opacity-50 has-[:focus-visible]:ring-[3px]',\n)\n\nexport const questionnaireInputVariants = cva(\n  'border-input bg-background placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 h-9 w-full rounded-md border px-3 text-sm outline-none focus-visible:ring-[3px]',\n)\n\nexport type QuestionnaireChoiceVariants = VariantProps<typeof questionnaireChoiceVariants>\n",
      "type": "registry:ui",
      "target": "~/components/ui/questionnaire/questionnaire.variants.ts"
    },
    {
      "path": "packages/registry-react/components/questionnaire/index.ts",
      "content": "export { Questionnaire, type QuestionnaireProps } from './questionnaire'\nexport { questionnaireChoiceVariants, questionnaireInputVariants } from './questionnaire.variants'\nexport type {\n  QuestionnaireItemDef,\n  QuestionnaireChoiceDef,\n  QuestionnaireAnswers,\n  QuestionnaireValue,\n  QuestionnaireShortcuts,\n} from './types'\n",
      "type": "registry:ui",
      "target": "~/components/ui/questionnaire/index.ts"
    }
  ],
  "dependencies": [
    "class-variance-authority"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/react/kbd.json"
  ],
  "description": "Single multi-step question component. Pass items, shortcuts, and showProgress — variants are props, not extra files.",
  "categories": [
    "form"
  ]
}