{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "questionnaire",
  "title": "Questionnaire",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-svelte/components/questionnaire/Questionnaire.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { HTMLFormAttributes } from 'svelte/elements'\n  import type {\n    QuestionnaireAnswers,\n    QuestionnaireItemDef,\n    QuestionnaireShortcuts,\n    QuestionnaireValue,\n  } from './types'\n\n  export interface QuestionnaireProps extends HTMLFormAttributes {\n    items?: QuestionnaireItemDef[]\n    shortcuts?: QuestionnaireShortcuts\n    showProgress?: boolean\n    requiredMessage?: string\n    /** Controlled active question name (`bind:value`). Omit for uncontrolled. */\n    value?: string\n    /** Fires with the next active question name on navigation. */\n    onValueChange?: (name: string) => void\n    /** Controlled answers map (`bind:answers`). */\n    answers?: QuestionnaireAnswers\n    /** Fires with the next answers map on every answer. */\n    onAnswersChange?: (answers: QuestionnaireAnswers) => void\n    /** Fired with the answers map when the last question submits. */\n    onSubmit?: (answers: QuestionnaireAnswers) => void\n    ref?: HTMLFormElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { cn } from '$lib/utils'\n  import { Kbd } from '$lib/components/ui/kbd'\n  import { questionnaireChoiceVariants, questionnaireInputVariants } from './questionnaire.variants'\n\n  let {\n    class: className,\n    items = [],\n    shortcuts = false,\n    showProgress = true,\n    requiredMessage = 'Choose an answer to continue.',\n    value = $bindable<string | undefined>(undefined),\n    onValueChange,\n    answers = $bindable<QuestionnaireAnswers>({}),\n    onAnswersChange,\n    onSubmit,\n    ref = $bindable(null),\n    ...restProps\n  }: QuestionnaireProps = $props()\n\n  let showError = $state(false)\n\n  const names = $derived(items.map((i) => i.name))\n  const currentName = $derived(value || names[0])\n  const activeIndex = $derived(Math.max(0, names.indexOf(currentName ?? '')))\n  const current = $derived(items[activeIndex])\n  const isFirst = $derived(activeIndex <= 0)\n  const isLast = $derived(names.length === 0 || activeIndex >= names.length - 1)\n  const progress = $derived({\n    current: names.length ? activeIndex + 1 : 0,\n    total: Math.max(names.length, 1),\n  })\n\n  function 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  function setAnswer(name: string, answer: QuestionnaireValue | undefined) {\n    const next = { ...answers, [name]: answer }\n    answers = next\n    onAnswersChange?.(next)\n    showError = false\n  }\n\n  function toggleChoice(name: string, choice: string, multiple?: boolean) {\n    if (multiple) {\n      const currentVal = Array.isArray(answers[name]) ? [...(answers[name] as string[])] : []\n      const next = currentVal.includes(choice) ? currentVal.filter((v) => v !== choice) : [...currentVal, choice]\n      setAnswer(name, next)\n      return\n    }\n    setAnswer(name, choice)\n  }\n\n  function 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  function 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  function goTo(index: number) {\n    const name = names[index]\n    if (!name) return\n    value = name\n    onValueChange?.(name)\n    showError = false\n  }\n\n  function goPrev() {\n    if (!isFirst) goTo(activeIndex - 1)\n  }\n\n  function goNext() {\n    const item = current\n    if (!item) return false\n    if (item.required && !isAnswered(item.name)) {\n      showError = true\n      return false\n    }\n    showError = false\n    if (isLast) {\n      onSubmit?.({ ...answers })\n      return true\n    }\n    goTo(activeIndex + 1)\n    return true\n  }\n\n  function skip() {\n    const item = current\n    if (!item || item.required) return\n    showError = false\n    if (isLast) onSubmit?.({ ...answers })\n    else goTo(activeIndex + 1)\n  }\n\n  function 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 (!shortcuts || e.metaKey || e.ctrlKey || e.altKey) return\n    const item = current\n    const values = item?.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(item!.name, values[idx]!, item!.multiple)\n  }\n\n  function onFreeform(e: Event) {\n    const item = current\n    if (!item) return\n    setAnswer(item.name, (e.target as HTMLInputElement).value)\n  }\n</script>\n\n<form\n  {...restProps}\n  bind:this={ref}\n  data-uipkge=\"\"\n  data-slot=\"questionnaire\"\n  class={cn('flex w-full max-w-lg flex-col gap-4', className)}\n  onsubmit={(e) => {\n    e.preventDefault()\n    goNext()\n  }}\n  onkeydown={onKeydown}\n>\n  {#if 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      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        ></div>\n      </div>\n      <p class=\"text-muted-foreground text-xs\">Question {progress.current} of {progress.total}</p>\n    </div>\n  {/if}\n\n  {#if current}\n    <fieldset 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      {#if current.description}\n        <p data-slot=\"questionnaire-description\" class=\"text-muted-foreground text-sm\">\n          {current.description}\n        </p>\n      {/if}\n      {#if current.choices?.length}\n        <div data-slot=\"questionnaire-choices\" role=\"group\" class=\"flex flex-col gap-2\">\n          {#each current.choices as choice, index (choice.value)}\n            <label\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                onchange={() => 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                {#if choice.description}\n                  <span class=\"text-muted-foreground text-xs\">{choice.description}</span>\n                {/if}\n              </span>\n              {#if shortcutFor(index)}\n                <Kbd data-slot=\"questionnaire-choice-shortcut\" class=\"ml-auto\">{shortcutFor(index)}</Kbd>\n              {/if}\n            </label>\n          {/each}\n        </div>\n      {/if}\n      {#if 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] : ''}\n          class={cn(questionnaireInputVariants())}\n          oninput={onFreeform}\n        />\n      {/if}\n      {#if showError && current.required && !isAnswered(current.name)}\n        <p data-slot=\"questionnaire-error\" role=\"alert\" class=\"text-destructive text-sm\">\n          {requiredMessage}\n        </p>\n      {/if}\n    </fieldset>\n  {/if}\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      onclick={goPrev}\n    >\n      Previous\n    </button>\n    {#if current && !current.required}\n      <button\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        onclick={skip}\n      >\n        Skip\n      </button>\n    {/if}\n    {#if !isLast}\n      <button\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        onclick={goNext}\n      >\n        Next\n      </button>\n    {:else}\n      <button\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    {/if}\n  </div>\n</form>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/questionnaire/Questionnaire.svelte"
    },
    {
      "path": "packages/registry-svelte/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": "~/src/lib/components/ui/questionnaire/types.ts"
    },
    {
      "path": "packages/registry-svelte/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": "~/src/lib/components/ui/questionnaire/questionnaire.variants.ts"
    },
    {
      "path": "packages/registry-svelte/components/questionnaire/index.ts",
      "content": "export { default as Questionnaire, type QuestionnaireProps } from './Questionnaire.svelte'\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": "~/src/lib/components/ui/questionnaire/index.ts"
    }
  ],
  "dependencies": [
    "class-variance-authority"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/svelte/kbd.json"
  ],
  "description": "Single multi-step question component. Pass items, shortcuts, and showProgress — variants are props, not extra files.",
  "categories": [
    "form"
  ]
}