{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "questionnaire",
  "title": "Questionnaire",
  "description": "Multi-step question flow with single/multiple/free-text questions, skip, conditional logic and keyboard shortcuts",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "@boldkit/utils",
    "@boldkit/button",
    "@boldkit/input",
    "@boldkit/kbd",
    "@boldkit/label",
    "@boldkit/textarea"
  ],
  "files": [
    {
      "path": "registry/default/ui/questionnaire.tsx",
      "content": "import * as React from 'react'\nimport { ArrowLeft, ArrowRight, Check } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { Button } from '@/components/ui/button'\nimport { Input } from '@/components/ui/input'\nimport { Kbd } from '@/components/ui/kbd'\nimport { Label } from '@/components/ui/label'\nimport { Textarea } from '@/components/ui/textarea'\n\n/**\n * Questionnaire: a data-driven, one-question-per-screen flow for agent\n * clarification prompts, onboarding, surveys and intake forms.\n *\n * Answers are plain values: `single` → string, `multiple` → string[],\n * `text` → string. An \"Other…\" answer is stored as the typed text itself, so\n * the submitted payload needs no decoding.\n *\n * It renders a real <form>: the current question's native radios / checkboxes /\n * textarea carry `name={question.id}`, and every other answered question is\n * mirrored into hidden inputs, so a server-rendered POST (`action` + `method`)\n * receives every answer. Without `action`, submission stays client-side and\n * only `onSubmit(answers, event)` fires.\n *\n * Why not <MultiStepForm />: it owns its values internally (no controlled\n * `value`), its step list is fixed (no `when` branching) and its validators\n * can't be bypassed by an optional question's Skip.\n */\n\nexport type QuestionnaireQuestionType = 'single' | 'multiple' | 'text'\n\nexport interface QuestionnaireChoice {\n  value: string\n  label: string\n  description?: string\n}\n\n/** `single` and `text` answer with a string, `multiple` with a string[]. */\nexport type QuestionnaireAnswer = string | string[]\n\nexport type QuestionnaireAnswers = Record<string, QuestionnaireAnswer>\n\nexport interface QuestionnaireQuestion {\n  id: string\n  type: QuestionnaireQuestionType\n  title: string\n  description?: string\n  choices?: QuestionnaireChoice[]\n  /** Shows a Skip button and lets Next pass with no answer. */\n  optional?: boolean\n  /** Adds an \"Other…\" choice that reveals a text input (single / multiple). */\n  allowOther?: boolean\n  /** Minimum selections for `multiple`. */\n  min?: number\n  /** Maximum selections for `multiple`; further choices disable at the cap. */\n  max?: number\n  /**\n   * Runs after the built-in checks pass on a non-empty answer, with the answers\n   * of the visible questions. Return an error message or null.\n   */\n  validate?: (answer: QuestionnaireAnswer, answers: QuestionnaireAnswers) => string | null\n  /**\n   * Show this question only when it returns true. Receives the answers of the\n   * earlier visible questions only, so a hidden question's stale answer can't\n   * keep its dependents alive. Hidden answers are left out of the payload.\n   */\n  when?: (answers: QuestionnaireAnswers) => boolean\n}\n\nexport interface QuestionnaireProps\n  extends Omit<React.FormHTMLAttributes<HTMLFormElement>, 'onSubmit' | 'defaultValue' | 'children'> {\n  questions: QuestionnaireQuestion[]\n  /** Controlled answers. */\n  value?: QuestionnaireAnswers\n  /** Initial answers when uncontrolled. */\n  defaultValue?: QuestionnaireAnswers\n  onValueChange?: (answers: QuestionnaireAnswers) => void\n  /** Fires on the last question with the answers of every visible question. */\n  onSubmit?: (answers: QuestionnaireAnswers, event: React.FormEvent<HTMLFormElement>) => void\n  /** `default` draws a bordered panel; `plain` drops it for use inside a Card or Dialog. */\n  variant?: 'default' | 'plain'\n  submitLabel?: React.ReactNode\n}\n\ninterface OtherState {\n  on: boolean\n  text: string\n}\n\nconst NON_TEXT_INPUTS = ['radio', 'checkbox', 'button', 'submit', 'reset', 'hidden']\n\nfunction hasChoice(question: QuestionnaireQuestion, value: string) {\n  return question.choices?.some((choice) => choice.value === value) ?? false\n}\n\nfunction isEmptyAnswer(answer: QuestionnaireAnswer | undefined) {\n  return answer === undefined || answer === '' || (Array.isArray(answer) && answer.length === 0)\n}\n\nfunction asArray(answer: QuestionnaireAnswer | undefined): string[] {\n  if (answer === undefined) return []\n  return Array.isArray(answer) ? answer : [answer]\n}\n\n// Whether \"Other…\" is selected and what it says. Derived from the answer (a\n// value that isn't a choice is the Other text) unless the UI state still\n// matches it — which keeps the input open while the typed text happens to\n// equal a choice value, or is still empty.\nfunction resolveOther(\n  question: QuestionnaireQuestion,\n  answer: QuestionnaireAnswer | undefined,\n  ui: OtherState | undefined\n): OtherState {\n  if (!question.allowOther || question.type === 'text') return { on: false, text: '' }\n  if (question.type === 'single') {\n    const single = typeof answer === 'string' ? answer : undefined\n    if (ui?.on && single === ui.text) return ui\n    if (single !== undefined && !hasChoice(question, single)) return { on: true, text: single }\n  } else {\n    const list = asArray(answer)\n    if (ui?.on && list.includes(ui.text)) return ui\n    const extra = list.find((v) => !hasChoice(question, v))\n    if (extra !== undefined) return { on: true, text: extra }\n  }\n  return { on: false, text: ui?.text ?? '' }\n}\n\n// The selected regular choices, with the Other entry removed.\nfunction selectedChoices(\n  question: QuestionnaireQuestion,\n  answer: QuestionnaireAnswer | undefined,\n  other: OtherState\n): string[] {\n  const list = [...asArray(answer)]\n  if (other.on) {\n    const at = list.indexOf(other.text)\n    if (at >= 0) list.splice(at, 1)\n  }\n  return list.filter((v) => hasChoice(question, v))\n}\n\nfunction getQuestionError(\n  question: QuestionnaireQuestion,\n  answer: QuestionnaireAnswer | undefined,\n  other: OtherState,\n  answers: QuestionnaireAnswers\n): string | null {\n  if (question.type === 'text') {\n    const text = typeof answer === 'string' ? answer : ''\n    if (!text.trim()) return question.optional ? null : 'This question is required.'\n  } else {\n    const count = question.type === 'single' ? (answer === undefined ? 0 : 1) : asArray(answer).length\n    if (count === 0) {\n      if (question.optional) return null\n      return question.type === 'single' ? 'Choose an option.' : 'Choose at least one option.'\n    }\n    if (other.on && !other.text.trim()) return 'Please describe your other answer.'\n    if (question.type === 'multiple') {\n      if (question.min != null && count < question.min) return `Choose at least ${question.min} options.`\n      if (question.max != null && count > question.max) return `Choose at most ${question.max} options.`\n    }\n  }\n  return isEmptyAnswer(answer) ? null : (question.validate?.(answer as QuestionnaireAnswer, answers) ?? null)\n}\n\n// Evaluate `when` in order against an accumulator of the answers of questions\n// already found visible.\nfunction resolveVisible(questions: QuestionnaireQuestion[], answers: QuestionnaireAnswers) {\n  const visible: QuestionnaireQuestion[] = []\n  const visibleAnswers: QuestionnaireAnswers = {}\n  for (const question of questions) {\n    if (question.when && !question.when({ ...visibleAnswers })) continue\n    visible.push(question)\n    if (answers[question.id] !== undefined) visibleAnswers[question.id] = answers[question.id]\n  }\n  return { visible, visibleAnswers }\n}\n\n// Position of the current question in `visible`. If it has been hidden, fall\n// forward to the nearest following visible question.\nfunction resolveIndex(\n  questions: QuestionnaireQuestion[],\n  visible: QuestionnaireQuestion[],\n  currentId: string | null\n) {\n  if (currentId === null) return 0\n  const at = visible.findIndex((q) => q.id === currentId)\n  if (at >= 0) return at\n  const order = questions.findIndex((q) => q.id === currentId)\n  const following = visible.findIndex((q) => questions.indexOf(q) > order)\n  return following >= 0 ? following : Math.max(visible.length - 1, 0)\n}\n\nfunction collectAnswers(visible: QuestionnaireQuestion[], answers: QuestionnaireAnswers) {\n  const out: QuestionnaireAnswers = {}\n  for (const question of visible) {\n    const answer = answers[question.id]\n    if (isEmptyAnswer(answer)) continue\n    out[question.id] = answer\n  }\n  return out\n}\n\nfunction selectionHint(question: QuestionnaireQuestion) {\n  if (question.type !== 'multiple') return null\n  const { min, max } = question\n  if (min != null && max != null) return `Choose ${min}–${max}`\n  if (min != null) return `Choose at least ${min}`\n  if (max != null) return `Choose up to ${max}`\n  return 'Choose all that apply'\n}\n\n// Letter A–Z or digit 1–9 → choice index.\nfunction shortcutIndex(key: string) {\n  if (/^[1-9]$/.test(key)) return Number(key) - 1\n  if (/^[a-z]$/i.test(key)) return key.toLowerCase().charCodeAt(0) - 97\n  return -1\n}\n\nfunction isTypingTarget(el: HTMLElement) {\n  if (el.isContentEditable) return true\n  if (el.tagName === 'TEXTAREA' || el.tagName === 'SELECT') return true\n  return el.tagName === 'INPUT' && !NON_TEXT_INPUTS.includes((el as HTMLInputElement).type)\n}\n\nconst titleClass = 'block text-xl font-black normal-case leading-tight tracking-tight sm:text-2xl'\n\nconst Questionnaire = React.forwardRef<HTMLFormElement, QuestionnaireProps>(\n  (\n    {\n      questions,\n      value,\n      defaultValue,\n      onValueChange,\n      onSubmit,\n      variant = 'default',\n      submitLabel = 'Submit',\n      className,\n      action,\n      onKeyDown,\n      ...props\n    },\n    ref\n  ) => {\n    const baseId = React.useId()\n    const formRef = React.useRef<HTMLFormElement | null>(null)\n    const questionRef = React.useRef<HTMLDivElement>(null)\n    const otherInputRef = React.useRef<HTMLInputElement>(null)\n    const focusOtherRef = React.useRef(false)\n\n    const [innerAnswers, setInnerAnswers] = React.useState<QuestionnaireAnswers>(defaultValue ?? {})\n    const answers = value ?? innerAnswers\n    const [otherUi, setOtherUi] = React.useState<Record<string, OtherState>>({})\n    const [currentId, setCurrentId] = React.useState<string | null>(null)\n    const [attempted, setAttempted] = React.useState(false)\n    const [submitRequest, setSubmitRequest] = React.useState(0)\n    // Double-submit guards: set once onSubmit fires / a Skip-submit is queued,\n    // cleared by any edit or navigation.\n    const submittedRef = React.useRef(false)\n    const submitPendingRef = React.useRef(false)\n\n    const { visible, visibleAnswers } = React.useMemo(\n      () => resolveVisible(questions, answers),\n      [questions, answers]\n    )\n    const total = visible.length\n    const index = resolveIndex(questions, visible, currentId)\n    const current = visible[index] as QuestionnaireQuestion | undefined\n    const isLast = index === total - 1\n    const answer = current ? answers[current.id] : undefined\n    const other = current ? resolveOther(current, answer, otherUi[current.id]) : { on: false, text: '' }\n    const error = current ? getQuestionError(current, answer, other, visibleAnswers) : null\n    const showError = attempted && error !== null\n    const payload = React.useMemo(() => collectAnswers(visible, answers), [visible, answers])\n\n    const setFormRef = React.useCallback(\n      (node: HTMLFormElement | null) => {\n        formRef.current = node\n        if (typeof ref === 'function') ref(node)\n        else if (ref) ref.current = node\n      },\n      [ref]\n    )\n\n    // Move focus into the new question on navigation (not on first render), so\n    // keyboard users land on it and the shortcuts keep working.\n    const shownId = current?.id\n    const prevShownIdRef = React.useRef(shownId)\n    React.useEffect(() => {\n      if (prevShownIdRef.current === shownId) return\n      prevShownIdRef.current = shownId\n      const root = questionRef.current\n      const target =\n        root?.querySelector<HTMLElement>('textarea') ??\n        root?.querySelector<HTMLElement>('input:checked') ??\n        root?.querySelector<HTMLElement>('input:not([type=hidden]):not(:disabled)')\n      target?.focus()\n    }, [shownId])\n\n    React.useEffect(() => {\n      if (focusOtherRef.current && otherInputRef.current) {\n        focusOtherRef.current = false\n        otherInputRef.current.focus()\n      }\n    })\n\n    // Skip on the last question clears the answer first, then submits once the\n    // form has re-rendered without it.\n    React.useEffect(() => {\n      if (submitRequest === 0) return\n      submitPendingRef.current = false\n      formRef.current?.requestSubmit()\n    }, [submitRequest])\n\n    const commit = (next: QuestionnaireAnswers) => {\n      submittedRef.current = false\n      if (value === undefined) setInnerAnswers(next)\n      onValueChange?.(next)\n    }\n\n    const setAnswer = (id: string, next: QuestionnaireAnswer | undefined) => {\n      const updated = { ...answers }\n      if (next === undefined || (Array.isArray(next) && next.length === 0)) delete updated[id]\n      else updated[id] = next\n      commit(updated)\n    }\n\n    const setOther = (id: string, state: OtherState) =>\n      setOtherUi((prev) => ({ ...prev, [id]: state }))\n\n    const goTo = (target: number) => {\n      submittedRef.current = false\n      setAttempted(false)\n      setCurrentId(visible[target]?.id ?? null)\n    }\n\n    if (!current) {\n      return <form ref={setFormRef} data-slot=\"questionnaire\" className={className} {...props} />\n    }\n\n    const choices = current.choices ?? []\n    const selected = selectedChoices(current, answer, other)\n    const count = selected.length + (other.on ? 1 : 0)\n    const atMax = current.type === 'multiple' && current.max != null && count >= current.max\n\n    const selectChoice = (choice: string) => {\n      if (current.type === 'single') {\n        setOther(current.id, { on: false, text: other.text })\n        setAnswer(current.id, choice)\n        return\n      }\n      const isOn = selected.includes(choice)\n      if (!isOn && atMax) return\n      const next = isOn ? selected.filter((v) => v !== choice) : [...selected, choice]\n      setAnswer(current.id, other.on ? [...next, other.text] : next)\n    }\n\n    const toggleOther = (focus: boolean) => {\n      if (current.type === 'single') {\n        setOther(current.id, { on: true, text: other.text })\n        setAnswer(current.id, other.text)\n        focusOtherRef.current = focus\n        return\n      }\n      const on = !other.on\n      if (on && atMax) return\n      setOther(current.id, { on, text: other.text })\n      setAnswer(current.id, on ? [...selected, other.text] : selected)\n      focusOtherRef.current = focus && on\n    }\n\n    const setOtherText = (text: string) => {\n      setOther(current.id, { on: true, text })\n      setAnswer(current.id, current.type === 'single' ? text : [...selected, text])\n    }\n\n    const skip = () => {\n      if (isLast && (submittedRef.current || submitPendingRef.current)) return\n      setOtherUi((prev) => {\n        const next = { ...prev }\n        delete next[current.id]\n        return next\n      })\n      setAnswer(current.id, undefined)\n      if (isLast) {\n        submitPendingRef.current = true\n        setSubmitRequest((n) => n + 1)\n      } else {\n        goTo(index + 1)\n      }\n    }\n\n    const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {\n      if (error !== null) {\n        event.preventDefault()\n        setAttempted(true)\n        return\n      }\n      if (!isLast) {\n        event.preventDefault()\n        goTo(index + 1)\n        return\n      }\n      if (submittedRef.current) {\n        event.preventDefault()\n        return\n      }\n      submittedRef.current = true\n      onSubmit?.(payload, event)\n      // Without an action there's nowhere to post to — keep it client-side.\n      if (action === undefined) event.preventDefault()\n    }\n\n    const handleKeyDown = (event: React.KeyboardEvent<HTMLFormElement>) => {\n      onKeyDown?.(event)\n      // keyCode 229: Safari fires the IME-confirming Enter after compositionend.\n      if (event.defaultPrevented || event.nativeEvent.isComposing || event.keyCode === 229) return\n      const target = event.target as HTMLElement\n\n      if (event.key === 'Enter') {\n        if (target.tagName === 'BUTTON' || target.tagName === 'A') return\n        if (target.tagName === 'TEXTAREA' && !(event.metaKey || event.ctrlKey)) return\n        event.preventDefault()\n        formRef.current?.requestSubmit()\n        return\n      }\n\n      if (current.type === 'text' || isTypingTarget(target)) return\n      if (event.metaKey || event.ctrlKey || event.altKey) return\n      const i = shortcutIndex(event.key)\n      if (i < 0) return\n      if (i < choices.length) {\n        const choice = choices[i].value\n        if (current.type === 'multiple' && atMax && !selected.includes(choice)) return\n        event.preventDefault()\n        selectChoice(choice)\n      } else if (i === choices.length && current.allowOther) {\n        if (current.type === 'multiple' && atMax && !other.on) return\n        event.preventDefault()\n        toggleOther(true)\n      }\n    }\n\n    const fieldId = `${baseId}-field`\n    const progressId = `${baseId}-progress`\n    const descriptionId = current.description ? `${baseId}-description` : undefined\n    const hint = selectionHint(current)\n    const hintId = hint ? `${baseId}-hint` : undefined\n    const errorId = showError ? `${baseId}-error` : undefined\n    const describedBy = [descriptionId, hintId, errorId].filter(Boolean).join(' ') || undefined\n    const inputType = current.type === 'single' ? 'radio' : 'checkbox'\n\n    const renderChoice = (\n      key: string,\n      i: number,\n      label: string,\n      inputValue: string,\n      checked: boolean,\n      onChange: () => void,\n      description?: string\n    ) => {\n      const disabled = atMax && !checked\n      const letter = i < 26 ? String.fromCharCode(65 + i) : undefined\n      return (\n        <label\n          key={key}\n          data-slot=\"questionnaire-choice\"\n          data-state={checked ? 'checked' : 'unchecked'}\n          data-disabled={disabled ? '' : undefined}\n          className={cn(\n            'flex cursor-pointer items-start gap-3 border-3 border-foreground bg-background px-4 py-3 shadow-[4px_4px_0px_hsl(var(--shadow-color))] transition duration-150 ease-out motion-reduce:transition-none',\n            'hover:translate-x-[4px] hover:translate-y-[4px] hover:shadow-none',\n            'has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-ring has-[:focus-visible]:ring-offset-2 has-[:focus-visible]:ring-offset-background',\n            'data-[state=checked]:translate-x-[4px] data-[state=checked]:translate-y-[4px] data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:shadow-none',\n            'data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50 data-[disabled]:hover:translate-x-0 data-[disabled]:hover:translate-y-0 data-[disabled]:hover:shadow-[4px_4px_0px_hsl(var(--shadow-color))]'\n          )}\n        >\n          <input\n            type={inputType}\n            className=\"sr-only\"\n            name={current.id}\n            value={inputValue}\n            checked={checked}\n            disabled={disabled}\n            onChange={onChange}\n            aria-invalid={showError || undefined}\n            aria-keyshortcuts={letter ? `${letter}${i < 9 ? ` ${i + 1}` : ''}` : undefined}\n          />\n          {letter && (\n            <Kbd aria-hidden=\"true\" size=\"sm\" className=\"mt-0.5 shrink-0 text-foreground\">\n              {letter}\n            </Kbd>\n          )}\n          <span className=\"min-w-0 flex-1\">\n            <span className=\"block font-bold\">{label}</span>\n            {description && <span className=\"mt-0.5 block text-sm opacity-80\">{description}</span>}\n          </span>\n          <span\n            aria-hidden=\"true\"\n            className=\"mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center border-3 border-foreground bg-background text-foreground\"\n          >\n            {checked && <Check className=\"h-3 w-3\" strokeWidth={4} />}\n          </span>\n        </label>\n      )\n    }\n\n    return (\n      <form\n        ref={setFormRef}\n        data-slot=\"questionnaire\"\n        data-variant={variant}\n        noValidate\n        action={action}\n        className={cn(\n          'text-foreground',\n          variant === 'default' &&\n            'border-3 border-foreground bg-background p-5 shadow-[4px_4px_0px_hsl(var(--shadow-color))] sm:p-6',\n          className\n        )}\n        onSubmit={handleSubmit}\n        onKeyDown={handleKeyDown}\n        {...props}\n      >\n        <div data-slot=\"questionnaire-header\" className=\"mb-6 space-y-3\">\n          <div className=\"flex items-center justify-between gap-3\">\n            <p id={progressId} className=\"text-xs font-bold uppercase tracking-widest\">\n              Question {index + 1} of {total}\n            </p>\n            {current.optional && (\n              <span className=\"border-2 border-foreground bg-muted px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-widest\">\n                Optional\n              </span>\n            )}\n          </div>\n          <div\n            role=\"progressbar\"\n            aria-labelledby={progressId}\n            aria-valuemin={1}\n            aria-valuemax={total}\n            aria-valuenow={index + 1}\n            data-slot=\"questionnaire-progress\"\n            className=\"flex gap-1.5\"\n          >\n            {visible.map((q, i) => (\n              <span\n                key={q.id}\n                data-state={i < index ? 'complete' : i === index ? 'current' : 'upcoming'}\n                className={cn(\n                  'h-3 flex-1 border-3 border-foreground transition-colors duration-200 motion-reduce:transition-none',\n                  i < index ? 'bg-primary' : i === index ? 'bg-accent' : 'bg-muted'\n                )}\n              />\n            ))}\n          </div>\n        </div>\n\n        <div\n          key={current.id}\n          ref={questionRef}\n          data-slot=\"questionnaire-question\"\n          className=\"animate-in fade-in-0 slide-in-from-bottom-2 duration-200 motion-reduce:animate-none\"\n        >\n          {current.type === 'text' ? (\n            <div className=\"space-y-3\">\n              <Label htmlFor={fieldId} className={titleClass}>\n                {current.title}\n              </Label>\n              {current.description && (\n                <p id={descriptionId} className=\"text-sm text-muted-foreground\">\n                  {current.description}\n                </p>\n              )}\n              <Textarea\n                id={fieldId}\n                name={current.id}\n                value={typeof answer === 'string' ? answer : ''}\n                onChange={(e) => setAnswer(current.id, e.target.value)}\n                aria-describedby={describedBy}\n                aria-invalid={showError || undefined}\n              />\n              <p className=\"flex items-center gap-1.5 text-xs text-muted-foreground\">\n                <Kbd size=\"sm\">Ctrl</Kbd>\n                <span aria-hidden=\"true\">+</span>\n                <Kbd size=\"sm\">Enter</Kbd>\n                <span>to continue</span>\n              </p>\n            </div>\n          ) : (\n            <fieldset aria-describedby={describedBy} className=\"min-w-0 space-y-3\">\n              <legend className={cn(titleClass, 'mb-3')}>{current.title}</legend>\n              {current.description && (\n                <p id={descriptionId} className=\"text-sm text-muted-foreground\">\n                  {current.description}\n                </p>\n              )}\n              {hint && (\n                <p id={hintId} className=\"text-xs font-bold uppercase tracking-wide text-muted-foreground\">\n                  {hint}\n                </p>\n              )}\n              <div className=\"grid gap-3 pt-1\">\n                {choices.map((choice, i) =>\n                  renderChoice(\n                    choice.value,\n                    i,\n                    choice.label,\n                    choice.value,\n                    current.type === 'single'\n                      ? !other.on && answer === choice.value\n                      : selected.includes(choice.value),\n                    () => selectChoice(choice.value),\n                    choice.description\n                  )\n                )}\n                {current.allowOther &&\n                  renderChoice('__other__', choices.length, 'Other…', other.text, other.on, () =>\n                    toggleOther(false)\n                  )}\n              </div>\n              {other.on && (\n                <Input\n                  ref={otherInputRef}\n                  value={other.text}\n                  onChange={(e) => setOtherText(e.target.value)}\n                  placeholder=\"Type your answer\"\n                  aria-label={`Other answer for: ${current.title}`}\n                  aria-invalid={(showError && !other.text.trim()) || undefined}\n                />\n              )}\n            </fieldset>\n          )}\n\n          {showError && (\n            <p id={errorId} role=\"alert\" className=\"mt-3 text-sm font-bold text-destructive\">\n              {error}\n            </p>\n          )}\n        </div>\n\n        {visible.map((q) =>\n          q.id === current.id\n            ? null\n            : asArray(payload[q.id]).map((v, i) => (\n                <input key={`${q.id}-${i}`} type=\"hidden\" name={q.id} value={v} />\n              ))\n        )}\n\n        <div data-slot=\"questionnaire-footer\" className=\"mt-8 flex flex-wrap items-center justify-between gap-3\">\n          <Button type=\"button\" variant=\"outline\" onClick={() => goTo(index - 1)} disabled={index === 0}>\n            <ArrowLeft />\n            Previous\n          </Button>\n          <div className=\"flex items-center gap-3\">\n            {current.optional && (\n              <Button type=\"button\" variant=\"ghost\" onClick={skip}>\n                Skip\n              </Button>\n            )}\n            <Button\n              type=\"submit\"\n              aria-disabled={error !== null || undefined}\n              className=\"aria-disabled:cursor-not-allowed aria-disabled:opacity-50 aria-disabled:hover:translate-x-0 aria-disabled:hover:translate-y-0 aria-disabled:hover:shadow-[4px_4px_0px_hsl(var(--shadow-color))]\"\n            >\n              {isLast ? (\n                <>\n                  {submitLabel}\n                  <Check />\n                </>\n              ) : (\n                <>\n                  Next\n                  <ArrowRight />\n                </>\n              )}\n            </Button>\n          </div>\n        </div>\n      </form>\n    )\n  }\n)\nQuestionnaire.displayName = 'Questionnaire'\n\nexport { Questionnaire }\n",
      "type": "registry:ui",
      "target": "components/ui/questionnaire.tsx"
    }
  ],
  "type": "registry:ui"
}