# Questionnaire
Questions asked one at a time: choice keys on the hairline that take the pale blue when chosen, a mono progress count and the blue Next key.

Install: npx opencommerce-ds add questionnaire
Requires: theme, button, input, kbd, progress, utils
Packages: lucide-react
Exports: Questionnaire, QuestionnaireActions, QuestionnaireChoice, QuestionnaireChoiceDescription, QuestionnaireChoices, QuestionnaireDescription, QuestionnaireError, QuestionnaireInput, QuestionnaireItem, QuestionnaireNext, QuestionnairePrevious, QuestionnaireProgress, QuestionnaireSkip, QuestionnaireSubmit, QuestionnaireTitle
Client boundary: yes
Reference: Extrapolated from opencommerce (in-action.tsx step list, checkbox.tsx, the keys)


## Usage
Use for a short set of questions with set answers (onboarding an agent, a checkout preference survey). Pass items with name, required and multiple; for each, a QuestionnaireItem with a title, choices (QuestionnaireChoice), an optional QuestionnaireInput and QuestionnaireError; finish with QuestionnaireActions (Previous, Skip, Next, Submit). Read the answers from FormData on submit.
- Avoid: More than one question on screen
- Avoid: Animating between questions
- Avoid: Hiding the progress


## Examples
Example components: npx opencommerce-ds add questionnaire window
[Site](https://opencommerce-ds.vercel.app/preview/questionnaire/Site) | [Letters](https://opencommerce-ds.vercel.app/preview/questionnaire/Letters)

examples/questionnaire.tsx

```tsx
"use client";

import * as React from "react";

import {
  Questionnaire,
  QuestionnaireActions,
  QuestionnaireChoice,
  QuestionnaireChoiceDescription,
  QuestionnaireChoices,
  QuestionnaireDescription,
  QuestionnaireError,
  QuestionnaireInput,
  QuestionnaireItem,
  QuestionnaireNext,
  QuestionnairePrevious,
  QuestionnaireProgress,
  QuestionnaireSkip,
  QuestionnaireSubmit,
  QuestionnaireTitle,
} from "@/components/ui/questionnaire";
import { Window } from "@/components/ui/window";

const ITEMS = [
  { name: "buys", required: true },
  { name: "stores", multiple: true },
  { name: "approval", required: true },
] as const;

export function Site() {
  const [result, setResult] = React.useState<string>();
  return (
    <Window name="agent.setup" className="mx-auto w-full max-w-lg" bodyClassName="p-5">
      {result ? (
        <p role="status" className="text-body-sm leading-relaxed text-fg">
          {result}
        </p>
      ) : (
        <Questionnaire
          items={ITEMS}
          onSubmit={(event) => {
            event.preventDefault();
            const data = new FormData(event.currentTarget);
            setResult(`Saved. Your agent buys ${data.get("buys")}, from ${data.getAll("stores").filter(Boolean).join(", ") || "any store"}, and asks first ${data.get("approval")}.`);
          }}
        >
          <QuestionnaireProgress />
          <QuestionnaireItem name="buys" required>
            <QuestionnaireTitle>What will your agent buy?</QuestionnaireTitle>
            <QuestionnaireDescription>Pick the closest; you can change it later.</QuestionnaireDescription>
            <QuestionnaireChoices>
              <QuestionnaireChoice value="office supplies">Office supplies</QuestionnaireChoice>
              <QuestionnaireChoice value="travel">
                Travel
                <QuestionnaireChoiceDescription>Flights, hotels and trains</QuestionnaireChoiceDescription>
              </QuestionnaireChoice>
              <QuestionnaireChoice value="groceries">Groceries</QuestionnaireChoice>
            </QuestionnaireChoices>
            <QuestionnaireInput placeholder="Something else" aria-label="Something else" />
            <QuestionnaireError />
          </QuestionnaireItem>
          <QuestionnaireItem name="stores" multiple>
            <QuestionnaireTitle>Which stores can it use?</QuestionnaireTitle>
            <QuestionnaireDescription>Leave them all off to allow any store.</QuestionnaireDescription>
            <QuestionnaireChoices>
              <QuestionnaireChoice value="Amazon">Amazon</QuestionnaireChoice>
              <QuestionnaireChoice value="Target">Target</QuestionnaireChoice>
              <QuestionnaireChoice value="Best Buy">Best Buy</QuestionnaireChoice>
              <QuestionnaireChoice value="Staples" disabled>
                Staples
                <QuestionnaireChoiceDescription>Paused while its route recovers</QuestionnaireChoiceDescription>
              </QuestionnaireChoice>
            </QuestionnaireChoices>
          </QuestionnaireItem>
          <QuestionnaireItem name="approval" required>
            <QuestionnaireTitle>When should it ask you first?</QuestionnaireTitle>
            <QuestionnaireChoices>
              <QuestionnaireChoice value="for every order">Every order</QuestionnaireChoice>
              <QuestionnaireChoice value="over $100">Orders over $100</QuestionnaireChoice>
              <QuestionnaireChoice value="never">Never</QuestionnaireChoice>
            </QuestionnaireChoices>
            <QuestionnaireError />
          </QuestionnaireItem>
          <QuestionnaireActions>
            <QuestionnairePrevious />
            <QuestionnaireSkip />
            <QuestionnaireNext />
            <QuestionnaireSubmit>Save</QuestionnaireSubmit>
          </QuestionnaireActions>
        </Questionnaire>
      )}
    </Window>
  );
}

export function Letters() {
  return (
    <Window name="feedback.form" className="mx-auto w-full max-w-lg" bodyClassName="p-5">
      <Questionnaire items={[{ name: "speed", required: true }]} shortcuts="letters" onSubmit={(event) => event.preventDefault()}>
        <QuestionnaireItem name="speed" required>
          <QuestionnaireTitle>How fast did checkout feel?</QuestionnaireTitle>
          <QuestionnaireChoices>
            <QuestionnaireChoice value="instant">Instant</QuestionnaireChoice>
            <QuestionnaireChoice value="fine">Fine</QuestionnaireChoice>
            <QuestionnaireChoice value="slow">Slow</QuestionnaireChoice>
          </QuestionnaireChoices>
          <QuestionnaireError />
        </QuestionnaireItem>
        <QuestionnaireActions>
          <QuestionnaireSubmit>Send</QuestionnaireSubmit>
        </QuestionnaireActions>
      </Questionnaire>
    </Window>
  );
}

```

## Source

components/ui/questionnaire.tsx

```tsx
"use client";

import * as React from "react";
import { Check } from "lucide-react";

import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Kbd } from "@/components/ui/kbd";
import { Progress } from "@/components/ui/progress";
import { cn } from "@/lib/utils";

// A short set of questions asked one at a time, in the site's form voice (the step list in in-action.tsx, checkbox.tsx,
// globals.css .btn; computed). A choice is a 44px white key on the 16 percent ink edge with 8px corners, the round or
// square mark 14px in, a 15px Inter label and its number or letter key at the right; under the pointer it takes the
// mist, and a chosen one sits on the pale blue with the blue edge, as the selected step does. The mark is the
// checkbox's 18px box (the blue key with a white check when chosen) or its round kin (a blue dot in a blue ring). The
// question is 17px in the display face at 500, tracked tight, with a 14px line at 64 percent ink under it; progress is
// a mono count over the brand's 2px-cornered bar. Changing questions is instant: it is driven by the keyboard as often
// as by the pointer. The answers submit as one native form (FormData), every question's inputs included, so the page
// owns saving and what happens next.

type Choice = { readonly value: string; readonly disabled?: boolean };
type ItemConfig = { readonly name: string; readonly required?: boolean; readonly multiple?: boolean; readonly choices?: readonly Choice[] };
type Shortcuts = "numbers" | "letters" | "none";

type QuestionnaireContextValue = {
  order: string[];
  active: string | undefined;
  goTo: (name: string) => void;
  shortcuts: Shortcuts;
  answers: Record<string, string[]>;
  other: Record<string, string>;
  errors: Record<string, string | undefined>;
  configs: React.RefObject<Map<string, ItemConfig>>;
  register: (config: ItemConfig) => () => void;
  setAnswer: (name: string, value: string, checked: boolean) => void;
  setOther: (name: string, text: string) => void;
  next: () => void;
  previous: () => void;
  skip: () => void;
};

const QuestionnaireContext = React.createContext<QuestionnaireContextValue | null>(null);
const ItemContext = React.createContext<{ name: string; required: boolean; multiple: boolean; id: string } | null>(null);

function useQuestionnaire() {
  const context = React.useContext(QuestionnaireContext);
  if (!context) throw new Error("Questionnaire parts must be inside a Questionnaire.");
  return context;
}

function useItem() {
  const context = React.useContext(ItemContext);
  if (!context) throw new Error("This part must be inside a QuestionnaireItem.");
  return context;
}

const shortcutKey = (index: number, shortcuts: Shortcuts) => (shortcuts === "none" || index > 8 ? undefined : shortcuts === "letters" ? String.fromCharCode(65 + index) : String(index + 1));

/**
 * The questions and their answers. Pass `items` (name, required, multiple, choices) so the order, progress and shortcuts
 * are known on the server; without it the order is the QuestionnaireItems' order. `defaultItem` or a controlled
 * `item` with `onItemChange` picks the question shown. `shortcuts` puts 1 to 9 (or A to I) on the choices.
 */
function Questionnaire({
  items,
  defaultItem,
  item,
  onItemChange,
  shortcuts = "numbers",
  className,
  onSubmit,
  onKeyDown,
  children,
  ...props
}: Omit<React.ComponentProps<"form">, "onSubmit"> & {
  items?: readonly ItemConfig[];
  defaultItem?: string;
  item?: string;
  onItemChange?: (name: string) => void;
  shortcuts?: Shortcuts;
  onSubmit?: (event: React.FormEvent<HTMLFormElement>) => void;
}) {
  const form = React.useRef<HTMLFormElement>(null);
  const configs = React.useRef(new Map<string, ItemConfig>(items?.map((i) => [i.name, i])));
  const [registered, setRegistered] = React.useState<string[]>([]);
  const order = items ? items.map((i) => i.name) : registered;
  const [internal, setInternal] = React.useState(defaultItem ?? items?.[0]?.name);
  const active = item ?? internal ?? order[0];
  const [answers, setAnswers] = React.useState<Record<string, string[]>>({});
  const [other, setOtherState] = React.useState<Record<string, string>>({});
  const [errors, setErrors] = React.useState<Record<string, string | undefined>>({});

  const focusItem = React.useCallback((name: string) => {
    requestAnimationFrame(() => {
      const fieldset = form.current?.querySelector<HTMLElement>(`[data-slot=questionnaire-item][data-name="${CSS.escape(name)}"]`);
      fieldset?.querySelector<HTMLElement>("input:not(:disabled)")?.focus({ preventScroll: true });
    });
  }, []);

  const goTo = React.useCallback(
    (name: string) => {
      if (item === undefined) setInternal(name);
      onItemChange?.(name);
      focusItem(name);
    },
    [item, onItemChange, focusItem],
  );

  const register = React.useCallback((config: ItemConfig) => {
    configs.current.set(config.name, { ...configs.current.get(config.name), ...config });
    setRegistered((list) => (list.includes(config.name) ? list : [...list, config.name]));
    return () => setRegistered((list) => list.filter((n) => n !== config.name));
  }, []);

  const answered = React.useCallback((name: string) => (answers[name]?.length ?? 0) > 0 || (other[name] ?? "").trim() !== "", [answers, other]);

  const validate = React.useCallback(
    (name: string) => {
      const config = configs.current.get(name);
      const message = config?.required && !answered(name) ? (config.multiple ? "Choose at least one answer to continue." : "Choose an answer to continue.") : undefined;
      setErrors((e) => ({ ...e, [name]: message }));
      return !message;
    },
    [answered],
  );

  const setAnswer = React.useCallback((name: string, value: string, checked: boolean) => {
    const multiple = configs.current.get(name)?.multiple;
    setAnswers((a) => ({ ...a, [name]: multiple ? (checked ? [...(a[name] ?? []).filter((v) => v !== value), value] : (a[name] ?? []).filter((v) => v !== value)) : checked ? [value] : [] }));
    if (!multiple && checked) setOtherState((o) => ({ ...o, [name]: "" }));
    setErrors((e) => ({ ...e, [name]: undefined }));
  }, []);

  const setOther = React.useCallback((name: string, text: string) => {
    setOtherState((o) => ({ ...o, [name]: text }));
    if (text && !configs.current.get(name)?.multiple) setAnswers((a) => ({ ...a, [name]: [] }));
    setErrors((e) => ({ ...e, [name]: undefined }));
  }, []);

  const index = active ? order.indexOf(active) : -1;
  const next = React.useCallback(() => {
    if (!active || !validate(active)) return focusItem(active ?? "");
    const following = order[index + 1];
    if (following) goTo(following);
  }, [active, validate, focusItem, order, index, goTo]);
  const previous = React.useCallback(() => {
    const before = order[index - 1];
    if (before) goTo(before);
  }, [order, index, goTo]);
  const skip = React.useCallback(() => {
    if (!active) return;
    setAnswers((a) => ({ ...a, [active]: [] }));
    setOtherState((o) => ({ ...o, [active]: "" }));
    setErrors((e) => ({ ...e, [active]: undefined }));
    const following = order[index + 1];
    if (following) goTo(following);
  }, [active, order, index, goTo]);

  const value = React.useMemo<QuestionnaireContextValue>(
    () => ({ order, active, goTo, shortcuts, answers, other, errors, configs, register, setAnswer, setOther, next, previous, skip }),
    [order, active, goTo, shortcuts, answers, other, errors, register, setAnswer, setOther, next, previous, skip],
  );

  return (
    <QuestionnaireContext.Provider value={value}>
      <form
        ref={form}
        data-slot="questionnaire"
        noValidate
        className={cn("flex w-full min-w-0 flex-col gap-4", className)}
        onSubmit={(event) => {
          // Every required question must be answered; the first one that is not is shown with its message.
          const missing = order.find((name) => !validate(name));
          if (missing) {
            event.preventDefault();
            goTo(missing);
            return;
          }
          onSubmit?.(event);
        }}
        onKeyDown={(event) => {
          onKeyDown?.(event);
          if (event.defaultPrevented || !active || event.metaKey || event.ctrlKey || event.altKey) return;
          const target = event.target as HTMLElement;
          const typing = target instanceof HTMLInputElement && target.type === "text";
          if (event.key === "Enter" && !(target instanceof HTMLButtonElement) && !(target instanceof HTMLTextAreaElement)) {
            event.preventDefault();
            if (index < order.length - 1) next();
            else form.current?.requestSubmit();
            return;
          }
          if (typing || shortcuts === "none") return;
          const key = event.key.toUpperCase();
          const at = shortcuts === "letters" ? key.charCodeAt(0) - 65 : Number(key) - 1;
          if (key.length !== 1 || Number.isNaN(at) || at < 0 || at > 8) return;
          const choice = form.current?.querySelectorAll<HTMLInputElement>(`[data-slot=questionnaire-item][data-name="${CSS.escape(active)}"] [data-slot=questionnaire-choice-input]`)[at];
          if (!choice || choice.disabled) return;
          event.preventDefault();
          choice.focus();
          setAnswer(active, choice.value, choice.type === "checkbox" ? !choice.checked : true);
        }}
        {...props}
      >
        {children}
      </form>
    </QuestionnaireContext.Provider>
  );
}

/** Where the person is: "Question 2 of 3" in the 11.5px mono at 64 percent ink over a 4px blue bar. */
function QuestionnaireProgress({ className, children, ...props }: React.ComponentProps<"div">) {
  const { order, active } = useQuestionnaire();
  const at = active ? order.indexOf(active) + 1 : 0;
  const total = order.length;
  return (
    <div data-slot="questionnaire-progress" className={cn("grid w-full gap-1.5", className)} {...props}>
      <span className="font-mono text-bar leading-tight text-fg-muted tabular-nums" aria-live="polite">
        {children ?? (total ? `Question ${at} of ${total}` : " ")}
      </span>
      <Progress value={total ? (at / total) * 100 : 0} aria-label="Progress" className="h-1" />
    </div>
  );
}

/** One question. Only the current one shows; the others stay in the form so their answers submit with it. */
function QuestionnaireItem({ name, required = false, multiple = false, className, children, ...props }: Omit<React.ComponentProps<"fieldset">, "name"> & { name: string; required?: boolean; multiple?: boolean }) {
  const { active, register, errors } = useQuestionnaire();
  const id = React.useId();
  React.useLayoutEffect(() => register({ name, required, multiple }), [register, name, required, multiple]);
  const context = React.useMemo(() => ({ name, required, multiple, id }), [name, required, multiple, id]);
  const isActive = active === name;
  return (
    <ItemContext.Provider value={context}>
      <fieldset
        data-slot="questionnaire-item"
        data-name={name}
        data-active={isActive || undefined}
        hidden={!isActive}
        aria-labelledby={`${id}-title`}
        aria-describedby={`${id}-description${errors[name] ? ` ${id}-error` : ""}`}
        aria-invalid={errors[name] ? true : undefined}
        className={cn("m-0 flex min-w-0 flex-col gap-4 border-0 p-0 outline-none", className)}
        {...props}
      >
        {children}
      </fieldset>
    </ItemContext.Provider>
  );
}

function QuestionnaireTitle({ className, ...props }: React.ComponentProps<"div">) {
  const { id } = useItem();
  return <div id={`${id}-title`} data-slot="questionnaire-title" className={cn("font-display text-h5 leading-snug font-medium tracking-tight text-pretty text-fg", className)} {...props} />;
}

function QuestionnaireDescription({ className, ...props }: React.ComponentProps<"p">) {
  const { id } = useItem();
  return <p id={`${id}-description`} data-slot="questionnaire-description" className={cn("-mt-3 font-sans text-body-sm leading-relaxed text-pretty text-fg-muted", className)} {...props} />;
}

/** The choices of a question, 8px apart; each direct QuestionnaireChoice gets its shortcut in order. */
function QuestionnaireChoices({ className, children, ...props }: React.ComponentProps<"div">) {
  const { multiple, id } = useItem();
  let index = 0;
  return (
    <div data-slot="questionnaire-choices" role={multiple ? "group" : "radiogroup"} aria-labelledby={`${id}-title`} className={cn("group/questionnaire-choices grid min-w-0 gap-2", className)} {...props}>
      {React.Children.map(children, (child) => (React.isValidElement<{ index?: number }>(child) && child.type === QuestionnaireChoice ? React.cloneElement(child, { index: index++ }) : child))}
    </div>
  );
}

function QuestionnaireChoice({ value, disabled = false, index = 0, className, children, ...props }: Omit<React.ComponentProps<"label">, "children"> & { value: string; disabled?: boolean; index?: number; children?: React.ReactNode }) {
  const { answers, setAnswer, shortcuts } = useQuestionnaire();
  const { name, multiple } = useItem();
  const checked = answers[name]?.includes(value) ?? false;
  const key = shortcutKey(index, shortcuts);
  return (
    <label
      data-slot="questionnaire-choice"
      data-type={multiple ? "checkbox" : "radio"}
      data-checked={checked || undefined}
      data-disabled={disabled || undefined}
      className={cn(
        "group/questionnaire-choice relative flex min-h-11 cursor-pointer items-start gap-2.5 rounded-control border border-hairline bg-surface px-3.5 py-2.5 text-start font-sans text-button leading-snug text-fg select-none transition-[background-color,border-color] duration-160 ease-out hover:bg-subtle has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-solid has-focus-visible:outline-focus-ring data-checked:border-line-brand data-checked:bg-blue-5 data-checked:hover:bg-blue-5 data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-disabled:opacity-50 motion-reduce:transition-none",
        className,
      )}
      {...props}
    >
      <input
        data-slot="questionnaire-choice-input"
        type={multiple ? "checkbox" : "radio"}
        name={name}
        value={value}
        checked={checked}
        disabled={disabled}
        onChange={(event) => setAnswer(name, value, event.target.checked)}
        className="peer sr-only"
      />
      <span
        aria-hidden
        data-slot="questionnaire-choice-indicator"
        className={cn(
          "pointer-events-none mt-px flex size-4.5 shrink-0 items-center justify-center border border-hairline bg-surface text-on-primary transition-[background-color,border-color] duration-160 ease-out motion-reduce:transition-none",
          multiple ? "rounded-menu group-data-checked/questionnaire-choice:border-line-brand-edge group-data-checked/questionnaire-choice:bg-primary" : "rounded-pill group-data-checked/questionnaire-choice:border-line-brand-edge after:size-2 after:scale-0 after:rounded-pill after:bg-primary after:transition-[scale] after:duration-160 after:ease-out group-data-checked/questionnaire-choice:after:scale-100 motion-reduce:after:transition-none",
        )}
      >
        {multiple && <Check strokeWidth={2.25} className="hidden size-3 group-data-checked/questionnaire-choice:block" />}
      </span>
      <span data-slot="questionnaire-choice-label" className="flex min-w-0 flex-1 flex-col gap-0.5">
        {children}
      </span>
      {key && (
        <Kbd data-slot="questionnaire-choice-shortcut" aria-hidden className="ml-auto hidden shrink-0 sm:inline-flex">
          {key}
        </Kbd>
      )}
    </label>
  );
}

/** A choice's second line: 14px at 64 percent ink. */
function QuestionnaireChoiceDescription({ className, ...props }: React.ComponentProps<"span">) {
  return <span data-slot="questionnaire-choice-description" className={cn("text-body-sm leading-relaxed text-fg-muted", className)} {...props} />;
}

/** A free answer typed instead of a choice (another answer); typing clears a single choice, choosing clears the text. */
function QuestionnaireInput({ className, ...props }: Omit<React.ComponentProps<typeof Input>, "name" | "value" | "onChange">) {
  const { other, setOther } = useQuestionnaire();
  const { name } = useItem();
  return (
    <div data-slot="questionnaire-input-wrapper" className="group/questionnaire-input relative w-full min-w-0">
      <Input data-slot="questionnaire-input" type="text" name={name} value={other[name] ?? ""} onChange={(event) => setOther(name, event.target.value)} className={cn("h-11", className)} {...props} />
    </div>
  );
}

/** Why the question cannot be passed yet: 13px red, read out when it appears. */
function QuestionnaireError({ className, children, ...props }: React.ComponentProps<"p">) {
  const { errors } = useQuestionnaire();
  const { name, id } = useItem();
  const message = children ?? errors[name];
  if (!message) return null;
  return (
    <p id={`${id}-error`} role="alert" data-slot="questionnaire-error" className={cn("-mt-2 font-sans text-snippet leading-snug text-danger", className)} {...props}>
      {message}
    </p>
  );
}

/** The buttons under the question: Previous at the left, then Skip and Next (or Submit on the last question). */
function QuestionnaireActions({ className, ...props }: React.ComponentProps<"div">) {
  return <div data-slot="questionnaire-actions" className={cn("grid min-h-11 w-full grid-cols-[minmax(0,1fr)_auto_auto] items-center gap-2", className)} {...props} />;
}

type ActionProps = Omit<React.ComponentProps<typeof Button>, "onClick" | "type"> & { onClick?: React.MouseEventHandler<HTMLButtonElement> };

function QuestionnairePrevious({ children, className, variant = "secondary", onClick, ...props }: ActionProps) {
  const { order, active, previous } = useQuestionnaire();
  const first = !active || order.indexOf(active) <= 0;
  return (
    <Button
      data-slot="questionnaire-previous"
      variant={variant}
      disabled={first}
      className={cn("col-start-1 row-start-1 justify-self-start", first && "invisible", className)}
      onClick={(event) => {
        onClick?.(event);
        if (!event.defaultPrevented) previous();
      }}
      {...props}
    >
      {children ?? "Previous"}
    </Button>
  );
}

function QuestionnaireSkip({ children, className, variant = "ghost", onClick, ...props }: ActionProps) {
  const { active, configs, order, skip } = useQuestionnaire();
  const required = active ? configs.current.get(active)?.required : true;
  const last = !active || order.indexOf(active) === order.length - 1;
  if (required || last) return null;
  return (
    <Button
      data-slot="questionnaire-skip"
      variant={variant}
      className={cn("col-start-2 row-start-1 justify-self-end", className)}
      onClick={(event) => {
        onClick?.(event);
        if (!event.defaultPrevented) skip();
      }}
      {...props}
    >
      {children ?? "Skip"}
    </Button>
  );
}

function QuestionnaireNext({ children, className, variant = "primary", onClick, ...props }: ActionProps) {
  const { order, active, next } = useQuestionnaire();
  if (!active || order.indexOf(active) >= order.length - 1) return null;
  return (
    <Button
      data-slot="questionnaire-next"
      variant={variant}
      className={cn("col-start-3 row-start-1 justify-self-end", className)}
      onClick={(event) => {
        onClick?.(event);
        if (!event.defaultPrevented) next();
      }}
      {...props}
    >
      {children ?? "Next"}
    </Button>
  );
}

function QuestionnaireSubmit({ children, className, variant = "primary", ...props }: Omit<React.ComponentProps<typeof Button>, "type">) {
  const { order, active } = useQuestionnaire();
  if (!active || order.indexOf(active) !== order.length - 1) return null;
  return (
    <Button data-slot="questionnaire-submit" type="submit" variant={variant} className={cn("col-start-3 row-start-1 justify-self-end", className)} {...props}>
      {children ?? "Submit"}
    </Button>
  );
}

export {
  Questionnaire,
  QuestionnaireActions,
  QuestionnaireChoice,
  QuestionnaireChoiceDescription,
  QuestionnaireChoices,
  QuestionnaireDescription,
  QuestionnaireError,
  QuestionnaireInput,
  QuestionnaireItem,
  QuestionnaireNext,
  QuestionnairePrevious,
  QuestionnaireProgress,
  QuestionnaireSkip,
  QuestionnaireSubmit,
  QuestionnaireTitle,
};

```
