{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"c-questionnaire-4","type":"registry:block","title":"Segmented progress with step labels","description":"A custom progress bar built from the progress render state, over a four question flow.","dependencies":["cn"],"registryDependencies":["button","empty","questionnaire"],"files":[{"path":"c-questionnaire-4.tsx","type":"registry:block","content":"\"use client\"\n\nimport { useState, type FormEvent } from \"react\"\nimport { cn } from \"cn\"\n\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Empty,\n  EmptyContent,\n  EmptyDescription,\n  EmptyHeader,\n  EmptyMedia,\n  EmptyTitle,\n} from \"@/components/ui/empty\"\nimport {\n  Questionnaire,\n  QuestionnaireActions,\n  QuestionnaireChoice,\n  QuestionnaireChoices,\n  QuestionnaireError,\n  QuestionnaireItem,\n  QuestionnaireNext,\n  QuestionnairePrevious,\n  QuestionnaireProgress,\n  QuestionnaireSubmit,\n  QuestionnaireTitle,\n} from \"@/components/ui/questionnaire\"\nimport { IconPlaceholder } from \"@/app/(create)/components/icon-placeholder\"\n\ntype Question = {\n  name: string\n  step: string\n  title: string\n  choices: { value: string; label: string }[]\n}\n\n// Four questions of three choices each, none of them carrying a description,\n// so every step is the same height. Inactive fieldsets are hidden and have no\n// box at all, so uneven steps would make the card jump on each Next.\nconst QUESTIONS: Question[] = [\n  {\n    name: \"source\",\n    step: \"Source\",\n    title: \"Where does the data come from?\",\n    choices: [\n      { value: \"warehouse\", label: \"A warehouse table\" },\n      { value: \"stream\", label: \"An event stream\" },\n      { value: \"uploads\", label: \"Files uploaded by hand\" },\n    ],\n  },\n  {\n    name: \"volume\",\n    step: \"Volume\",\n    title: \"How much arrives in a day?\",\n    choices: [\n      { value: \"small\", label: \"Under 1M rows\" },\n      { value: \"medium\", label: \"1M to 50M rows\" },\n      { value: \"large\", label: \"More than 50M rows\" },\n    ],\n  },\n  {\n    name: \"alerting\",\n    step: \"Alerting\",\n    title: \"Who should be alerted when it stalls?\",\n    choices: [\n      { value: \"oncall\", label: \"The on-call rota\" },\n      { value: \"data\", label: \"The data team channel\" },\n      { value: \"none\", label: \"Nobody yet\" },\n    ],\n  },\n  {\n    name: \"owner\",\n    step: \"Owner\",\n    title: \"Who owns the pipeline?\",\n    choices: [\n      { value: \"me\", label: \"Me\" },\n      { value: \"teammate\", label: \"A teammate\" },\n      { value: \"undecided\", label: \"Still deciding\" },\n    ],\n  },\n]\n\nconst ITEMS = QUESTIONS.map((question) => ({\n  name: question.name,\n  required: true,\n}))\n\nexport function Pattern() {\n  const [created, setCreated] = useState<string | null>(null)\n\n  function handleSubmit(event: FormEvent<HTMLFormElement>) {\n    event.preventDefault()\n\n    const formData = new FormData(event.currentTarget)\n    const source = QUESTIONS[0].choices.find(\n      (choice) => choice.value === formData.get(\"source\")\n    )\n\n    setCreated(source?.label.toLowerCase() ?? \"the chosen source\")\n  }\n\n  if (created) {\n    return (\n      // Empty, not a Card, for the ending. The flow itself runs bare, so a\n      // panel appearing only at the finish would introduce a frame the visitor\n      // had not been looking at for the previous four steps.\n      <Empty className=\"mx-auto w-full max-w-md\">\n        <EmptyHeader>\n          <EmptyMedia variant=\"icon\">\n            <IconPlaceholder\n              lucide=\"CircleCheckIcon\"\n              tabler=\"IconCircleCheck\"\n              hugeicons=\"CheckmarkCircle01Icon\"\n              phosphor=\"CheckCircleIcon\"\n              remixicon=\"RiCheckboxCircleLine\"\n              aria-hidden=\"true\"\n            />\n          </EmptyMedia>\n          <EmptyTitle>Pipeline queued</EmptyTitle>\n          <EmptyDescription>\n            We are provisioning a pipeline from {created}. The first run starts\n            as soon as the connection is verified.\n          </EmptyDescription>\n        </EmptyHeader>\n        <EmptyContent>\n          <Button size=\"sm\" variant=\"outline\" onClick={() => setCreated(null)}>\n            Set up another\n          </Button>\n        </EmptyContent>\n      </Empty>\n    )\n  }\n\n  return (\n    <Questionnaire\n      className=\"mx-auto w-full max-w-md\"\n      defaultItem=\"source\"\n      items={ITEMS}\n      onSubmit={handleSubmit}\n    >\n      <QuestionnaireProgress\n        className=\"w-full\"\n        render={(props, state) => {\n          // w-full above is not optional: the primitive ships w-fit with a\n          // min-width sized for the default \"Question N of M\" string, and a\n          // custom bar left inside that box stops well short of the questions.\n          //\n          // One string for both readings. aria-valuetext goes on after the\n          // spread because the props carry the primitive's own \"Question N of\n          // M\", and for a progressbar that value text is what gets announced.\n          const step = QUESTIONS[state.current - 1]?.step\n          const label = `Step ${state.current} of ${state.total} - ${step}`\n\n          return (\n            <div {...props} aria-valuetext={label}>\n              <div aria-hidden=\"true\" className=\"mb-2 flex gap-1.5\">\n                {Array.from({ length: state.total }, (_, index) => (\n                  // Hidden from assistive tech because the wrapper already\n                  // carries role=\"progressbar\" with aria-valuenow, so exposing\n                  // the segments would announce the position a second time.\n                  //\n                  // state.current is 1-based, hence the strict less-than, and\n                  // state.total counts the enabled items rather than the\n                  // rendered ones, so disabling a question drops a segment.\n                  <span\n                    key={index}\n                    className={cn(\n                      \"h-1.5 flex-1 rounded-full\",\n                      index < state.current ? \"bg-primary\" : \"bg-muted\"\n                    )}\n                  />\n                ))}\n              </div>\n              <span>{label}</span>\n            </div>\n          )\n        }}\n      />\n\n      {QUESTIONS.map((question) => (\n        // The title renders as the fieldset's own legend with nothing between\n        // the two, so the group is named without any aria-labelledby pairing.\n        // A card header in between would break that and need one.\n        <QuestionnaireItem key={question.name} name={question.name} required>\n          <QuestionnaireTitle>{question.title}</QuestionnaireTitle>\n          <QuestionnaireChoices>\n            {question.choices.map((choice) => (\n              <QuestionnaireChoice key={choice.value} value={choice.value}>\n                {choice.label}\n              </QuestionnaireChoice>\n            ))}\n          </QuestionnaireChoices>\n          <QuestionnaireError />\n        </QuestionnaireItem>\n      ))}\n\n      <QuestionnaireActions>\n        <QuestionnairePrevious />\n        <QuestionnaireNext>Next</QuestionnaireNext>\n        <QuestionnaireSubmit>Create the pipeline</QuestionnaireSubmit>\n      </QuestionnaireActions>\n    </Questionnaire>\n  )\n}","target":"components/examples/c-questionnaire-4.tsx"}],"meta":{"order":4,"gridSize":2}}