{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ask",
  "title": "Ask",
  "description": "A questionnaire wrap with HITL onResult for AI SDK addToolOutput.",
  "dependencies": [
    "sonner"
  ],
  "registryDependencies": [
    "questionnaire",
    "card",
    "button",
    "popover",
    "kbd",
    "sonner"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/ask/ask.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  Questionnaire as QuestionnairePrimitive,\n  type QuestionnaireItemStatus,\n} from \"@shadcn/react/questionnaire\"\nimport {\n  ArrowDownIcon,\n  ArrowLeftIcon,\n  ArrowRightIcon,\n  ArrowUpIcon,\n  CheckIcon,\n  CornerDownLeftIcon,\n  XIcon,\n} from \"lucide-react\"\n\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Card,\n  CardAction,\n  CardContent,\n  CardDescription,\n  CardFooter,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\"\nimport { Kbd, KbdGroup } from \"@/components/ui/kbd\"\nimport {\n  Questionnaire,\n  QuestionnaireActions,\n  QuestionnaireChoices,\n  QuestionnaireDescription,\n  QuestionnaireError,\n  QuestionnaireItem,\n  QuestionnaireNext,\n  QuestionnairePrevious,\n  QuestionnaireProgress,\n  QuestionnaireSkip,\n  QuestionnaireSubmit,\n  QuestionnaireTitle,\n} from \"@/components/ui/questionnaire\"\nimport {\n  Popover,\n  PopoverContent,\n  PopoverDescription,\n  PopoverHeader,\n  PopoverTitle,\n  PopoverTrigger,\n} from \"@/components/ui/popover\"\nimport { toast } from \"sonner\"\nimport { cn } from \"@/lib/utils\"\nimport {\n  useInteractiveFocusRegistration,\n  isOwnedPortaledOverlay,\n  rootHasOpenPortaledOverlay,\n  type InteractiveFocusTrigger,\n} from \"@/registry/new-york/blocks/ask/interactive-focus\"\n\nexport {\n  InteractiveFocusProvider,\n  InteractiveFocusSurface,\n  useInteractiveFocusSurface,\n} from \"@/registry/new-york/blocks/ask/interactive-focus\"\nexport type { InteractiveFocusTrigger } from \"@/registry/new-york/blocks/ask/interactive-focus\"\n\nconst DEFAULT_AUTO_ADVANCE_DELAY_MS = 380\n\nconst CARD_ROW_CLASS =\n  \"group/questionnaire-choice relative flex min-h-11 items-center justify-between gap-3 rounded-md border border-input px-2.5 py-2 text-start text-sm transition-[color,background-color] outline-none select-none hover:bg-accent/60 has-[>input:focus-visible]:ring-1 has-[>input:focus-visible]:ring-ring/70 sm:border-transparent sm:px-2 sm:py-1.5\"\n\nconst PLAIN_ROW_CLASS =\n  \"group/questionnaire-choice relative flex min-h-11 items-start justify-between gap-3 rounded-lg border border-input bg-transparent px-3 py-2.5 text-start text-sm transition-colors outline-none select-none hover:bg-muted/50 has-[>input:focus-visible]:border-foreground/40 data-checked:border-primary/40 data-checked:bg-muted\"\n\nconst TOUCH_ACTION_CLASS = \"min-h-11 sm:min-h-0\"\n\nconst BATCH_BADGE_CLASS =\n  \"inline-flex size-6 shrink-0 items-center justify-center rounded-md border font-mono text-xs font-medium\"\n\ntype OtherDraft = {\n  text: string\n  committed: boolean\n}\n\ntype OtherTrailingAction = \"commit\" | \"deselect-keep-text\" | \"focus-input\"\n\nfunction emptyOtherDraft(): OtherDraft {\n  return { text: \"\", committed: false }\n}\n\nfunction resolveOtherTrailingAction(args: {\n  committed: boolean\n  focused: boolean\n  text: string\n}): OtherTrailingAction {\n  if (args.focused && args.text.trim().length > 0) return \"commit\"\n  if (args.committed && !args.focused) return \"deselect-keep-text\"\n  return \"focus-input\"\n}\n\nexport type AskChoice = {\n  value: string\n  label: string\n  /** Optional subtext under the answer label. */\n  description?: string\n}\n\nexport type AskVariant = \"card\" | \"plain\"\n\ntype AskItemBase = {\n  name: string\n  title: string\n  description?: string\n  required?: boolean\n  choices: AskChoice[]\n  input?: {\n    label: string\n    placeholder?: string\n  }\n}\n\nexport type AskItem =\n  | (AskItemBase & {\n      multiple?: false\n      /** After a single choice, go to the next slide (or review). Not valid on `multiple`. */\n      autoAdvance?: boolean\n    })\n  | (AskItemBase & {\n      multiple: true\n    })\n\nexport type AskLabels = {\n  previous?: string\n  next?: string\n  skip?: string\n  submit?: string\n  review?: string\n  cancel?: string\n  cancelTitle?: string\n  cancelDescription?: string\n  cancelConfirm?: string\n  cancelKeep?: string\n}\n\nexport type AskAnswer = {\n  name: string\n  title: string\n  value: string | string[] | null\n  label: string\n}\n\nexport type AskResult =\n  | { status: \"submitted\"; answers: AskAnswer[] }\n  | { status: \"canceled\" }\n\nexport type AskProps = {\n  items: AskItem[]\n  className?: string\n  onSubmit?: (event: React.FormEvent<HTMLFormElement>) => void\n  /**\n   * HITL-shaped result. Pass this straight to `addToolOutput({ output })`.\n   * Submit → `{ status: \"submitted\", answers }`. Cancel → `{ status: \"canceled\" }`.\n   */\n  onResult?: (result: AskResult) => void\n  /** After the last question, show a review step before submit. Default false. */\n  review?: boolean\n  /** Show a confirm-to-cancel control. Default false. */\n  cancel?: boolean\n  onCancel?: () => void\n  autoAdvanceDelay?: number\n  /**\n   * Visual shell. `card` keeps the Card chrome (default).\n   * `plain` drops the card background and uses bordered answer rows\n   * (shadcn Questionnaire look).\n   */\n  variant?: AskVariant\n  /**\n   * Show a success toast when the batch is submitted.\n   * Requires a root `<Toaster />` from `@/components/ui/sonner`.\n   * Default false.\n   */\n  toastOnSubmit?: boolean\n  /** Answer shortcut keys on choices. Default `\"numbers\"`. */\n  shortcuts?: \"numbers\" | \"letters\" | false\n  /**\n   * While Command (Meta) or Ctrl is held, show Kbd hints inline on\n   * Previous / Skip / Next / Submit. Default true. Only applies when\n   * `shortcuts` is not `false`.\n   */\n  shortcutHints?: boolean\n  /**\n   * Gate keyboard shortcuts behind interactive focus. Clicking the Ask\n   * card (or any `focusTriggers` / shared focus surface) activates it.\n   * With `InteractiveFocusProvider`, Tab cycles cards by `focusPriority`.\n   * Default false.\n   */\n  focusable?: boolean\n  /**\n   * When multiple focusable cards share a trigger (e.g. chat background),\n   * higher priority wins. Default 0.\n   */\n  focusPriority?: number\n  /**\n   * Extra elements that activate this Ask when clicked (chat pane, preview\n   * chrome, etc.). Shared surfaces from `InteractiveFocusSurface` are\n   * included automatically.\n   */\n  focusTriggers?: InteractiveFocusTrigger[]\n  /** Fires when interactive focus becomes active or inactive. */\n  onFocusChange?: (focused: boolean) => void\n  labels?: AskLabels\n  defaultItem?: string\n  item?: string\n  onItemChange?: (item: string) => void\n}\n\nexport const ASK_SHORTCUTS = {\n  previous: \"ArrowLeft\",\n  next: \"ArrowRight\",\n  skip: \"ArrowRight\",\n  submit: \"Enter\",\n} as const\n\nfunction itemAutoAdvances(item: AskItem) {\n  if (item.multiple) return false\n  return item.autoAdvance === true\n}\n\nfunction otherBadge(\n  item: AskItem,\n  shortcuts: AskProps[\"shortcuts\"],\n) {\n  const index = item.choices.length\n  if (shortcuts === \"letters\") {\n    return index < 26 ? String.fromCharCode(65 + index) : String(index + 1)\n  }\n  return String(index + 1)\n}\n\nfunction otherShortcutKey(\n  item: AskItem,\n  shortcuts: AskProps[\"shortcuts\"],\n) {\n  if (shortcuts === false || shortcuts == null) return null\n  const index = item.choices.length\n  if (shortcuts === \"numbers\") {\n    return index < 9 ? String(index + 1) : null\n  }\n  return index < 26 ? String.fromCharCode(65 + index) : null\n}\n\ntype ItemSelection = string | string[] | null\n\nfunction emptySelection(item: AskItem): ItemSelection {\n  return item.multiple ? [] : null\n}\n\nfunction isChoiceSelected(\n  item: AskItem,\n  choiceValue: string,\n  selection: Record<string, ItemSelection>,\n) {\n  const value = selection[item.name]\n  if (item.multiple) {\n    return Array.isArray(value) && value.includes(choiceValue)\n  }\n  return value === choiceValue\n}\n\nfunction selectedValues(value: ItemSelection | undefined): string[] {\n  return Array.isArray(value) ? value : []\n}\n\nfunction isEditableTarget(target: EventTarget | null) {\n  if (!(target instanceof HTMLElement)) return false\n  if (target.isContentEditable) return true\n  if (target instanceof HTMLTextAreaElement) return true\n  if (!(target instanceof HTMLInputElement)) return false\n  return ![\"button\", \"checkbox\", \"radio\", \"reset\", \"submit\"].includes(\n    target.type,\n  )\n}\n\nfunction isChoiceInput(\n  target: EventTarget | null,\n): target is HTMLInputElement {\n  return (\n    target instanceof HTMLInputElement &&\n    (target.type === \"radio\" || target.type === \"checkbox\")\n  )\n}\n\nfunction clickSlot(\n  form: HTMLFormElement | null,\n  slot: \"previous\" | \"skip\" | \"next\" | \"submit\",\n) {\n  const button = form?.querySelector<HTMLButtonElement>(\n    `[data-slot=questionnaire-${slot}]`,\n  )\n  if (!button || button.disabled || button.hidden) return false\n  if (button.getAttribute(\"aria-hidden\") === \"true\") return false\n  button.click()\n  return true\n}\n\nfunction activeChoiceInputs(form: HTMLFormElement | null) {\n  if (!form) return []\n  const items = [\n    ...form.querySelectorAll<HTMLElement>(\"[data-slot=questionnaire-item]\"),\n  ]\n  const active = items.find((item) => !item.hidden)\n  if (!active) return []\n  const choices = [\n    ...active.querySelectorAll<HTMLInputElement>(\n      \"[data-slot=questionnaire-choice-input]\",\n    ),\n  ].filter((input) => !input.disabled)\n  const other = active.querySelector<HTMLInputElement>(\n    \"[data-slot=questionnaire-other-row] input:not([disabled])\",\n  )\n  return other ? [...choices, other] : choices\n}\n\nfunction moveChoiceFocus(form: HTMLFormElement | null, delta: 1 | -1) {\n  const inputs = activeChoiceInputs(form)\n  if (inputs.length === 0) return\n  const currentIndex = inputs.findIndex(\n    (input) => input === document.activeElement,\n  )\n  const nextIndex =\n    currentIndex < 0\n      ? delta === 1\n        ? 0\n        : inputs.length - 1\n      : (currentIndex + delta + inputs.length) % inputs.length\n  inputs[nextIndex]?.focus()\n}\n\nfunction useModifierHeld(enabled = true) {\n  const [held, setHeld] = React.useState(false)\n\n  React.useEffect(() => {\n    if (!enabled) {\n      setHeld(false)\n      return\n    }\n    const sync = (event: KeyboardEvent) => {\n      setHeld(event.metaKey || event.ctrlKey)\n    }\n    const onBlur = () => setHeld(false)\n\n    window.addEventListener(\"keydown\", sync)\n    window.addEventListener(\"keyup\", sync)\n    window.addEventListener(\"blur\", onBlur)\n    return () => {\n      window.removeEventListener(\"keydown\", sync)\n      window.removeEventListener(\"keyup\", sync)\n      window.removeEventListener(\"blur\", onBlur)\n    }\n  }, [enabled])\n\n  return enabled ? held : false\n}\n\nfunction AskProgress({ className }: { className?: string }) {\n  return (\n    <QuestionnaireProgress\n      className={cn(\n        \"min-w-0 whitespace-nowrap text-sm font-normal tabular-nums\",\n        className,\n      )}\n      render={(props, state) => (\n        <div\n          {...props}\n          aria-label={`Question ${state.current} of ${state.total}`}\n        >\n          <span className=\"sm:hidden\">\n            {state.current}/{state.total}\n          </span>\n          <span className=\"hidden sm:inline\">\n            Question {state.current} of {state.total}\n          </span>\n        </div>\n      )}\n    />\n  )\n}\n\nfunction AskOptionRow({\n  children,\n  description,\n  className,\n  isPending = false,\n  showHoverArrow = false,\n  variant = \"card\",\n  ...props\n}: React.ComponentProps<typeof QuestionnairePrimitive.Choice> & {\n  description?: string\n  isPending?: boolean\n  showHoverArrow?: boolean\n  variant?: AskVariant\n}) {\n  const plain = variant === \"plain\"\n  const hasDescription = Boolean(description)\n\n  return (\n    <QuestionnairePrimitive.Choice\n      data-slot=\"questionnaire-choice\"\n      className={cn(\n        plain ? PLAIN_ROW_CLASS : CARD_ROW_CLASS,\n        !plain &&\n          \"cursor-default data-checked:bg-accent data-checked:text-accent-foreground sm:cursor-pointer\",\n        plain &&\n          \"cursor-default data-checked:text-accent-foreground sm:cursor-pointer\",\n        \"data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-disabled:opacity-50\",\n        hasDescription && !plain && \"items-start\",\n        isPending && \"ring-1 ring-primary/50\",\n        className,\n      )}\n      {...props}\n    >\n      <QuestionnairePrimitive.ChoiceInput\n        data-slot=\"questionnaire-choice-input\"\n        className=\"absolute inset-0 size-full cursor-default opacity-0 sm:cursor-pointer\"\n      />\n      <QuestionnairePrimitive.ChoiceLabel\n        data-slot=\"questionnaire-choice-label\"\n        className={cn(\n          \"min-w-0 flex-1 leading-snug\",\n          hasDescription && \"flex flex-col gap-0.5\",\n        )}\n      >\n        <span className={cn(hasDescription && \"font-medium\")}>{children}</span>\n        {description ? (\n          <span\n            data-slot=\"questionnaire-choice-description\"\n            className=\"text-sm font-normal text-muted-foreground\"\n          >\n            {description}\n          </span>\n        ) : null}\n      </QuestionnairePrimitive.ChoiceLabel>\n      <span className=\"relative size-6 shrink-0\">\n        <QuestionnairePrimitive.ChoiceShortcut\n          data-slot=\"questionnaire-choice-shortcut\"\n          className={cn(\n            BATCH_BADGE_CLASS,\n            \"pointer-events-none border-transparent text-muted-foreground group-data-checked/questionnaire-choice:border-primary group-data-checked/questionnaire-choice:bg-primary group-data-checked/questionnaire-choice:text-primary-foreground\",\n            hasDescription && \"translate-y-0.5\",\n            showHoverArrow && \"group-hover/questionnaire-choice:hidden\",\n          )}\n        />\n        {showHoverArrow ? (\n          <span\n            aria-hidden\n            className={cn(\n              \"pointer-events-none absolute inset-0 hidden items-center justify-center rounded-md bg-primary text-primary-foreground group-hover/questionnaire-choice:inline-flex\",\n              hasDescription && \"translate-y-0.5\",\n            )}\n          >\n            <ArrowRightIcon className=\"size-3\" />\n          </span>\n        ) : null}\n      </span>\n    </QuestionnairePrimitive.Choice>\n  )\n}\n\nfunction AskOtherRow({\n  name,\n  label,\n  placeholder = \"Other\",\n  badge,\n  disabled = false,\n  isPending = false,\n  committed,\n  text,\n  autoAdvance,\n  multiple = false,\n  variant = \"card\",\n  inputRef,\n  onTextChange,\n  onCommit,\n  onUncommitKeepText,\n  onResetDraft,\n  onFocusChange,\n}: {\n  name: string\n  label: string\n  placeholder?: string\n  badge: string\n  disabled?: boolean\n  isPending?: boolean\n  committed: boolean\n  text: string\n  autoAdvance: boolean\n  multiple?: boolean\n  variant?: AskVariant\n  inputRef: (node: HTMLInputElement | null) => void\n  onTextChange: (value: string) => void\n  onCommit: () => void\n  onUncommitKeepText: () => void\n  onResetDraft: (refocus: boolean) => void\n  onFocusChange: (focused: boolean) => void\n}) {\n  const localRef = React.useRef<HTMLInputElement | null>(null)\n  const [focused, setFocused] = React.useState(false)\n  const plain = variant === \"plain\"\n\n  function setInputNode(node: HTMLInputElement | null) {\n    localRef.current = node\n    inputRef(node)\n  }\n\n  React.useLayoutEffect(() => {\n    if (committed) return\n    const el = localRef.current\n    if (el && el.value !== text) el.value = text\n  }, [committed, text])\n\n  React.useLayoutEffect(() => {\n    const isFocus = localRef.current === document.activeElement\n    setFocused(isFocus)\n  }, [committed])\n\n  const showClear = focused && text.length > 0\n  const showCommitArrow =\n    focused && text.trim().length > 0 && autoAdvance\n  const showCommitCheck =\n    focused && text.trim().length > 0 && (multiple || !autoAdvance)\n  const highlighted = focused || committed || isPending\n  const trailingAction = resolveOtherTrailingAction({\n    committed,\n    focused,\n    text,\n  })\n\n  function handleTrailingMouseDown(event: React.MouseEvent) {\n    if (trailingAction === \"commit\" || trailingAction === \"deselect-keep-text\") {\n      event.preventDefault()\n    }\n  }\n\n  function handleTrailingClick() {\n    if (disabled) return\n    if (trailingAction === \"commit\") {\n      onCommit()\n      return\n    }\n    if (trailingAction === \"deselect-keep-text\") {\n      onUncommitKeepText()\n      localRef.current?.blur()\n      return\n    }\n    const input = localRef.current\n    if (!input) return\n    input.focus()\n    const end = input.value.length\n    input.setSelectionRange(end, end)\n  }\n\n  return (\n    <div\n      data-slot=\"questionnaire-other-row\"\n      className={cn(\n        plain ? PLAIN_ROW_CLASS : CARD_ROW_CLASS,\n        plain\n          ? (committed || isPending) &&\n              \"border-primary/40 bg-muted text-accent-foreground\"\n          : highlighted && \"bg-accent text-accent-foreground\",\n        isPending && \"ring-1 ring-primary/50\",\n        disabled && \"cursor-not-allowed opacity-50\",\n      )}\n      onClick={(event) => {\n        if (disabled) return\n        if (event.target instanceof HTMLButtonElement) return\n        localRef.current?.focus()\n      }}\n    >\n      {committed ? (\n        <QuestionnairePrimitive.Input\n          key={`${name}-committed`}\n          ref={setInputNode}\n          aria-label={label}\n          disabled={disabled}\n          placeholder={placeholder}\n          value={text}\n          onBlur={() => {\n            setFocused(false)\n            onFocusChange(false)\n          }}\n          onChange={(event) => {\n            const next = event.currentTarget.value\n            onTextChange(next)\n            if (next.trim().length === 0) onUncommitKeepText()\n          }}\n          onFocus={() => {\n            setFocused(true)\n            onFocusChange(true)\n          }}\n          onKeyDown={(event) => {\n            if (event.key === \"ArrowLeft\" || event.key === \"ArrowRight\") {\n              event.stopPropagation()\n              return\n            }\n            if (event.key === \"Backspace\" && text.length === 0) {\n              event.preventDefault()\n              event.stopPropagation()\n              onResetDraft(false)\n              localRef.current?.blur()\n              return\n            }\n            if (event.key === \"Enter\") {\n              event.preventDefault()\n              event.stopPropagation()\n              onCommit()\n            }\n          }}\n          className=\"min-w-0 flex-1 border-0 bg-transparent p-0 text-sm shadow-none outline-none placeholder:text-muted-foreground focus-visible:ring-0 disabled:cursor-not-allowed\"\n        />\n      ) : (\n        <QuestionnairePrimitive.Input\n          key={`${name}-draft`}\n          ref={setInputNode}\n          aria-label={label}\n          disabled={disabled}\n          placeholder={placeholder}\n          onBlur={() => {\n            setFocused(false)\n            onFocusChange(false)\n          }}\n          onChange={(event) => {\n            event.preventDefault()\n            onTextChange(event.currentTarget.value)\n          }}\n          onFocus={() => {\n            setFocused(true)\n            onFocusChange(true)\n          }}\n          onKeyDown={(event) => {\n            if (event.key === \"ArrowLeft\" || event.key === \"ArrowRight\") {\n              event.stopPropagation()\n              return\n            }\n            if (event.key === \"Backspace\" && text.length === 0) {\n              event.preventDefault()\n              event.stopPropagation()\n              onResetDraft(false)\n              localRef.current?.blur()\n              return\n            }\n            if (event.key === \"Enter\") {\n              event.preventDefault()\n              event.stopPropagation()\n              onCommit()\n            }\n          }}\n          className=\"min-w-0 flex-1 border-0 bg-transparent p-0 text-sm shadow-none outline-none placeholder:text-muted-foreground focus-visible:ring-0 disabled:cursor-not-allowed\"\n        />\n      )}\n      {showClear ? (\n        <button\n          type=\"button\"\n          tabIndex={-1}\n          disabled={disabled}\n          aria-label=\"Clear Other text\"\n          className={cn(\n            BATCH_BADGE_CLASS,\n            \"cursor-pointer border-transparent text-muted-foreground hover:bg-accent hover:text-foreground disabled:cursor-not-allowed\",\n          )}\n          onMouseDown={(event) => event.preventDefault()}\n          onClick={() => onResetDraft(true)}\n        >\n          <XIcon className=\"size-3.5\" />\n        </button>\n      ) : null}\n      <button\n        type=\"button\"\n        tabIndex={-1}\n        disabled={disabled}\n        aria-label={\n          showCommitCheck || showCommitArrow\n            ? \"Save Other answer\"\n            : committed && !focused\n              ? \"Other — click to deselect and keep your text\"\n              : `Other option ${badge}`\n        }\n        className={cn(\n          BATCH_BADGE_CLASS,\n          \"cursor-pointer disabled:cursor-not-allowed\",\n          committed && !focused\n            ? \"border-primary bg-primary text-primary-foreground\"\n            : \"border-transparent bg-transparent text-muted-foreground\",\n          (showCommitCheck || showCommitArrow) &&\n            \"border-primary bg-primary text-primary-foreground\",\n        )}\n        onMouseDown={handleTrailingMouseDown}\n        onClick={handleTrailingClick}\n      >\n        {showCommitArrow ? (\n          <ArrowRightIcon className=\"size-3\" />\n        ) : showCommitCheck ? (\n          <CheckIcon className=\"size-3.5\" />\n        ) : (\n          <span>{badge}</span>\n        )}\n      </button>\n    </div>\n  )\n}\n\nfunction skippedAnswer(item: AskItem): AskAnswer {\n  return {\n    name: item.name,\n    title: item.title,\n    value: null,\n    label: \"Skipped\",\n  }\n}\n\nfunction labelsForValues(item: AskItem, values: string[]) {\n  return values.map(\n    (value) =>\n      item.choices.find((choice) => choice.value === value)?.label ?? value,\n  )\n}\n\nfunction committedOtherText(\n  itemName: string,\n  otherDrafts: Record<string, OtherDraft>,\n) {\n  const draft = otherDrafts[itemName]\n  if (!draft?.committed) return \"\"\n  return draft.text.trim()\n}\n\nfunction readAnswers(\n  form: HTMLFormElement,\n  items: AskItem[],\n  selection: Record<string, ItemSelection>,\n  otherDrafts: Record<string, OtherDraft>,\n): AskAnswer[] {\n  const data = new FormData(form)\n\n  return items.map((item) => {\n    const other = committedOtherText(item.name, otherDrafts)\n\n    if (item.multiple) {\n      const selected = selectedValues(selection[item.name])\n      const fromForm = data\n        .getAll(item.name)\n        .map(String)\n        .filter((value) => value.length > 0 && value !== other)\n      const values = selected.length > 0 ? selected : fromForm\n      if (values.length === 0 && !other) return skippedAnswer(item)\n      const labels = labelsForValues(item, values)\n      if (other) labels.push(other)\n      return {\n        name: item.name,\n        title: item.title,\n        value: other ? [...values, other] : values,\n        label: labels.join(\", \"),\n      }\n    }\n\n    if (other) {\n      return {\n        name: item.name,\n        title: item.title,\n        value: other,\n        label: other,\n      }\n    }\n\n    const selected = selection[item.name]\n    const fromForm = data.get(item.name)?.toString() ?? \"\"\n    const value =\n      typeof selected === \"string\" && selected.length > 0 ? selected : fromForm\n    if (!value) return skippedAnswer(item)\n    const choice = item.choices.find((entry) => entry.value === value)\n    return {\n      name: item.name,\n      title: item.title,\n      value,\n      label: choice?.label ?? value,\n    }\n  })\n}\n\nfunction useAutoAdvance(delay: number) {\n  const [pendingKey, setPendingKey] = React.useState<string | null>(null)\n  const timeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)\n\n  const clear = React.useCallback(() => {\n    if (timeoutRef.current) {\n      clearTimeout(timeoutRef.current)\n      timeoutRef.current = null\n    }\n    setPendingKey(null)\n  }, [])\n\n  React.useEffect(() => {\n    return () => {\n      if (timeoutRef.current) clearTimeout(timeoutRef.current)\n    }\n  }, [])\n\n  const schedule = React.useCallback(\n    (key: string, advance: () => void) => {\n      if (timeoutRef.current) {\n        clearTimeout(timeoutRef.current)\n        timeoutRef.current = null\n      }\n      setPendingKey(key)\n      timeoutRef.current = setTimeout(() => {\n        timeoutRef.current = null\n        setPendingKey(null)\n        advance()\n      }, delay)\n    },\n    [delay],\n  )\n\n  return { pendingKey, schedule, clear }\n}\n\nfunction ShortcutKey({ part }: { part: string }) {\n  if (part === \"ArrowLeft\") {\n    return (\n      <Kbd>\n        <ArrowLeftIcon aria-label=\"Left arrow\" />\n      </Kbd>\n    )\n  }\n  if (part === \"ArrowRight\") {\n    return (\n      <Kbd>\n        <ArrowRightIcon aria-label=\"Right arrow\" />\n      </Kbd>\n    )\n  }\n  if (part === \"ArrowUp\") {\n    return (\n      <Kbd>\n        <ArrowUpIcon aria-label=\"Up arrow\" />\n      </Kbd>\n    )\n  }\n  if (part === \"ArrowDown\") {\n    return (\n      <Kbd>\n        <ArrowDownIcon aria-label=\"Down arrow\" />\n      </Kbd>\n    )\n  }\n  if (part === \"Enter\") {\n    return (\n      <Kbd>\n        <CornerDownLeftIcon aria-label=\"Enter\" />\n      </Kbd>\n    )\n  }\n  return <Kbd>{part}</Kbd>\n}\n\nexport function AskShortcutKbd({\n  shortcut,\n  className,\n}: {\n  shortcut: string\n  className?: string\n}) {\n  const parts = shortcut.split(\"+\")\n\n  return (\n    <KbdGroup className={cn(\"align-middle\", className)}>\n      {parts.map((part, index) => (\n        <ShortcutKey key={`${shortcut}-${index}-${part}`} part={part} />\n      ))}\n    </KbdGroup>\n  )\n}\n\nfunction ActionShortcutHint({\n  shortcut,\n  visible,\n}: {\n  shortcut: string\n  visible: boolean\n}) {\n  if (!visible) return null\n\n  return (\n    <AskShortcutKbd\n      shortcut={shortcut}\n      className=\"shrink-0 **:data-[slot=kbd]:bg-current/15 **:data-[slot=kbd]:text-current\"\n    />\n  )\n}\n\nfunction CancelBatchButton({\n  labels,\n  onCancel,\n  className,\n}: {\n  labels?: AskLabels\n  onCancel?: () => void\n  className?: string\n}) {\n  const [open, setOpen] = React.useState(false)\n\n  return (\n    <Popover open={open} onOpenChange={setOpen}>\n      <PopoverTrigger asChild>\n        <Button\n          type=\"button\"\n          variant=\"ghost\"\n          size=\"icon\"\n          className={cn(\"size-11 sm:size-9\", className)}\n          aria-label={labels?.cancel ?? \"Cancel batch\"}\n        >\n          <XIcon />\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent align=\"end\" sideOffset={8} className=\"w-72\">\n        <PopoverHeader>\n          <PopoverTitle>\n            {labels?.cancelTitle ?? \"Cancel this batch?\"}\n          </PopoverTitle>\n          <PopoverDescription>\n            {labels?.cancelDescription ??\n              \"Your current answers in this batch will be discarded.\"}\n          </PopoverDescription>\n        </PopoverHeader>\n        <div className=\"flex justify-end gap-2\">\n          <Button\n            type=\"button\"\n            size=\"sm\"\n            variant=\"outline\"\n            onClick={() => setOpen(false)}\n          >\n            {labels?.cancelKeep ?? \"Keep answering\"}\n          </Button>\n          <Button\n            type=\"button\"\n            size=\"sm\"\n            onClick={() => {\n              setOpen(false)\n              onCancel?.()\n            }}\n          >\n            {labels?.cancelConfirm ?? \"Cancel batch\"}\n          </Button>\n        </div>\n      </PopoverContent>\n    </Popover>\n  )\n}\n\nexport function Ask({\n  items,\n  className,\n  onSubmit,\n  onResult,\n  review = false,\n  cancel = false,\n  onCancel,\n  autoAdvanceDelay = DEFAULT_AUTO_ADVANCE_DELAY_MS,\n  variant = \"card\",\n  toastOnSubmit = false,\n  shortcuts = \"numbers\",\n  shortcutHints = true,\n  focusable = false,\n  focusPriority = 0,\n  focusTriggers,\n  onFocusChange,\n  labels,\n  defaultItem,\n  item: itemProp,\n  onItemChange,\n}: AskProps) {\n  const plain = variant === \"plain\"\n  const firstName = items[0]?.name ?? \"\"\n  const lastName = items.at(-1)?.name\n  const formRef = React.useRef<HTMLFormElement>(null)\n  const { focused: interactiveFocused } = useInteractiveFocusRegistration({\n    enabled: focusable,\n    priority: focusPriority,\n    rootRef: formRef as React.RefObject<HTMLElement | null>,\n    triggers: focusTriggers,\n    onFocusChange,\n  })\n  const keyboardArmed = !focusable || interactiveFocused\n  const modifierHeld = useModifierHeld(keyboardArmed)\n  const showShortcutHints =\n    shortcuts !== false && shortcutHints && modifierHeld && keyboardArmed\n  const [phase, setPhase] = React.useState<\"questions\" | \"review\">(\"questions\")\n  const [reviewAnswers, setReviewAnswers] = React.useState<\n    AskAnswer[]\n  >([])\n  const [itemStatus, setItemStatus] = React.useState<\n    Partial<Record<string, QuestionnaireItemStatus>>\n  >({})\n  const [selection, setSelection] = React.useState<\n    Record<string, ItemSelection>\n  >({})\n  const selectionRef = React.useRef(selection)\n  const [otherDrafts, setOtherDrafts] = React.useState<\n    Record<string, OtherDraft>\n  >({})\n  const otherDraftsRef = React.useRef(otherDrafts)\n  const [otherFocusedName, setOtherFocusedName] = React.useState<string | null>(\n    null,\n  )\n  const otherInputRefs = React.useRef<Record<string, HTMLInputElement | null>>(\n    {},\n  )\n  const [uncontrolledItem, setUncontrolledItem] = React.useState(\n    () => defaultItem ?? firstName,\n  )\n  const activeItem = itemProp ?? uncontrolledItem\n  const activeItemRef = React.useRef(activeItem)\n\n  React.useEffect(() => {\n    selectionRef.current = selection\n  }, [selection])\n  React.useEffect(() => {\n    otherDraftsRef.current = otherDrafts\n  }, [otherDrafts])\n  React.useEffect(() => {\n    activeItemRef.current = activeItem\n  }, [activeItem])\n\n  const { pendingKey, schedule, clear } = useAutoAdvance(autoAdvanceDelay)\n\n  const setActiveItem = React.useCallback(\n    (next: string) => {\n      if (itemProp == null) setUncontrolledItem(next)\n      onItemChange?.(next)\n    },\n    [itemProp, onItemChange],\n  )\n\n  function handleItemChange(next: string) {\n    clear()\n    setActiveItem(next)\n  }\n\n  function restoreBatchKeyboard() {\n    requestAnimationFrame(() => {\n      formRef.current?.focus({ preventScroll: true })\n    })\n  }\n\n  function blurOther(name: string) {\n    otherInputRefs.current[name]?.blur()\n    setOtherFocusedName((current) => (current === name ? null : current))\n  }\n\n  function enterReview() {\n    clear()\n    if (formRef.current) {\n      setReviewAnswers(\n        readAnswers(\n          formRef.current,\n          items,\n          selectionRef.current,\n          otherDraftsRef.current,\n        ),\n      )\n    }\n    setPhase(\"review\")\n  }\n\n  React.useLayoutEffect(() => {\n    if (phase !== \"review\") return\n    formRef.current?.focus({ preventScroll: true })\n  }, [phase])\n\n  function leaveReview() {\n    setPhase(\"questions\")\n  }\n\n  function continueForward() {\n    if (showReviewNext) {\n      if (hasAnswer) enterReview()\n      return\n    }\n    if (clickSlot(formRef.current, \"next\")) return\n    clickSlot(formRef.current, \"submit\")\n  }\n\n  function handleAskKeyDown(\n    event: KeyboardEvent | React.KeyboardEvent<HTMLFormElement>,\n  ) {\n    if (!keyboardArmed) return\n    if (event.defaultPrevented) return\n    const composing =\n      \"nativeEvent\" in event\n        ? event.nativeEvent.isComposing\n        : event.isComposing\n    if (composing) return\n    if (event.metaKey || event.ctrlKey || event.altKey) return\n\n    const form = formRef.current\n    if (rootHasOpenPortaledOverlay(form)) return\n\n    const target = event.target\n    const key = event.key\n    // ↑/↓ still cycle choices (including Other) while the Other text field is focused.\n    const isVerticalChoiceNav = key === \"ArrowUp\" || key === \"ArrowDown\"\n    if (form && target instanceof Node && !form.contains(target)) {\n      if (isEditableTarget(target)) return\n      if (isOwnedPortaledOverlay(form, target)) return\n    } else if (isEditableTarget(target) && !isVerticalChoiceNav) {\n      return\n    }\n\n    if (phase === \"review\") {\n      if (key === \"ArrowLeft\") {\n        event.preventDefault()\n        leaveReview()\n      } else if (key === \"Enter\") {\n        event.preventDefault()\n        clickSlot(form, \"submit\")\n      }\n      return\n    }\n\n    if (pendingKey != null && (key === \"ArrowLeft\" || key === \"ArrowRight\")) {\n      event.preventDefault()\n      return\n    }\n\n    if (isVerticalChoiceNav) {\n      event.preventDefault()\n      event.stopPropagation()\n      moveChoiceFocus(form, key === \"ArrowDown\" ? 1 : -1)\n      return\n    }\n\n    if (key === \"ArrowLeft\") {\n      event.preventDefault()\n      event.stopPropagation()\n      clickSlot(form, \"previous\")\n      return\n    }\n\n    if (key === \"ArrowRight\") {\n      event.preventDefault()\n      event.stopPropagation()\n      if (nextIsShowing) {\n        if (showReviewNext) {\n          if (hasAnswer) enterReview()\n          return\n        }\n        clickSlot(form, \"next\")\n        return\n      }\n      if (!skipWouldSubmit) clickSlot(form, \"skip\")\n      return\n    }\n\n    if (\n      activeSlide?.input &&\n      otherShortcutKey(activeSlide, shortcuts) &&\n      key.toUpperCase() === otherShortcutKey(activeSlide, shortcuts)\n    ) {\n      event.preventDefault()\n      applyOtherTrailing(activeSlide)\n      return\n    }\n\n    if (key === \" \" && isChoiceInput(target)) {\n      event.preventDefault()\n      target.click()\n      return\n    }\n\n    if (key !== \"Enter\") return\n\n    if (isChoiceInput(target) && activeSlide) {\n      const focusedSelected = isChoiceSelected(\n        activeSlide,\n        target.value,\n        selection,\n      )\n      if (!hasAnswer || !focusedSelected) {\n        event.preventDefault()\n        target.click()\n        return\n      }\n      event.preventDefault()\n      continueForward()\n      return\n    }\n\n    if (hasAnswer) {\n      event.preventDefault()\n      continueForward()\n    }\n  }\n\n  const handleAskKeyDownRef = React.useRef(handleAskKeyDown)\n  handleAskKeyDownRef.current = handleAskKeyDown\n\n  React.useEffect(() => {\n    if (!focusable || !keyboardArmed) return\n    // Capture-phase so ←/→ never hit native radio group navigation.\n    const onKeyDown = (event: KeyboardEvent) => {\n      if (\n        event.key !== \"ArrowLeft\" &&\n        event.key !== \"ArrowRight\" &&\n        event.key !== \"ArrowUp\" &&\n        event.key !== \"ArrowDown\"\n      ) {\n        return\n      }\n      handleAskKeyDownRef.current(event)\n    }\n    window.addEventListener(\"keydown\", onKeyDown, true)\n    return () => window.removeEventListener(\"keydown\", onKeyDown, true)\n  }, [focusable, keyboardArmed])\n\n  function goToNextFrom(fromName: string) {\n    if (activeItemRef.current !== fromName) return\n    const index = items.findIndex((item) => item.name === fromName)\n    const next = items[index + 1]\n    if (next) {\n      setActiveItem(next.name)\n      return\n    }\n    if (review) enterReview()\n  }\n\n  function emitCancel() {\n    onResult?.({ status: \"canceled\" })\n    onCancel?.()\n  }\n\n  function handleSubmit(event: React.FormEvent<HTMLFormElement>) {\n    // Skip on the last item calls form.requestSubmit(). Intercept that\n    // while questions are still open so review is not bypassed.\n    if (review && phase === \"questions\") {\n      event.preventDefault()\n      enterReview()\n      return\n    }\n    event.preventDefault()\n    const answers = readAnswers(\n      event.currentTarget,\n      items,\n      selectionRef.current,\n      otherDraftsRef.current,\n    )\n    const result: AskResult = { status: \"submitted\", answers }\n    if (toastOnSubmit) {\n      const summary = answers\n        .map((answer) => `${answer.title}: ${answer.label}`)\n        .join(\" · \")\n      toast.success(\"Submitted\", {\n        description: summary || \"Batch submitted.\",\n      })\n    }\n    onResult?.(result)\n    onSubmit?.(event)\n  }\n\n  const collection = items.map((item) => ({\n    name: item.name,\n    required: item.required,\n    choices: item.choices.map((choice) => ({ value: choice.value })),\n  }))\n\n  const showReviewNext =\n    review && phase === \"questions\" && activeItem === lastName\n  const activeSlide = items.find((item) => item.name === activeItem)\n  const hasAnswer = itemStatus[activeItem ?? \"\"] === \"answered\"\n  const autoAdvanceSlide =\n    Boolean(activeSlide) && itemAutoAdvances(activeSlide!)\n  const hideAutoAdvanceNext =\n    autoAdvanceSlide && (!hasAnswer || pendingKey != null)\n  const nextIsShowing =\n    !hideAutoAdvanceNext && Boolean(showReviewNext || activeItem !== lastName)\n  const skipWouldSubmit = activeItem === lastName && !review\n  const skipHasArrow = !nextIsShowing && !skipWouldSubmit\n  const typingOtherSingle =\n    Boolean(activeSlide && !activeSlide.multiple && activeSlide.input) &&\n    (otherDrafts[activeItem ?? \"\"]?.text.length ?? 0) > 0\n  const hideSkip = pendingKey != null || typingOtherSingle\n  const hasPrevious =\n    phase === \"review\" || (items.length > 1 && activeItem !== firstName)\n  const hasSkip =\n    phase === \"questions\" &&\n    activeSlide != null &&\n    !activeSlide.required &&\n    !hideSkip\n  const hasNext = phase === \"questions\" && nextIsShowing\n  const hasSubmit =\n    phase === \"review\" ||\n    (phase === \"questions\" && activeItem === lastName && !review)\n  const showActions = hasPrevious || hasSkip || hasNext || hasSubmit\n  const backLabel = labels?.previous ?? \"Back\"\n  const skipLabel = labels?.skip ?? \"Skip\"\n  const nextLabel = labels?.next ?? \"Next\"\n  const showCancel = cancel\n\n  function clearItemSelection(name: string) {\n    const item = items.find((entry) => entry.name === name)\n    if (!item) return\n    setSelection((current) => ({\n      ...current,\n      [name]: emptySelection(item),\n    }))\n  }\n\n  function patchOtherDraft(name: string, patch: Partial<OtherDraft>) {\n    setOtherDrafts((current) => {\n      const previous = current[name] ?? emptyOtherDraft()\n      return {\n        ...current,\n        [name]: { ...previous, ...patch },\n      }\n    })\n  }\n\n  function resetOtherDraft(name: string) {\n    setOtherDrafts((current) => ({\n      ...current,\n      [name]: emptyOtherDraft(),\n    }))\n  }\n\n  function uncommitOther(item: AskItem, keepText = true) {\n    setOtherDrafts((current) => {\n      const previous = current[item.name] ?? emptyOtherDraft()\n      return {\n        ...current,\n        [item.name]: {\n          text: keepText ? previous.text : \"\",\n          committed: false,\n        },\n      }\n    })\n    if (!item.multiple && keepText) {\n      const selected = selectionRef.current[item.name]\n      if (\n        typeof selected === \"string\" &&\n        selected ===\n          committedOtherText(item.name, otherDraftsRef.current)\n      ) {\n        setSelection((current) => ({\n          ...current,\n          [item.name]: null,\n        }))\n      }\n    }\n    blurOther(item.name)\n    restoreBatchKeyboard()\n  }\n\n  function commitOther(item: AskItem) {\n    if (pendingKey != null) return\n    const raw = otherDraftsRef.current[item.name]?.text ?? \"\"\n    const trimmed = raw.trim()\n    if (!trimmed) {\n      resetOtherDraft(item.name)\n      if (!item.multiple) {\n        setSelection((current) => ({\n          ...current,\n          [item.name]: null,\n        }))\n      }\n      blurOther(item.name)\n      restoreBatchKeyboard()\n      return\n    }\n\n    setOtherDrafts((current) => ({\n      ...current,\n      [item.name]: { text: raw, committed: true },\n    }))\n\n    if (item.multiple) {\n      blurOther(item.name)\n      restoreBatchKeyboard()\n      return\n    }\n\n    setSelection((current) => ({\n      ...current,\n      [item.name]: trimmed,\n    }))\n    blurOther(item.name)\n    restoreBatchKeyboard()\n\n    const isLast = item.name === lastName\n    const hasNext = !isLast || review\n    if (!itemAutoAdvances(item) || !hasNext) return\n    schedule(`${item.name}:other`, () => goToNextFrom(item.name))\n  }\n\n  function applyOtherTrailing(item: AskItem) {\n    if (pendingKey != null) return\n    const draft = otherDrafts[item.name] ?? emptyOtherDraft()\n    const action = resolveOtherTrailingAction({\n      committed: draft.committed,\n      focused: otherFocusedName === item.name,\n      text: draft.text,\n    })\n    if (action === \"commit\") {\n      commitOther(item)\n      return\n    }\n    if (action === \"deselect-keep-text\") {\n      uncommitOther(item)\n      return\n    }\n    const input = otherInputRefs.current[item.name]\n    if (!input) return\n    input.focus()\n    const end = input.value.length\n    input.setSelectionRange(end, end)\n  }\n\n  return (\n    <Questionnaire\n      ref={formRef}\n      tabIndex={-1}\n      data-ask-focused={interactiveFocused ? \"true\" : \"false\"}\n      className={cn(\"w-full outline-none\", className)}\n      defaultItem={defaultItem}\n      item={activeItem || undefined}\n      items={collection}\n      shortcuts={shortcuts === false ? undefined : shortcuts}\n      onItemChange={handleItemChange}\n      onKeyDown={(event) => {\n        if (focusable && !keyboardArmed) {\n          // Form may still be focused briefly; don't let Questionnaire act.\n          event.preventDefault()\n          event.stopPropagation()\n          return\n        }\n        handleAskKeyDownRef.current(event)\n      }}\n      onSubmit={handleSubmit}\n    >\n      <Card\n        className={cn(\n          !plain && \"gap-2!\",\n          plain &&\n            \"gap-4 rounded-none bg-transparent py-0 ring-0 [--card-spacing:--spacing(0)] has-data-[slot=card-footer]:pb-0\",\n          showCancel && phase === \"questions\" && \"relative\",\n          showCancel && phase === \"questions\" && !plain && \"pt-2\",\n          focusable && !plain && \"bg-sidebar\",\n          focusable && !plain && !interactiveFocused && \"ring-0\",\n        )}\n      >\n        {showCancel && phase === \"questions\" ? (\n          <CancelBatchButton\n            className={cn(\n              \"absolute z-10\",\n              plain ? \"top-0 right-0\" : \"top-2 right-2\",\n            )}\n            labels={labels}\n            onCancel={emitCancel}\n          />\n        ) : null}\n        <div hidden={phase === \"review\"}>\n          {items.map((item) => {\n            const titleId = `ask-${item.name}-title`\n            const canAutoAdvance = itemAutoAdvances(item)\n            const isLast = item.name === lastName\n            const hasNext = !isLast || review\n\n            return (\n              <QuestionnaireItem\n                key={item.name}\n                aria-labelledby={titleId}\n                name={item.name}\n                required={item.required}\n                multiple={item.multiple}\n                onStatusChange={(status) => {\n                  setItemStatus((current) => ({\n                    ...current,\n                    [item.name]: status,\n                  }))\n                }}\n              >\n                <CardHeader\n                  className={cn(\n                    \"gap-0.5\",\n                    plain && \"rounded-none px-0\",\n                  )}\n                >\n                  {showCancel ? (\n                    <AskProgress className=\"pr-10\" />\n                  ) : null}\n                  <QuestionnaireTitle\n                    id={titleId}\n                    className=\"mb-0\"\n                    render={<CardTitle />}\n                  >\n                    {item.title}\n                  </QuestionnaireTitle>\n                  {showCancel ? null : (\n                    <CardAction className=\"row-span-1\">\n                      <AskProgress />\n                    </CardAction>\n                  )}\n                  {item.description ? (\n                    <QuestionnaireDescription\n                      className={cn(!showCancel && \"col-span-full\")}\n                      render={<CardDescription />}\n                    >\n                      {item.description}\n                    </QuestionnaireDescription>\n                  ) : null}\n                </CardHeader>\n                <CardContent className={cn(plain && \"px-0\")}>\n                  <QuestionnaireChoices\n                    className={cn(plain ? \"gap-2\" : \"gap-2 sm:gap-1\")}\n                  >\n                    {item.choices.map((choice) => {\n                      const choiceKey = `${item.name}:${choice.value}`\n                      const isPending = pendingKey === choiceKey\n                      const isSelected = isChoiceSelected(\n                        item,\n                        choice.value,\n                        selection,\n                      )\n                      const showHoverArrow =\n                        canAutoAdvance &&\n                        itemStatus[item.name] !== \"answered\" &&\n                        !isSelected\n\n                      return (\n                        <AskOptionRow\n                          key={choice.value}\n                          checked={isSelected}\n                          description={choice.description}\n                          disabled={pendingKey != null && !isPending}\n                          isPending={isPending}\n                          showHoverArrow={showHoverArrow}\n                          value={choice.value}\n                          variant={variant}\n                          onClick={() => {\n                            if (item.multiple || !isSelected) return\n                            clear()\n                            clearItemSelection(item.name)\n                          }}\n                          onChange={(event) => {\n                            const checked = event.currentTarget.checked\n                            const value = choice.value\n\n                            if (!item.multiple) {\n                              setOtherDrafts((current) => {\n                                const previous =\n                                  current[item.name] ?? emptyOtherDraft()\n                                if (!previous.committed) return current\n                                return {\n                                  ...current,\n                                  [item.name]: {\n                                    ...previous,\n                                    committed: false,\n                                  },\n                                }\n                              })\n                            }\n\n                            setSelection((current) => {\n                              if (item.multiple) {\n                                const selected = selectedValues(\n                                  current[item.name],\n                                )\n                                return {\n                                  ...current,\n                                  [item.name]: checked\n                                    ? [...selected, value]\n                                    : selected.filter((entry) => entry !== value),\n                                }\n                              }\n                              return {\n                                ...current,\n                                [item.name]: checked ? value : null,\n                              }\n                            })\n\n                            if (!checked) {\n                              clear()\n                              return\n                            }\n                            if (!canAutoAdvance || !hasNext) return\n                            if (itemStatus[item.name] === \"answered\") return\n                            schedule(choiceKey, () => goToNextFrom(item.name))\n                          }}\n                        >\n                          {choice.label}\n                        </AskOptionRow>\n                      )\n                    })}\n                    {item.input ? (\n                      <AskOtherRow\n                        name={item.name}\n                        label={item.input.label}\n                        placeholder={item.input.placeholder}\n                        badge={otherBadge(item, shortcuts)}\n                        disabled={pendingKey != null}\n                        isPending={pendingKey === `${item.name}:other`}\n                        committed={\n                          otherDrafts[item.name]?.committed ?? false\n                        }\n                        text={otherDrafts[item.name]?.text ?? \"\"}\n                        autoAdvance={canAutoAdvance}\n                        multiple={item.multiple === true}\n                        variant={variant}\n                        inputRef={(node) => {\n                          otherInputRefs.current[item.name] = node\n                        }}\n                        onTextChange={(value) => {\n                          patchOtherDraft(item.name, { text: value })\n                          if (\n                            !item.multiple &&\n                            (otherDrafts[item.name]?.committed ?? false)\n                          ) {\n                            setSelection((current) => ({\n                              ...current,\n                              [item.name]: value.trim() || null,\n                            }))\n                          }\n                        }}\n                        onCommit={() => commitOther(item)}\n                        onUncommitKeepText={() => uncommitOther(item)}\n                        onResetDraft={(refocus) => {\n                          resetOtherDraft(item.name)\n                          if (!item.multiple) {\n                            const selected = selectionRef.current[item.name]\n                            const other = committedOtherText(\n                              item.name,\n                              otherDraftsRef.current,\n                            )\n                            if (\n                              typeof selected === \"string\" &&\n                              selected === other\n                            ) {\n                              setSelection((current) => ({\n                                ...current,\n                                [item.name]: null,\n                              }))\n                            }\n                          }\n                          if (refocus) {\n                            requestAnimationFrame(() =>\n                              otherInputRefs.current[item.name]?.focus(),\n                            )\n                          } else {\n                            blurOther(item.name)\n                            restoreBatchKeyboard()\n                          }\n                        }}\n                        onFocusChange={(focused) =>\n                          setOtherFocusedName(focused ? item.name : null)\n                        }\n                      />\n                    ) : null}\n                  </QuestionnaireChoices>\n                  <QuestionnaireError />\n                </CardContent>\n              </QuestionnaireItem>\n            )\n          })}\n        </div>\n        {phase === \"review\" ? (\n          <>\n            <CardHeader className={cn(plain && \"rounded-none px-0\")}>\n              <CardTitle>{labels?.review ?? \"Review\"}</CardTitle>\n              <CardDescription>Submit this batch?</CardDescription>\n              {showCancel ? (\n                <CardAction>\n                  <CancelBatchButton labels={labels} onCancel={emitCancel} />\n                </CardAction>\n              ) : null}\n            </CardHeader>\n            <CardContent className={cn(plain && \"px-0\")}>\n              <ul className=\"flex flex-col gap-4\">\n                {reviewAnswers.map((answer) => (\n                  <li key={answer.name} className=\"flex flex-col gap-1\">\n                    <p className=\"text-sm text-muted-foreground\">\n                      {answer.title}\n                    </p>\n                    <p className=\"text-sm font-medium\">{answer.label}</p>\n                  </li>\n                ))}\n              </ul>\n            </CardContent>\n          </>\n        ) : null}\n        {showActions ? (\n          <CardFooter\n            className={cn(\n              \"border-t-0 bg-transparent p-0 px-(--card-spacing) pt-1 pb-(--card-spacing)\",\n              plain && \"px-0 pb-0\",\n            )}\n          >\n          <QuestionnaireActions className=\"w-full\">\n            {phase === \"review\" ? (\n              <>\n                <Button\n                  type=\"button\"\n                  variant=\"outline\"\n                  className={cn(\n                    \"col-start-1 row-start-1 justify-self-start\",\n                    TOUCH_ACTION_CLASS,\n                  )}\n                  onClick={leaveReview}\n                >\n                  <ActionShortcutHint\n                    shortcut={ASK_SHORTCUTS.previous}\n                    visible={showShortcutHints}\n                  />\n                  {backLabel}\n                </Button>\n                <QuestionnaireSubmit>\n                  {labels?.submit ?? \"Submit\"}\n                  <ActionShortcutHint\n                    shortcut={ASK_SHORTCUTS.submit}\n                    visible={showShortcutHints}\n                  />\n                </QuestionnaireSubmit>\n              </>\n            ) : (\n              <>\n                <QuestionnairePrevious>\n                  <ActionShortcutHint\n                    shortcut={ASK_SHORTCUTS.previous}\n                    visible={showShortcutHints}\n                  />\n                  {backLabel}\n                </QuestionnairePrevious>\n                <QuestionnaireSkip\n                  className={cn(\n                    hideSkip && \"hidden\",\n                    skipHasArrow && \"col-start-3\",\n                  )}\n                  disabled={hideSkip}\n                  onClick={() => {\n                    if (!activeItem) return\n                    clear()\n                    clearItemSelection(activeItem)\n                    resetOtherDraft(activeItem)\n                  }}\n                >\n                  {skipLabel}\n                  <ActionShortcutHint\n                    shortcut={ASK_SHORTCUTS.skip}\n                    visible={showShortcutHints && !hideSkip}\n                  />\n                </QuestionnaireSkip>\n                {showReviewNext ? (\n                  hideAutoAdvanceNext ? null : !hasAnswer ? (\n                    <span className=\"col-start-3 row-start-1 inline-flex justify-self-end\">\n                      <Button\n                        type=\"button\"\n                        disabled\n                        className={TOUCH_ACTION_CLASS}\n                        onClick={enterReview}\n                      >\n                        {nextLabel}\n                        <ActionShortcutHint\n                          shortcut={ASK_SHORTCUTS.next}\n                          visible={showShortcutHints}\n                        />\n                      </Button>\n                    </span>\n                  ) : (\n                    <Button\n                      type=\"button\"\n                      className={cn(\n                        \"col-start-3 row-start-1 justify-self-end\",\n                        TOUCH_ACTION_CLASS,\n                      )}\n                      onClick={enterReview}\n                    >\n                      {nextLabel}\n                      <ActionShortcutHint\n                        shortcut={ASK_SHORTCUTS.next}\n                        visible={showShortcutHints}\n                      />\n                    </Button>\n                  )\n                ) : (\n                  <>\n                    {hideAutoAdvanceNext ? null : (\n                      <QuestionnaireNext>\n                        {nextLabel}\n                        <ActionShortcutHint\n                          shortcut={ASK_SHORTCUTS.next}\n                          visible={showShortcutHints}\n                        />\n                      </QuestionnaireNext>\n                    )}\n                    <QuestionnaireSubmit>\n                      {labels?.submit ?? \"Submit\"}\n                      <ActionShortcutHint\n                        shortcut={ASK_SHORTCUTS.submit}\n                        visible={showShortcutHints}\n                      />\n                    </QuestionnaireSubmit>\n                  </>\n                )}\n              </>\n            )}\n          </QuestionnaireActions>\n          </CardFooter>\n        ) : null}\n      </Card>\n    </Questionnaire>\n  )\n}\n",      "type": "registry:block"
    },
    {
      "path": "registry/new-york/blocks/ask/interactive-focus.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nexport type InteractiveFocusTrigger =\n  | React.RefObject<HTMLElement | null>\n  | HTMLElement\n  | null\n  | undefined\n\ntype InteractiveFocusEntry = {\n  id: string\n  priority: number\n  getRoot: () => HTMLElement | null\n  getTriggers: () => InteractiveFocusTrigger[]\n  activate: () => void\n  deactivate: () => void\n}\n\ntype InteractiveFocusContextValue = {\n  register: (entry: InteractiveFocusEntry) => () => void\n  requestFocus: (id: string) => void\n  focusedId: string | null\n}\n\nconst InteractiveFocusContext =\n  React.createContext<InteractiveFocusContextValue | null>(null)\n\nconst InteractiveFocusSurfaceContext =\n  React.createContext<React.RefObject<HTMLElement | null> | null>(null)\n\nfunction resolveElement(\n  trigger: InteractiveFocusTrigger,\n): HTMLElement | null {\n  if (!trigger) return null\n  if (typeof HTMLElement !== \"undefined\" && trigger instanceof HTMLElement) {\n    return trigger\n  }\n  if (typeof trigger === \"object\" && \"current\" in trigger) {\n    return trigger.current\n  }\n  return null\n}\n\nfunction isEditableKeyboardTarget(target: EventTarget | null) {\n  if (!(target instanceof HTMLElement)) return false\n  if (target.isContentEditable) return true\n  if (!(target instanceof HTMLInputElement)) {\n    return (\n      target instanceof HTMLTextAreaElement ||\n      target instanceof HTMLSelectElement\n    )\n  }\n  return ![\"button\", \"checkbox\", \"radio\", \"reset\", \"submit\"].includes(\n    target.type,\n  )\n}\n\nfunction findPortaledOverlay(target: EventTarget | null): Element | null {\n  if (!(target instanceof Element)) return null\n  return (\n    target.closest(\"[data-slot=popover-content]\") ??\n    target.closest(\"[data-slot=dialog-content]\") ??\n    target.closest(\"[role=dialog]\")\n  )\n}\n\n/**\n * Portaled popovers/dialogs sit outside the Ask root in the DOM. Treat them as\n * owned when their trigger lives under `root` (aria-controls) or the root has\n * an open popover/dialog trigger while the event is inside overlay content.\n */\nexport function isOwnedPortaledOverlay(\n  root: HTMLElement | null,\n  target: EventTarget | null,\n): boolean {\n  if (!root) return false\n  const overlay = findPortaledOverlay(target)\n  if (!overlay) return false\n\n  const overlayId = overlay.id\n  if (\n    overlayId &&\n    root.querySelector(`[aria-controls=\"${CSS.escape(overlayId)}\"]`)\n  ) {\n    return true\n  }\n\n  const slot = overlay.getAttribute(\"data-slot\")\n  if (\n    (slot === \"popover-content\" ||\n      overlay.closest(\"[data-slot=popover-content]\") != null) &&\n    root.querySelector(\n      '[data-slot=popover-trigger][data-state=open], [data-slot=popover-trigger][aria-expanded=\"true\"]',\n    )\n  ) {\n    return true\n  }\n\n  if (\n    (slot === \"dialog-content\" || overlay.getAttribute(\"role\") === \"dialog\") &&\n    root.querySelector(\n      '[data-slot=dialog-trigger][data-state=open], [aria-haspopup=\"dialog\"][aria-expanded=\"true\"]',\n    )\n  ) {\n    return true\n  }\n\n  return false\n}\n\nexport function rootHasOpenPortaledOverlay(\n  root: HTMLElement | null,\n): boolean {\n  if (!root) return false\n  return (\n    root.querySelector(\n      '[data-slot=popover-trigger][data-state=open], [data-slot=popover-trigger][aria-expanded=\"true\"]',\n    ) != null ||\n    root.querySelector(\n      '[data-slot=dialog-trigger][data-state=open], [aria-haspopup=\"dialog\"][aria-expanded=\"true\"]',\n    ) != null\n  )\n}\n\nfunction eventInside(\n  entry: InteractiveFocusEntry,\n  target: EventTarget | null,\n) {\n  if (!(target instanceof Node)) return false\n  const root = entry.getRoot()\n  if (root?.contains(target)) return true\n  for (const trigger of entry.getTriggers()) {\n    const el = resolveElement(trigger)\n    if (el?.contains(target)) return true\n  }\n  if (isOwnedPortaledOverlay(root, target)) return true\n  return false\n}\n\nfunction blurEntryRoot(getRoot: () => HTMLElement | null) {\n  const root = getRoot()\n  if (!root) return\n  const active = document.activeElement\n  if (active instanceof HTMLElement && root.contains(active)) {\n    active.blur()\n  }\n  root.blur()\n}\n\nexport function InteractiveFocusProvider({\n  children,\n}: {\n  children: React.ReactNode\n}) {\n  const entriesRef = React.useRef(new Map<string, InteractiveFocusEntry>())\n  const [focusedId, setFocusedId] = React.useState<string | null>(null)\n  const focusedIdRef = React.useRef<string | null>(null)\n\n  const setFocused = React.useCallback((nextId: string | null) => {\n    const previous = focusedIdRef.current\n    if (previous === nextId) return\n    if (previous) entriesRef.current.get(previous)?.deactivate()\n    focusedIdRef.current = nextId\n    setFocusedId(nextId)\n    if (nextId) entriesRef.current.get(nextId)?.activate()\n  }, [])\n\n  const requestFocus = React.useCallback(\n    (id: string) => {\n      if (!entriesRef.current.has(id)) return\n      setFocused(id)\n    },\n    [setFocused],\n  )\n\n  const register = React.useCallback((entry: InteractiveFocusEntry) => {\n    entriesRef.current.set(entry.id, entry)\n    // Do not auto-focus on mount — host click / Tab arms a card.\n    return () => {\n      entriesRef.current.delete(entry.id)\n      if (focusedIdRef.current === entry.id) {\n        focusedIdRef.current = null\n        setFocusedId(null)\n      }\n    }\n  }, [])\n\n  React.useEffect(() => {\n    const onPointerDown = (event: PointerEvent) => {\n      const target = event.target\n      const matches = [...entriesRef.current.values()].filter((entry) =>\n        eventInside(entry, target),\n      )\n      if (matches.length === 0) {\n        setFocused(null)\n        return\n      }\n      matches.sort((a, b) => b.priority - a.priority)\n      setFocused(matches[0]!.id)\n    }\n\n    const onKeyDown = (event: KeyboardEvent) => {\n      if (event.key !== \"Tab\") return\n      if (event.defaultPrevented || event.isComposing) return\n      if (isEditableKeyboardTarget(event.target)) return\n      const focusedEntry = focusedIdRef.current\n        ? entriesRef.current.get(focusedIdRef.current)\n        : undefined\n      // Let Tab move inside portaled confirm UI (e.g. cancel popover).\n      if (\n        focusedEntry &&\n        rootHasOpenPortaledOverlay(focusedEntry.getRoot())\n      ) {\n        return\n      }\n      const ranked = [...entriesRef.current.values()].sort(\n        (a, b) => b.priority - a.priority || a.id.localeCompare(b.id),\n      )\n      if (ranked.length < 2) return\n      if (focusedIdRef.current == null) return\n\n      event.preventDefault()\n      const currentIndex = ranked.findIndex(\n        (entry) => entry.id === focusedIdRef.current,\n      )\n      const delta = event.shiftKey ? -1 : 1\n      const nextIndex =\n        currentIndex < 0\n          ? 0\n          : (currentIndex + delta + ranked.length) % ranked.length\n      setFocused(ranked[nextIndex]!.id)\n    }\n\n    document.addEventListener(\"pointerdown\", onPointerDown, true)\n    document.addEventListener(\"keydown\", onKeyDown, true)\n    return () => {\n      document.removeEventListener(\"pointerdown\", onPointerDown, true)\n      document.removeEventListener(\"keydown\", onKeyDown, true)\n    }\n  }, [setFocused])\n\n  const value = React.useMemo(\n    () => ({ register, requestFocus, focusedId }),\n    [register, requestFocus, focusedId],\n  )\n\n  return (\n    <InteractiveFocusContext.Provider value={value}>\n      {children}\n    </InteractiveFocusContext.Provider>\n  )\n}\n\n/**\n * Marks a container (e.g. chat pane / preview chrome) as a shared focus\n * surface. Interactive cards inside can treat it as a focus trigger.\n */\nexport function InteractiveFocusSurface({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  const ref = React.useRef<HTMLDivElement>(null)\n\n  return (\n    <InteractiveFocusSurfaceContext.Provider value={ref}>\n      <div\n        ref={ref}\n        className={className}\n        data-slot=\"interactive-focus-surface\"\n        {...props}\n      >\n        {children}\n      </div>\n    </InteractiveFocusSurfaceContext.Provider>\n  )\n}\n\nexport function useInteractiveFocusSurface() {\n  return React.useContext(InteractiveFocusSurfaceContext)\n}\n\nexport function useInteractiveFocusRegistration(options: {\n  enabled: boolean\n  priority?: number\n  rootRef: React.RefObject<HTMLElement | null>\n  triggers?: InteractiveFocusTrigger[]\n  onFocusChange?: (focused: boolean) => void\n}) {\n  const ctx = React.useContext(InteractiveFocusContext)\n  const surface = useInteractiveFocusSurface()\n  const id = React.useId()\n  const [soloFocused, setSoloFocused] = React.useState(false)\n  const onFocusChangeRef = React.useRef(options.onFocusChange)\n  const triggersRef = React.useRef(options.triggers)\n  const priority = options.priority ?? 0\n  const ctxRegister = ctx?.register\n  const ctxRequestFocus = ctx?.requestFocus\n\n  React.useEffect(() => {\n    onFocusChangeRef.current = options.onFocusChange\n  }, [options.onFocusChange])\n\n  React.useEffect(() => {\n    triggersRef.current = options.triggers\n  }, [options.triggers])\n\n  const focused = !options.enabled\n    ? true\n    : ctx\n      ? ctx.focusedId === id\n      : soloFocused\n\n  React.useEffect(() => {\n    if (!options.enabled) return\n    onFocusChangeRef.current?.(focused)\n  }, [focused, options.enabled])\n\n  const activate = React.useCallback(() => {\n    options.rootRef.current?.focus({ preventScroll: true })\n  }, [options.rootRef])\n\n  const deactivate = React.useCallback(() => {\n    blurEntryRoot(() => options.rootRef.current)\n  }, [options.rootRef])\n\n  React.useEffect(() => {\n    if (!options.enabled) return\n\n    if (!ctxRegister) {\n      setSoloFocused(false)\n      const onPointerDown = (event: PointerEvent) => {\n        const target = event.target\n        const root = options.rootRef.current\n        const triggers = [...(triggersRef.current ?? []), surface]\n        const hit =\n          (root != null &&\n            target instanceof Node &&\n            root.contains(target)) ||\n          triggers.some((trigger) => {\n            const el = resolveElement(trigger)\n            return (\n              el != null && target instanceof Node && el.contains(target)\n            )\n          }) ||\n          isOwnedPortaledOverlay(root, target)\n        if (hit) {\n          setSoloFocused(true)\n          // Keep focus inside owned portals (cancel confirm) so buttons work.\n          if (!isOwnedPortaledOverlay(root, target)) {\n            options.rootRef.current?.focus({ preventScroll: true })\n          }\n          return\n        }\n        setSoloFocused(false)\n        blurEntryRoot(() => options.rootRef.current)\n      }\n      document.addEventListener(\"pointerdown\", onPointerDown, true)\n      return () =>\n        document.removeEventListener(\"pointerdown\", onPointerDown, true)\n    }\n\n    return ctxRegister({\n      id,\n      priority,\n      getRoot: () => options.rootRef.current,\n      getTriggers: () => [...(triggersRef.current ?? []), surface],\n      activate,\n      deactivate,\n    })\n  }, [\n    activate,\n    ctxRegister,\n    deactivate,\n    id,\n    options.enabled,\n    options.rootRef,\n    priority,\n    surface,\n  ])\n\n  const requestFocus = React.useCallback(() => {\n    if (!options.enabled) return\n    if (ctxRequestFocus) {\n      ctxRequestFocus(id)\n      return\n    }\n    setSoloFocused(true)\n    options.rootRef.current?.focus({ preventScroll: true })\n  }, [ctxRequestFocus, id, options.enabled, options.rootRef])\n\n  return {\n    focused,\n    requestFocus,\n    focusedId: ctx?.focusedId ?? null,\n  }\n}\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:block"
}