{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "questionnaire",
  "title": "Questionnaire",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/questionnaire/questionnaire.component.ts",
      "content": "import { Component, EventEmitter, Input, Output, ChangeDetectionStrategy } from '@angular/core'\nimport { cn } from '@/lib/utils'\nimport { questionnaireChoiceVariants, questionnaireInputVariants } from './questionnaire.variants'\nimport type { QuestionnaireAnswers, QuestionnaireItemDef, QuestionnaireShortcuts, QuestionnaireValue } from './types'\n\n/**\n * Angular port of UIPKGE Questionnaire. Single multi-step question\n * component: items, shortcuts, showProgress — variants are inputs, not\n * extra files. Step state (active/answers/error) mirrors the Vue source.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-questionnaire, [ui-questionnaire]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"questionnaire\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[class]': 'hostClass',\n    '(keydown)': 'onKeydown($event)',\n  },\n  template: `\n    <form class=\"contents\" (submit)=\"onFormSubmit($event)\">\n      @if (showProgress) {\n        <div\n          data-uipkge\n          data-slot=\"questionnaire-progress\"\n          role=\"progressbar\"\n          [attr.aria-valuemin]=\"1\"\n          [attr.aria-valuenow]=\"progress.current\"\n          [attr.aria-valuemax]=\"progress.total\"\n          [attr.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.%]=\"progressPercent\"\n            ></div>\n          </div>\n          <p class=\"text-muted-foreground text-xs\">Question {{ progress.current }} of {{ progress.total }}</p>\n        </div>\n      }\n\n      @if (current) {\n        <fieldset\n          data-uipkge\n          data-slot=\"questionnaire-item\"\n          [attr.name]=\"current.name\"\n          class=\"flex min-w-0 flex-col gap-3\"\n        >\n          <legend data-uipkge data-slot=\"questionnaire-title\" class=\"text-foreground text-base font-medium\">\n            {{ current.prompt }}\n          </legend>\n          @if (current.description) {\n            <p data-uipkge data-slot=\"questionnaire-description\" class=\"text-muted-foreground text-sm\">\n              {{ current.description }}\n            </p>\n          }\n          @if (current.choices?.length) {\n            <div data-uipkge data-slot=\"questionnaire-choices\" role=\"group\" class=\"flex flex-col gap-2\">\n              @for (choice of current.choices; track choice.value) {\n                <label\n                  data-uipkge\n                  data-slot=\"questionnaire-choice\"\n                  [attr.data-checked]=\"choiceChecked(current.name, choice.value) ? '' : null\"\n                  [class]=\"getChoiceClass(choice)\"\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 ?? false\"\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                    @if (choice.description) {\n                      <span class=\"text-muted-foreground text-xs\">{{ choice.description }}</span>\n                    }\n                  </span>\n                  @if (shortcutFor($index)) {\n                    <kbd\n                      data-uipkge\n                      data-slot=\"questionnaire-choice-shortcut\"\n                      class=\"bg-muted text-muted-foreground pointer-events-none ml-auto inline-flex h-5 items-center gap-1 rounded border px-1.5 font-mono text-[10px] font-medium opacity-100 select-none\"\n                    >\n                      {{ shortcutFor($index) }}\n                    </kbd>\n                  }\n                </label>\n              }\n            </div>\n          }\n          @if (current.input) {\n            <input\n              data-uipkge\n              data-slot=\"questionnaire-input\"\n              type=\"text\"\n              [name]=\"current.name\"\n              [placeholder]=\"current.input.placeholder ?? ''\"\n              [value]=\"inputValue(current.name)\"\n              [class]=\"inputClass()\"\n              (input)=\"onInputChange(current.name, $event)\"\n            />\n          }\n          @if (showError && current.required && !isAnswered(current.name)) {\n            <p data-uipkge data-slot=\"questionnaire-error\" role=\"alert\" class=\"text-destructive text-sm\">\n              {{ requiredMessage }}\n            </p>\n          }\n        </fieldset>\n      }\n\n      <div data-uipkge data-slot=\"questionnaire-actions\" class=\"flex flex-wrap items-center gap-2\">\n        <button\n          type=\"button\"\n          data-uipkge\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        @if (current && !current.required) {\n          <button\n            type=\"button\"\n            data-uipkge\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        }\n        @if (!isLast) {\n          <button\n            type=\"button\"\n            data-uipkge\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        } @else {\n          <button\n            type=\"button\"\n            data-uipkge\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            (click)=\"goNext()\"\n          >\n            Submit\n          </button>\n        }\n      </div>\n    </form>\n  `,\n})\nexport class UiQuestionnaireComponent {\n  @Input() items: QuestionnaireItemDef[] = []\n  @Input() modelValue?: string\n  @Output() modelValueChange = new EventEmitter<string>()\n  @Input() answers: QuestionnaireAnswers = {}\n  @Output() answersChange = new EventEmitter<QuestionnaireAnswers>()\n  @Input() shortcuts: QuestionnaireShortcuts = false\n  @Input() showProgress = true\n  @Input() requiredMessage = 'Choose an answer to continue.'\n  @Output() submit = new EventEmitter<QuestionnaireAnswers>()\n  @Input('class') className?: string\n\n  showError = false\n\n  get names(): string[] {\n    return this.items.map((i) => i.name)\n  }\n\n  get currentName(): string | undefined {\n    return this.modelValue || this.names[0]\n  }\n\n  get activeIndex(): number {\n    return Math.max(0, this.names.indexOf(this.currentName ?? ''))\n  }\n\n  get current(): QuestionnaireItemDef | undefined {\n    return this.items[this.activeIndex]\n  }\n\n  get isFirst(): boolean {\n    return this.activeIndex <= 0\n  }\n\n  get isLast(): boolean {\n    return this.names.length === 0 || this.activeIndex >= this.names.length - 1\n  }\n\n  get progress(): { current: number; total: number } {\n    return { current: this.names.length ? this.activeIndex + 1 : 0, total: Math.max(this.names.length, 1) }\n  }\n\n  get progressPercent(): number {\n    return (this.progress.current / this.progress.total) * 100\n  }\n\n  get hostClass(): string {\n    return cn('flex w-full max-w-lg flex-col gap-4', this.className)\n  }\n\n  choiceClass(): string {\n    return questionnaireChoiceVariants()\n  }\n\n  getChoiceClass(choice: { disabled?: boolean }): string {\n    return cn(this.choiceClass(), choice.disabled && 'pointer-events-none opacity-50')\n  }\n\n  inputClass(): string {\n    return questionnaireInputVariants()\n  }\n\n  isAnswered(name: string): boolean {\n    const v = this.answers[name]\n    if (Array.isArray(v)) return v.length > 0\n    return typeof v === 'string' && v.trim().length > 0\n  }\n\n  setAnswer(name: string, value: QuestionnaireValue | undefined): void {\n    this.answers = { ...this.answers, [name]: value }\n    this.answersChange.emit(this.answers)\n    this.showError = false\n  }\n\n  toggleChoice(name: string, value: string, multiple?: boolean): void {\n    if (multiple) {\n      const currentVal = Array.isArray(this.answers[name]) ? [...(this.answers[name] as string[])] : []\n      const next = currentVal.includes(value) ? currentVal.filter((v) => v !== value) : [...currentVal, value]\n      this.setAnswer(name, next)\n      return\n    }\n    this.setAnswer(name, value)\n  }\n\n  choiceChecked(name: string, value: string): boolean {\n    const v = this.answers[name]\n    if (Array.isArray(v)) return v.includes(value)\n    return v === value\n  }\n\n  shortcutFor(index: number): string | undefined {\n    if (!this.shortcuts) return undefined\n    if (this.shortcuts === 'letters' && index < 26) return String.fromCharCode(65 + index)\n    if (this.shortcuts === 'numbers' && index < 9) return String(index + 1)\n    return undefined\n  }\n\n  goTo(index: number): void {\n    const name = this.names[index]\n    if (!name) return\n    this.modelValue = name\n    this.modelValueChange.emit(name)\n    this.showError = false\n  }\n\n  goNext(): void {\n    const cur = this.current\n    if (cur?.required && !this.isAnswered(cur.name)) {\n      this.showError = true\n      return\n    }\n    if (this.isLast) {\n      this.submit.emit(this.answers)\n      return\n    }\n    this.goTo(this.activeIndex + 1)\n  }\n\n  goPrev(): void {\n    this.goTo(this.activeIndex - 1)\n  }\n\n  // React/Vue render a <form> root; the host element can't be one, so the inner form\n  // carries submit semantics. Enter keydown is preventDefaulted in onKeydown, so this\n  // never double-advances.\n  onFormSubmit(e: Event): void {\n    e.preventDefault()\n    this.goNext()\n  }\n\n  skip(): void {\n    if (!this.current || this.current.required) return\n    this.showError = false\n    if (this.isLast) {\n      this.submit.emit(this.answers)\n      return\n    }\n    this.goTo(this.activeIndex + 1)\n  }\n\n  inputValue(name: string): string {\n    const v = this.answers[name]\n    return typeof v === 'string' ? v : ''\n  }\n\n  onInputChange(name: string, event: Event): void {\n    const target = event.target as HTMLInputElement\n    this.setAnswer(name, target.value)\n  }\n\n  onKeydown(e: KeyboardEvent): void {\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        this.goNext()\n      }\n      return\n    }\n    if (e.key === 'Enter') {\n      e.preventDefault()\n      this.goNext()\n      return\n    }\n    if (!this.shortcuts || e.metaKey || e.ctrlKey || e.altKey || !this.current) return\n    const values = this.current.choices?.map((c) => c.value) ?? []\n    let idx = -1\n    if (this.shortcuts === 'letters') idx = e.key.toLowerCase().charCodeAt(0) - 97\n    if (this.shortcuts === 'numbers') idx = Number(e.key) - 1\n    if (idx < 0 || idx >= values.length) return\n    e.preventDefault()\n    this.toggleChoice(this.current.name, values[idx], this.current.multiple)\n  }\n}\n\nexport { questionnaireChoiceVariants, questionnaireInputVariants } from './questionnaire.variants'\nexport type { QuestionnaireAnswers, QuestionnaireItemDef, QuestionnaireShortcuts, QuestionnaireValue } from './types'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/questionnaire/questionnaire.component.ts"
    },
    {
      "path": "packages/registry-angular/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/app/components/ui/questionnaire/questionnaire.variants.ts"
    },
    {
      "path": "packages/registry-angular/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/app/components/ui/questionnaire/types.ts"
    },
    {
      "path": "packages/registry-angular/components/questionnaire/index.ts",
      "content": "export {\n  UiQuestionnaireComponent,\n  questionnaireChoiceVariants,\n  questionnaireInputVariants,\n  type QuestionnaireAnswers,\n  type QuestionnaireItemDef,\n  type QuestionnaireShortcuts,\n  type QuestionnaireValue,\n} from './questionnaire.component'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/questionnaire/index.ts"
    }
  ],
  "dependencies": [
    "class-variance-authority"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/kbd.json"
  ],
  "description": "Single multi-step question component. Pass items, shortcuts, and showProgress — variants are props, not extra files.",
  "categories": [
    "form"
  ]
}