{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "elements-question-flow",
  "type": "registry:component",
  "title": "Question flow",
  "description": "A few short questions asked one at a time, answered together and kept as a receipt.",
  "registryDependencies": [
    "https://r.assistant-ui.com/elements-surfaces.json",
    "https://r.assistant-ui.com/elements-option-list.json"
  ],
  "files": [
    {
      "type": "registry:component",
      "path": "components/assistant-ui/elements/question-flow.tsx",
      "sourcePath": "packages/ui/src/components/react/assistant-ui/elements/question-flow.tsx",
      "content": "\"use client\";\n\nimport {\n  useId,\n  useLayoutEffect,\n  useRef,\n  useState,\n  type ComponentProps,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { OptionList, type OptionListOption } from \"./option-list\";\nimport { ghostButton, mono, paper } from \"./surfaces\";\n\nexport interface QuestionFlowStep {\n  id: string;\n  question: string;\n  description?: string | undefined;\n  options: readonly OptionListOption[];\n  selectionMode?: \"single\" | \"multiple\" | undefined;\n  minSelections?: number | undefined;\n  maxSelections?: number | undefined;\n}\n\nexport interface QuestionFlowProps extends Omit<\n  ComponentProps<\"div\">,\n  \"children\" | \"defaultValue\" | \"onSubmit\"\n> {\n  steps: readonly QuestionFlowStep[];\n  defaultValue?: Readonly<Record<string, readonly string[]>> | undefined;\n  onComplete?:\n    | ((answers: Record<string, string[]>) => void | Promise<void>)\n    | undefined;\n  completeLabel?: string | undefined;\n  choice?: Readonly<Record<string, readonly string[]>> | undefined;\n}\n\nexport function QuestionFlow({\n  steps,\n  defaultValue,\n  onComplete,\n  completeLabel = \"Submit\",\n  choice,\n  className,\n  ...props\n}: QuestionFlowProps) {\n  const [stepIndex, setStepIndex] = useState(0);\n  const [answers, setAnswers] = useState<Record<string, string[]>>(() => {\n    const initial: Record<string, string[]> = {};\n    for (const [id, value] of Object.entries(defaultValue ?? {})) {\n      initial[id] = [...value];\n    }\n    return initial;\n  });\n  const [confirmedAnswers, setConfirmedAnswers] = useState<\n    Record<string, string[]> | undefined\n  >();\n  const [isCompleting, setIsCompleting] = useState(false);\n  const questionPrefix = useId();\n  const optionListRef = useRef<HTMLDivElement>(null);\n  const currentIndex = Math.min(stepIndex, Math.max(0, steps.length - 1));\n  const currentStep = steps[currentIndex];\n  const previousStepId = useRef(currentStep?.id);\n\n  useLayoutEffect(() => {\n    if (previousStepId.current === currentStep?.id) return;\n    previousStepId.current = currentStep?.id;\n    optionListRef.current\n      ?.querySelector<HTMLButtonElement>(\n        'button:not([disabled]):not([aria-disabled=\"true\"])',\n      )\n      ?.focus();\n  }, [currentStep?.id]);\n\n  const root = cn(\n    paper,\n    \"flex w-full max-w-sm flex-col rounded-2xl p-2\",\n    className,\n  );\n\n  const completedChoice = choice ?? confirmedAnswers;\n\n  if (completedChoice !== undefined) {\n    return (\n      <div\n        {...props}\n        data-slot=\"question-flow\"\n        data-state=\"receipt\"\n        className={cn(root, \"gap-3\")}\n      >\n        {steps.flatMap((step) => {\n          const selected = completedChoice[step.id];\n          if (!selected?.length) return [];\n          const labels = step.options\n            .filter((option) => selected.includes(option.id))\n            .map((option) => option.label)\n            .join(\", \");\n          return (\n            <div key={step.id} className=\"flex flex-col gap-0.5 px-2 py-1\">\n              <span className=\"text-foreground/45 text-xs leading-4\">\n                {step.question}\n              </span>\n              <span className=\"text-foreground/80 text-[13.5px] leading-5 break-words\">\n                {labels}\n              </span>\n            </div>\n          );\n        })}\n      </div>\n    );\n  }\n\n  if (!onComplete) {\n    return (\n      <div\n        {...props}\n        data-slot=\"question-flow\"\n        data-state=\"open\"\n        className={cn(root, \"gap-5\")}\n      >\n        {steps.map((step) => {\n          const questionId = `${questionPrefix}-${step.id}`;\n          return (\n            <div key={step.id} className=\"flex flex-col gap-3\">\n              <div className=\"flex flex-col gap-0.5 px-2 pt-1\">\n                <p\n                  id={questionId}\n                  className=\"text-[13.5px] leading-5 font-medium\"\n                >\n                  {step.question}\n                </p>\n                {step.description ? (\n                  <p className=\"text-foreground/45 text-xs leading-4\">\n                    {step.description}\n                  </p>\n                ) : null}\n              </div>\n              <OptionList\n                aria-labelledby={questionId}\n                options={step.options}\n                className=\"max-w-none border-0 bg-transparent p-0 dark:bg-transparent\"\n              />\n            </div>\n          );\n        })}\n      </div>\n    );\n  }\n\n  if (!currentStep) {\n    return (\n      <div\n        {...props}\n        data-slot=\"question-flow\"\n        data-state=\"open\"\n        className={root}\n      />\n    );\n  }\n\n  const questionId = `${questionPrefix}-${currentStep.id}`;\n  const currentAnswer = answers[currentStep.id];\n  const finalStep = currentIndex === steps.length - 1;\n\n  const confirm = (ids: string[]) => {\n    const nextAnswers = { ...answers, [currentStep.id]: ids };\n    if (!finalStep) {\n      setAnswers(nextAnswers);\n      setStepIndex(currentIndex + 1);\n      return;\n    }\n    const stepIds = new Set(steps.map((step) => step.id));\n    const completedAnswers = Object.fromEntries(\n      Object.entries(nextAnswers).filter(([id]) => stepIds.has(id)),\n    );\n    setIsCompleting(true);\n    try {\n      return Promise.resolve(onComplete(completedAnswers)).then(\n        () => {\n          setAnswers(nextAnswers);\n          setConfirmedAnswers(completedAnswers);\n        },\n        (error) => {\n          setIsCompleting(false);\n          throw error;\n        },\n      );\n    } catch (error) {\n      setIsCompleting(false);\n      return Promise.reject(error);\n    }\n  };\n\n  return (\n    <div\n      {...props}\n      data-slot=\"question-flow\"\n      data-state=\"open\"\n      className={cn(root, \"gap-3\")}\n    >\n      <div className=\"flex items-center justify-between gap-3 px-2 pt-1\">\n        <span className={cn(mono, \"text-foreground/35 tabular-nums\")}>\n          {currentIndex + 1} of {steps.length}\n        </span>\n        {currentIndex > 0 ? (\n          <button\n            type=\"button\"\n            disabled={isCompleting}\n            onClick={() => setStepIndex(currentIndex - 1)}\n            className={cn(\n              ghostButton,\n              \"h-7 px-2.5 text-xs font-medium\",\n              isCompleting && \"cursor-default opacity-40\",\n            )}\n          >\n            Back\n          </button>\n        ) : null}\n      </div>\n      <div\n        role=\"progressbar\"\n        aria-valuenow={currentIndex + 1}\n        aria-valuemin={1}\n        aria-valuemax={steps.length}\n        aria-valuetext={`Question ${currentIndex + 1} of ${steps.length}`}\n        className=\"bg-foreground/[0.08] mx-2 h-[3px] overflow-hidden rounded-full\"\n      >\n        <div\n          style={{ width: `${((currentIndex + 1) / steps.length) * 100}%` }}\n          className=\"bg-foreground/80 h-full transition-[width] duration-200 motion-reduce:transition-none\"\n        />\n      </div>\n      <div className=\"flex flex-col gap-0.5 px-2\">\n        <p id={questionId} className=\"text-[13.5px] leading-5 font-medium\">\n          {currentStep.question}\n        </p>\n        {currentStep.description ? (\n          <p className=\"text-foreground/45 text-xs leading-4\">\n            {currentStep.description}\n          </p>\n        ) : null}\n      </div>\n      <div ref={optionListRef}>\n        <OptionList\n          key={currentStep.id}\n          aria-labelledby={questionId}\n          options={currentStep.options}\n          selectionMode={currentStep.selectionMode}\n          defaultValue={currentAnswer}\n          minSelections={currentStep.minSelections}\n          maxSelections={currentStep.maxSelections}\n          onConfirm={confirm}\n          confirmLabel={finalStep ? completeLabel : \"Next\"}\n          className=\"max-w-none border-0 bg-transparent p-0 dark:bg-transparent\"\n        />\n      </div>\n    </div>\n  );\n}\n"
    }
  ]
}