{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "questionnaire",
  "title": "Questionnaire",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-vue/components/questionnaire/Questionnaire.vue",
      "content": "<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport type { HTMLAttributes } from 'vue'\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\nconst props = withDefaults(\n  defineProps<{\n    items?: QuestionnaireItemDef[]\n    shortcuts?: QuestionnaireShortcuts\n    showProgress?: boolean\n    requiredMessage?: string\n    class?: HTMLAttributes['class']\n  }>(),\n  {\n    items: () => [],\n    shortcuts: false,\n    showProgress: true,\n    requiredMessage: 'Choose an answer to continue.',\n  },\n)\n\nconst emit = defineEmits<{\n  submit: [answers: QuestionnaireAnswers]\n}>()\n\nconst activeName = defineModel<string>('modelValue')\nconst answers = defineModel<QuestionnaireAnswers>('answers', { default: () => ({}) })\nconst showError = ref(false)\n\nconst names = computed(() => props.items.map((i) => i.name))\nconst currentName = computed(() => activeName.value || names.value[0])\nconst activeIndex = computed(() => Math.max(0, names.value.indexOf(currentName.value ?? '')))\nconst current = computed(() => props.items[activeIndex.value])\nconst isFirst = computed(() => activeIndex.value <= 0)\nconst isLast = computed(() => names.value.length === 0 || activeIndex.value >= names.value.length - 1)\nconst progress = computed(() => ({\n  current: names.value.length ? activeIndex.value + 1 : 0,\n  total: Math.max(names.value.length, 1),\n}))\n\nfunction isAnswered(name: string) {\n  const v = answers.value[name]\n  if (Array.isArray(v)) return v.length > 0\n  return typeof v === 'string' && v.trim().length > 0\n}\n\nfunction setAnswer(name: string, value: QuestionnaireValue | undefined) {\n  answers.value = { ...answers.value, [name]: value }\n  showError.value = false\n}\n\nfunction toggleChoice(name: string, value: string, multiple?: boolean) {\n  if (multiple) {\n    const currentVal = Array.isArray(answers.value[name]) ? [...(answers.value[name] as string[])] : []\n    const next = currentVal.includes(value) ? currentVal.filter((v) => v !== value) : [...currentVal, value]\n    setAnswer(name, next)\n    return\n  }\n  setAnswer(name, value)\n}\n\nfunction shortcutFor(index: number) {\n  if (!props.shortcuts) return\n  if (props.shortcuts === 'letters' && index < 26) return String.fromCharCode(65 + index)\n  if (props.shortcuts === 'numbers' && index < 9) return String(index + 1)\n}\n\nfunction choiceChecked(name: string, value: string) {\n  const v = answers.value[name]\n  if (Array.isArray(v)) return v.includes(value)\n  return v === value\n}\n\nfunction goTo(index: number) {\n  const name = names.value[index]\n  if (!name) return\n  activeName.value = name\n  showError.value = false\n}\n\nfunction goPrev() {\n  if (!isFirst.value) goTo(activeIndex.value - 1)\n}\n\nfunction goNext() {\n  const item = current.value\n  if (!item) return false\n  if (item.required && !isAnswered(item.name)) {\n    showError.value = true\n    return false\n  }\n  showError.value = false\n  if (isLast.value) {\n    emit('submit', { ...answers.value })\n    return true\n  }\n  goTo(activeIndex.value + 1)\n  return true\n}\n\nfunction skip() {\n  const item = current.value\n  if (!item || item.required) return\n  showError.value = false\n  if (isLast.value) emit('submit', { ...answers.value })\n  else goTo(activeIndex.value + 1)\n}\n\nfunction onKeydown(e: KeyboardEvent) {\n  if (e.defaultPrevented) return\n  const target = e.target as HTMLElement | null\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 (!props.shortcuts || e.metaKey || e.ctrlKey || e.altKey) return\n  const item = current.value\n  const values = item?.choices?.map((c) => c.value) ?? []\n  let idx = -1\n  if (props.shortcuts === 'letters') idx = e.key.toLowerCase().charCodeAt(0) - 97\n  if (props.shortcuts === 'numbers') idx = Number(e.key) - 1\n  if (idx < 0 || idx >= values.length) return\n  e.preventDefault()\n  toggleChoice(item.name, values[idx], item.multiple)\n}\n\nfunction onFreeform(e: Event) {\n  const item = current.value\n  if (!item) return\n  setAnswer(item.name, (e.target as HTMLInputElement).value)\n}\n</script>\n\n<template>\n  <form\n    data-uipkge\n    data-slot=\"questionnaire\"\n    :class=\"cn('flex w-full max-w-lg flex-col gap-4', props.class)\"\n    @submit.prevent=\"goNext\"\n    @keydown=\"onKeydown\"\n  >\n    <div\n      v-if=\"showProgress\"\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      class=\"flex flex-col gap-2\"\n    >\n      <div class=\"bg-muted h-1 w-full overflow-hidden rounded-full\">\n        <div\n          class=\"bg-primary h-full rounded-full transition-[width] duration-200\"\n          :style=\"{ width: `${(progress.current / progress.total) * 100}%` }\"\n        />\n      </div>\n      <p class=\"text-muted-foreground text-xs\">Question {{ progress.current }} of {{ progress.total }}</p>\n    </div>\n\n    <fieldset v-if=\"current\" data-slot=\"questionnaire-item\" :name=\"current.name\" class=\"flex min-w-0 flex-col gap-3\">\n      <legend data-slot=\"questionnaire-title\" class=\"text-foreground text-base font-medium\">\n        {{ current.prompt }}\n      </legend>\n      <p v-if=\"current.description\" data-slot=\"questionnaire-description\" class=\"text-muted-foreground text-sm\">\n        {{ current.description }}\n      </p>\n      <div v-if=\"current.choices?.length\" data-slot=\"questionnaire-choices\" role=\"group\" class=\"flex flex-col gap-2\">\n        <label\n          v-for=\"(choice, index) in current.choices\"\n          :key=\"choice.value\"\n          data-slot=\"questionnaire-choice\"\n          :data-checked=\"choiceChecked(current.name, choice.value) ? '' : undefined\"\n          :class=\"cn(questionnaireChoiceVariants(), choice.disabled && 'pointer-events-none opacity-50')\"\n        >\n          <input\n            class=\"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            @change=\"toggleChoice(current.name, choice.value, current.multiple)\"\n          />\n          <span class=\"flex min-w-0 flex-1 flex-col\">\n            <span class=\"font-medium\">{{ choice.label }}</span>\n            <span v-if=\"choice.description\" class=\"text-muted-foreground text-xs\">{{ choice.description }}</span>\n          </span>\n          <Kbd v-if=\"shortcutFor(index)\" data-slot=\"questionnaire-choice-shortcut\" class=\"ml-auto\">{{\n            shortcutFor(index)\n          }}</Kbd>\n        </label>\n      </div>\n      <input\n        v-if=\"current.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] : ''\"\n        :class=\"cn(questionnaireInputVariants())\"\n        @input=\"onFreeform\"\n      />\n      <p\n        v-if=\"showError && current.required && !isAnswered(current.name)\"\n        data-slot=\"questionnaire-error\"\n        role=\"alert\"\n        class=\"text-destructive text-sm\"\n      >\n        {{ requiredMessage }}\n      </p>\n    </fieldset>\n\n    <div data-slot=\"questionnaire-actions\" class=\"flex flex-wrap items-center gap-2\">\n      <button\n        type=\"button\"\n        data-slot=\"questionnaire-previous\"\n        class=\"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        @click=\"goPrev\"\n      >\n        Previous\n      </button>\n      <button\n        v-if=\"current && !current.required\"\n        type=\"button\"\n        data-slot=\"questionnaire-skip\"\n        class=\"hover:bg-accent inline-flex h-9 items-center rounded-md px-4 text-sm font-medium\"\n        @click=\"skip\"\n      >\n        Skip\n      </button>\n      <button\n        v-if=\"!isLast\"\n        type=\"button\"\n        data-slot=\"questionnaire-next\"\n        class=\"bg-primary text-primary-foreground hover:bg-primary/90 inline-flex h-9 items-center rounded-md px-4 text-sm font-medium\"\n        @click=\"goNext\"\n      >\n        Next\n      </button>\n      <button\n        v-else\n        type=\"submit\"\n        data-slot=\"questionnaire-submit\"\n        class=\"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    </div>\n  </form>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/questionnaire/Questionnaire.vue"
    },
    {
      "path": "packages/registry-vue/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": "~/app/components/ui/questionnaire/types.ts"
    },
    {
      "path": "packages/registry-vue/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": "~/app/components/ui/questionnaire/questionnaire.variants.ts"
    },
    {
      "path": "packages/registry-vue/components/questionnaire/index.ts",
      "content": "export { default as Questionnaire } from './Questionnaire.vue'\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": "~/app/components/ui/questionnaire/index.ts"
    }
  ],
  "dependencies": [
    "class-variance-authority"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/vue/kbd.json"
  ],
  "description": "Single multi-step question component. Pass items, shortcuts, and showProgress — variants are props, not extra files.",
  "categories": [
    "form"
  ]
}