{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "user-ask",
  "type": "registry:component",
  "title": "UserAsk",
  "description": "Keyboard-first agent question panel that temporarily replaces the chat composer inside its container.",
  "dependencies": [
    "lucide-react@^1.31.0"
  ],
  "registryDependencies": [
    "https://control-ui.dev/r/button.json",
    "https://control-ui.dev/r/core.json",
    "https://control-ui.dev/r/kbd.json"
  ],
  "files": [
    {
      "path": "src/registry/hooks/use-user-ask.ts",
      "target": "@components/control-ui/hooks/use-user-ask.ts",
      "type": "registry:hook",
      "content": "import type { ComponentProps, CSSProperties, KeyboardEvent, ReactNode } from \"react\";\nimport { useState } from \"react\";\nimport type { UserAskKnobStyle } from \"@/components/control-ui/knob-contracts/user-ask-knobs\";\n\nexport type UserAskAnswers = Record<string, string>;\n\nexport type UserAskProps = Omit<ComponentProps<\"section\">, \"style\"> & {\n  children?: ReactNode;\n  onComplete?: (answers: UserAskAnswers) => void;\n  onDismiss?: () => void;\n  style?: CSSProperties & UserAskKnobStyle;\n};\n\nexport type UserAskQuestionEntry = {\n  key: string;\n  id: string;\n  title: string;\n  defaultValue?: string;\n};\n\nexport type UserAskOptionEntry = {\n  key: string;\n  value: string;\n  disabled?: boolean;\n  /** Freeform options resolve to user's typed text instead of `value`. */\n  freeform?: boolean;\n};\n\nfunction upsert<Entry extends { key: string }>(entries: Entry[], entry: Entry) {\n  const index = entries.findIndex((existing) => existing.key === entry.key);\n  if (index === -1) return [...entries, entry];\n  const next = [...entries];\n  next[index] = entry;\n  return next;\n}\n\nexport function useUserAsk({ onComplete, onDismiss }: Pick<UserAskProps, \"onComplete\" | \"onDismiss\">) {\n  const [questions, setQuestions] = useState<UserAskQuestionEntry[]>([]);\n  const [optionsByQuestion, setOptionsByQuestion] = useState<Record<string, UserAskOptionEntry[]>>({});\n  // Explicit picks by option registration key; question's defaultValue is DERIVED in selectionFor, never copied here.\n  const [selections, setSelections] = useState<Record<string, string>>({});\n  const [freeformTexts, setFreeformTexts] = useState<Record<string, string>>({});\n  const [requestedIndex, setRequestedIndex] = useState(0);\n\n  // useState-once keeps identities stable; register upserts IN PLACE (prop change must not reorder numbering)\n  // and unregister runs only at part unmount — combined effect cleanup would move updated entries to end.\n  const [registerQuestion] = useState(() => (entry: UserAskQuestionEntry) => {\n    setQuestions((previous) => upsert(previous, entry));\n  });\n\n  const [unregisterQuestion] = useState(() => (key: string) => {\n    setQuestions((previous) => previous.filter((question) => question.key !== key));\n  });\n\n  const [registerOption] = useState(() => (questionId: string, entry: UserAskOptionEntry) => {\n    setOptionsByQuestion((previous) => ({ ...previous, [questionId]: upsert(previous[questionId] ?? [], entry) }));\n  });\n\n  const [unregisterOption] = useState(() => (questionId: string, key: string) => {\n    setOptionsByQuestion((previous) => ({\n      ...previous,\n      [questionId]: (previous[questionId] ?? []).filter((option) => option.key !== key),\n    }));\n  });\n\n  const activeIndex = questions.length === 0 ? 0 : Math.min(requestedIndex, questions.length - 1);\n  const activeQuestion = questions[activeIndex];\n\n  function optionsFor(questionId: string) {\n    return optionsByQuestion[questionId] ?? [];\n  }\n\n  function selectionFor(questionId: string) {\n    const explicit = selections[questionId];\n    if (explicit) return explicit;\n    const defaultValue = questions.find((question) => question.id === questionId)?.defaultValue;\n    if (defaultValue === undefined) return undefined;\n    return optionsFor(questionId).find((option) => !option.freeform && option.value === defaultValue)?.key;\n  }\n\n  function isSelected(questionId: string, optionKey: string) {\n    return selectionFor(questionId) === optionKey;\n  }\n\n  function select(questionId: string, optionKey: string) {\n    setSelections((previous) => ({ ...previous, [questionId]: optionKey }));\n  }\n\n  function freeformTextFor(questionId: string) {\n    return freeformTexts[questionId] ?? \"\";\n  }\n\n  function setFreeformText(questionId: string, text: string) {\n    setFreeformTexts((previous) => ({ ...previous, [questionId]: text }));\n  }\n\n  function answerFor(questionId: string) {\n    const selectedKey = selectionFor(questionId);\n    const option = optionsFor(questionId).find((entry) => entry.key === selectedKey);\n    if (!option) return undefined;\n    if (!option.freeform) return option.value;\n    const text = freeformTextFor(questionId).trim();\n    return text.length > 0 ? text : undefined;\n  }\n\n  const canContinue = activeQuestion !== undefined && answerFor(activeQuestion.id) !== undefined;\n  const isLastQuestion = activeIndex === questions.length - 1;\n\n  function goTo(index: number) {\n    setRequestedIndex(Math.max(0, Math.min(index, questions.length - 1)));\n  }\n\n  function resolveAnswers() {\n    const answers: UserAskAnswers = {};\n    for (const question of questions) {\n      const answer = answerFor(question.id);\n      if (answer !== undefined) answers[question.id] = answer;\n    }\n    return answers;\n  }\n\n  function advance() {\n    if (!canContinue) return;\n    if (isLastQuestion) onComplete?.(resolveAnswers());\n    else goTo(activeIndex + 1);\n  }\n\n  function dismiss() {\n    onDismiss?.();\n  }\n\n  function moveSelection(delta: number) {\n    if (!activeQuestion) return;\n    const options = optionsFor(activeQuestion.id);\n    if (!options.some((option) => !option.disabled)) return;\n    const currentKey = selectionFor(activeQuestion.id);\n    const currentIndex = options.findIndex((option) => option.key === currentKey);\n    let start = currentIndex;\n    if (currentIndex === -1) start = delta > 0 ? -1 : 0;\n    for (let step = 1; step <= options.length; step += 1) {\n      const candidate = options[(start + delta * step + options.length * step) % options.length];\n      if (candidate && !candidate.disabled) {\n        select(activeQuestion.id, candidate.key);\n        return;\n      }\n    }\n  }\n\n  function selectDigit(digit: number) {\n    if (!activeQuestion) return;\n    const option = optionsFor(activeQuestion.id)[digit - 1];\n    if (option && !option.disabled) select(activeQuestion.id, option.key);\n  }\n\n  function handleKeyDown(event: KeyboardEvent<HTMLElement>) {\n    if (event.defaultPrevented) return;\n    if (event.key === \"Escape\") {\n      event.preventDefault();\n      dismiss();\n      return;\n    }\n    if (event.key === \"Enter\") {\n      // Enter always means \"continue\" (even from option or freeform input), matching CLI ask flows.\n      event.preventDefault();\n      advance();\n      return;\n    }\n    // Inside freeform input, digits and arrows must keep typing/caret behavior.\n    if (event.target instanceof HTMLElement && event.target.closest(\"input, textarea, [contenteditable]\")) return;\n    if (event.key === \"ArrowDown\") {\n      event.preventDefault();\n      moveSelection(1);\n      return;\n    }\n    if (event.key === \"ArrowUp\") {\n      event.preventDefault();\n      moveSelection(-1);\n      return;\n    }\n    if (/^[1-9]$/.test(event.key)) {\n      // preventDefault: selecting freeform option moves focus into its input before keypress — without this digit would be typed there.\n      event.preventDefault();\n      selectDigit(Number(event.key));\n    }\n  }\n\n  return {\n    questions,\n    activeIndex,\n    activeQuestion,\n    canContinue,\n    isLastQuestion,\n    registerQuestion,\n    unregisterQuestion,\n    registerOption,\n    unregisterOption,\n    optionsFor,\n    selectionFor,\n    isSelected,\n    select,\n    freeformTextFor,\n    setFreeformText,\n    answerFor,\n    resolveAnswers,\n    goTo,\n    advance,\n    dismiss,\n    handleKeyDown,\n  };\n}\n"
    },
    {
      "path": "src/registry/knob-contracts/user-ask-knobs.ts",
      "target": "@components/control-ui/knob-contracts/user-ask-knobs.ts",
      "type": "registry:component",
      "content": "// Generated from src/registry/sources/control-ui/recipes/user-ask.css by scripts/gen-knob-contracts.ts — run `bun run sync:knobs`.\nexport const userAskKnobs = [\n  \"--cui-user-ask-radius\",\n  \"--cui-user-ask-background\",\n  \"--cui-user-ask-foreground\",\n  \"--cui-user-ask-border-color\",\n  \"--cui-user-ask-shadow\",\n  \"--cui-user-ask-option-radius\",\n  \"--cui-user-ask-option-hover-background\",\n  \"--cui-user-ask-option-selected-background\",\n  \"--cui-user-ask-indicator-background\",\n  \"--cui-user-ask-indicator-foreground\",\n  \"--cui-user-ask-input-background\",\n  \"--cui-user-ask-input-foreground\",\n] as const;\nexport type UserAskKnobStyle = Partial<Record<(typeof userAskKnobs)[number], string>>;\n"
    },
    {
      "path": "src/registry/sources/control-ui/recipes/user-ask-details.css",
      "target": "@components/control-ui/styles/recipes/user-ask-details.css",
      "type": "registry:file",
      "content": "@layer components {\n  :where([data-control-family=\"button\"][data-user-ask-dismiss=\"true\"]) {\n    color: var(--muted-foreground);\n  }\n\n  :where([data-control-family=\"kbd\"][data-user-ask-submit-kbd=\"true\"]) {\n    background: oklch(from var(--primary-foreground) l c h / 0.15);\n    color: var(--primary-foreground);\n    box-shadow: inset 0 0 0 1px oklch(from var(--primary-foreground) l c h / 0.25);\n  }\n\n  :where([data-control-family=\"user-ask\"][data-slot=\"option-input\"]) {\n    font-weight: var(--font-weight-medium);\n    outline-style: none;\n  }\n\n  :where([data-control-family=\"user-ask\"][data-slot=\"option-indicator\"]) {\n    font-weight: var(--font-weight-medium);\n    font-variant-numeric: tabular-nums;\n  }\n\n  :where([data-control-family=\"user-ask\"][data-slot=\"option-description\"]) {\n    font-weight: var(--font-weight-normal);\n  }\n\n  :where([data-control-family=\"user-ask\"][data-slot=\"recommended\"]) {\n    font-weight: var(--font-weight-normal);\n  }\n\n  :where([data-control-family=\"user-ask\"][data-slot=\"option-label\"]) {\n    font-weight: var(--font-weight-medium);\n  }\n\n  :where([data-control-family=\"user-ask\"][data-slot=\"title\"]) {\n    font-weight: var(--font-weight-semibold);\n  }\n\n  :where([data-control-family=\"user-ask\"][data-slot=\"root\"]) {\n    outline-style: none;\n  }\n\n  :where([data-control-family=\"user-ask\"][data-slot=\"pagination-count\"]) {\n    font-variant-numeric: tabular-nums;\n  }\n}\n"
    },
    {
      "path": "src/registry/sources/control-ui/recipes/user-ask.css",
      "target": "@components/control-ui/styles/recipes/user-ask.css",
      "type": "registry:file",
      "content": "@layer components {\n  :where([data-control-family=\"user-ask\"][data-slot=\"root\"]) {\n    --cui-user-ask-background: oklch(from var(--card) l c h / 0.9);\n    --cui-user-ask-border-color: var(--border);\n    --cui-user-ask-foreground: var(--foreground);\n    --cui-user-ask-radius: var(--radius-field);\n    --cui-user-ask-shadow: var(--shadow-md);\n    --cui-user-ask-indicator-background: var(--muted);\n    --cui-user-ask-indicator-foreground: var(--muted-foreground);\n    --cui-user-ask-input-background: transparent;\n    --cui-user-ask-input-foreground: var(--foreground);\n    --cui-user-ask-option-hover-background: oklch(from var(--muted) l c h / 0.5);\n    --cui-user-ask-option-radius: var(--radius-popup-item);\n    --cui-user-ask-option-selected-background: oklch(from var(--muted) l c h / 0.7);\n    border: 1px solid var(--cui-user-ask-border-color);\n    border-radius: var(--cui-user-ask-radius);\n    background: var(--cui-user-ask-background);\n    color: var(--cui-user-ask-foreground);\n    box-shadow:\n      inset 0 0 0 1px oklch(from var(--foreground) l c h / 0.04),\n      var(--cui-user-ask-shadow);\n    backdrop-filter: blur(var(--backdrop-blur-popover));\n    font-size: var(--text-body);\n    transition-property: opacity, translate;\n    transition-duration: var(--duration-base);\n    transition-timing-function: var(--ease-emphasized);\n  }\n\n  @starting-style {\n    :where([data-control-family=\"user-ask\"][data-slot=\"root\"]) {\n      opacity: 0;\n      translate: 0 0.5rem;\n    }\n  }\n\n  :where([data-control-family=\"user-ask\"][data-slot=\"title\"]) {\n    color: var(--foreground);\n    font-size: var(--text-sm);\n  }\n\n  :where([data-control-family=\"user-ask\"][data-slot=\"pagination\"]) {\n    color: var(--muted-foreground);\n    font-size: var(--text-micro);\n  }\n\n  :where([data-control-family=\"user-ask\"][data-slot=\"option\"]) {\n    border-radius: var(--cui-user-ask-option-radius);\n    font-size: var(--text-sm);\n    transition-property: background-color, box-shadow;\n    transition-duration: var(--duration-fast);\n    transition-timing-function: var(--ease-standard);\n    text-align: left;\n  }\n\n  :where([data-control-family=\"user-ask\"][data-slot=\"option\"][data-selected]) {\n    background: var(--cui-user-ask-option-selected-background);\n  }\n\n  @media (hover: hover) {\n    :where([data-control-family=\"user-ask\"][data-slot=\"option\"]:hover) {\n      background: var(--cui-user-ask-option-hover-background);\n    }\n  }\n\n  :where([data-control-family=\"user-ask\"][data-slot=\"option\"]:disabled) {\n    opacity: 0.5;\n  }\n\n  :where([data-control-family=\"user-ask\"][data-slot=\"option-indicator\"]) {\n    border-radius: var(--radius-control);\n    background: var(--cui-user-ask-indicator-background);\n    color: var(--cui-user-ask-indicator-foreground);\n    font-size: var(--text-micro);\n  }\n\n  :where([data-control-family=\"user-ask\"][data-slot=\"option-indicator\"][data-selected]) {\n    background: var(--primary);\n    color: var(--primary-foreground);\n  }\n\n  :where([data-control-family=\"user-ask\"][data-slot=\"option-label\"]) {\n    color: var(--foreground);\n  }\n\n  :where([data-control-family=\"user-ask\"]:is([data-slot=\"recommended\"], [data-slot=\"option-description\"], [data-slot=\"freeform-label\"])) {\n    color: var(--muted-foreground);\n  }\n\n  :where([data-control-family=\"user-ask\"][data-slot=\"option-description\"]) {\n    font-size: var(--text-caption);\n  }\n\n  :where([data-control-family=\"user-ask\"][data-slot=\"option-input\"]) {\n    --_user-ask-input-placeholder-paint: var(--muted-foreground);\n    border: 0;\n    background: var(--cui-user-ask-input-background);\n    color: var(--cui-user-ask-input-foreground);\n  }\n\n  :where([data-control-family=\"user-ask\"][data-slot=\"option-input\"])::placeholder {\n    color: var(--_user-ask-input-placeholder-paint);\n    font-weight: 400;\n  }\n}\n\n@property --cui-user-ask-radius {\n  syntax: \"<length>\";\n  inherits: true;\n  initial-value: 0px;\n}\n\n@property --cui-user-ask-background {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n\n@property --cui-user-ask-foreground {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n\n@property --cui-user-ask-border-color {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n\n@property --cui-user-ask-shadow {\n  syntax: \"*\";\n  inherits: true;\n}\n\n@property --cui-user-ask-option-radius {\n  syntax: \"<length>\";\n  inherits: true;\n  initial-value: 0px;\n}\n\n@property --cui-user-ask-option-hover-background {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n\n@property --cui-user-ask-option-selected-background {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n\n@property --cui-user-ask-indicator-background {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n\n@property --cui-user-ask-indicator-foreground {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n\n@property --cui-user-ask-input-background {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n\n@property --cui-user-ask-input-foreground {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n"
    },
    {
      "path": "src/registry/sources/control-ui/user-ask.tsx",
      "target": "@components/control-ui/user-ask.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { Check, ChevronLeft, ChevronRight, PencilLine } from \"lucide-react\";\nimport type { ChangeEvent, ComponentProps, CSSProperties, MouseEvent } from \"react\";\nimport { createContext, useContext, useEffect, useId, useRef } from \"react\";\n\nimport type { UserAskProps } from \"@/components/control-ui/hooks/use-user-ask\";\nimport { useUserAsk } from \"@/components/control-ui/hooks/use-user-ask\";\nimport type { UserAskKnobStyle } from \"@/components/control-ui/knob-contracts/user-ask-knobs\";\nimport { cn } from \"@/components/control-ui/lib/cn\";\nimport { Button } from \"@/components/control-ui/ui/button\";\nimport { Kbd } from \"@/components/control-ui/ui/kbd\";\n\ntype UserAskStyleProps<Props, Style> = Omit<Props, \"style\"> & { style?: CSSProperties & Style };\n\ntype UserAskContextValue = ReturnType<typeof useUserAsk> & {\n  titleId: string;\n};\n\nconst UserAskContext = createContext<UserAskContextValue | null>(null);\n\nfunction useUserAskContext() {\n  const context = useContext(UserAskContext);\n  if (!context) throw new Error(\"UserAsk compound components must be rendered inside <UserAsk>.\");\n  return context;\n}\n\ntype UserAskQuestionContextValue = {\n  id: string;\n  active: boolean;\n};\n\nconst UserAskQuestionContext = createContext<UserAskQuestionContextValue | null>(null);\n\nfunction useUserAskQuestionContext() {\n  const context = useContext(UserAskQuestionContext);\n  if (!context) throw new Error(\"UserAsk options must be rendered inside <UserAskQuestion>.\");\n  return context;\n}\n\nexport function UserAsk({ onComplete, onDismiss, autoFocus = false, className, children, ...props }: UserAskProps) {\n  const ask = useUserAsk({ onComplete, onDismiss });\n  const titleId = useId();\n  const panelRef = useRef<HTMLElement>(null);\n\n  // native autofocus only fires at document load, and this panel mounts mid-conversation\n  useEffect(() => {\n    if (autoFocus) panelRef.current?.focus();\n  }, [autoFocus]);\n\n  // Hiding question blurs its focused descendants to <body> and kills Enter/Escape flow.\n  // browser may not have processed that blur yet at effect time, so test visibility, not containment.\n  const lastActiveIndex = useRef(ask.activeIndex);\n  useEffect(() => {\n    if (lastActiveIndex.current === ask.activeIndex) return;\n    lastActiveIndex.current = ask.activeIndex;\n    const panel = panelRef.current;\n    const active = document.activeElement;\n    const activeIsUsable = active instanceof HTMLElement && panel?.contains(active) && active.checkVisibility();\n    if (panel && !activeIsUsable) panel.focus();\n  }, [ask.activeIndex]);\n\n  return (\n    <UserAskContext.Provider value={{ ...ask, titleId }}>\n      {/* fx anchor candidate: \"user-ask:panel-layer\" */}\n      <section\n        ref={panelRef}\n        data-control-ui=\"user-ask\"\n        data-control-family=\"user-ask\"\n        data-slot=\"root\"\n        data-surface=\"panel\"\n        aria-labelledby={titleId}\n        tabIndex={-1}\n        onKeyDown={ask.handleKeyDown}\n        className={cn(\"relative w-full p-3\", className)}\n        {...props}\n      >\n        {children}\n      </section>\n    </UserAskContext.Provider>\n  );\n}\n\nexport type UserAskHeaderProps = ComponentProps<\"header\"> & { style?: CSSProperties & UserAskKnobStyle };\n\nexport function UserAskHeader({ className, ...props }: UserAskHeaderProps) {\n  return (\n    <header\n      data-control-ui=\"user-ask\"\n      data-control-family=\"user-ask\"\n      data-slot=\"header\"\n      className={cn(\"flex items-start justify-between gap-3 px-1 pb-2\", className)}\n      {...props}\n    />\n  );\n}\n\nexport type UserAskTitleProps = UserAskStyleProps<ComponentProps<\"h3\">, UserAskKnobStyle>;\n\nexport function UserAskTitle({ className, children, ...props }: UserAskTitleProps) {\n  const ask = useUserAskContext();\n\n  return (\n    <h3 id={ask.titleId} data-control-ui=\"user-ask\" data-control-family=\"user-ask\" data-slot=\"title\" className={className} {...props}>\n      {children ?? ask.activeQuestion?.title}\n    </h3>\n  );\n}\n\nexport type UserAskPaginationProps = UserAskStyleProps<ComponentProps<\"div\">, UserAskKnobStyle>;\n\nexport function UserAskPagination({ className, ...props }: UserAskPaginationProps) {\n  const ask = useUserAskContext();\n  if (ask.questions.length <= 1) return null;\n\n  return (\n    <div\n      data-control-ui=\"user-ask\"\n      data-control-family=\"user-ask\"\n      data-slot=\"pagination\"\n      className={cn(\"flex shrink-0 items-center gap-1\", className)}\n      {...props}\n    >\n      <Button\n        type=\"button\"\n        variant=\"quiet\"\n        size=\"xs\"\n        aria-label=\"Previous question\"\n        disabled={ask.activeIndex === 0}\n        onClick={() => ask.goTo(ask.activeIndex - 1)}\n      >\n        <ChevronLeft aria-hidden=\"true\" className=\"size-3.5\" />\n      </Button>\n      <span data-control-ui=\"user-ask\" data-control-family=\"user-ask\" data-slot=\"pagination-count\">\n        {ask.activeIndex + 1} of {ask.questions.length}\n      </span>\n      <Button\n        type=\"button\"\n        variant=\"quiet\"\n        size=\"xs\"\n        aria-label=\"Next question\"\n        disabled={ask.isLastQuestion}\n        onClick={() => ask.goTo(ask.activeIndex + 1)}\n      >\n        <ChevronRight aria-hidden=\"true\" className=\"size-3.5\" />\n      </Button>\n    </div>\n  );\n}\n\nexport type UserAskQuestionProps = Omit<ComponentProps<\"div\">, \"title\"> & {\n  id: string;\n  title: string;\n  /** Pre-selects option carrying this value (e.g. recommended one) until user picks another. */\n  defaultValue?: string;\n} & { style?: CSSProperties & UserAskKnobStyle };\n\nexport function UserAskQuestion({ id, title, defaultValue, className, children, ...props }: UserAskQuestionProps) {\n  const ask = useUserAskContext();\n  const key = useId();\n  const active = ask.activeQuestion?.key === key;\n\n  useEffect(() => ask.registerQuestion({ key, id, title, defaultValue }), [ask.registerQuestion, key, id, title, defaultValue]);\n  useEffect(() => () => ask.unregisterQuestion(key), [ask.unregisterQuestion, key]);\n\n  return (\n    <UserAskQuestionContext.Provider value={{ id, active }}>\n      {/* inactive questions stay mounted so their registrations keep pagination and answers alive */}\n      <div\n        role=\"radiogroup\"\n        aria-labelledby={active ? ask.titleId : undefined}\n        hidden={!active}\n        data-control-ui=\"user-ask\"\n        data-control-family=\"user-ask\"\n        data-slot=\"question\"\n        data-active={active ? \"\" : undefined}\n        className={cn(\"flex flex-col gap-0.5\", className)}\n        {...props}\n      >\n        {children}\n      </div>\n    </UserAskQuestionContext.Provider>\n  );\n}\n\ntype UserAskOptionContextValue = {\n  selected: boolean;\n  recommended: boolean;\n};\n\nconst UserAskOptionContext = createContext<UserAskOptionContextValue | null>(null);\n\nfunction useUserAskOptionContext() {\n  const context = useContext(UserAskOptionContext);\n  if (!context) throw new Error(\"UserAskOption parts must be rendered inside <UserAskOption>.\");\n  return context;\n}\n\nconst optionRowClasses = \"grid w-full grid-cols-[auto_minmax(0,1fr)] items-start gap-2.5 px-2 py-1.5\";\n\nexport type UserAskOptionProps = UserAskStyleProps<Omit<ComponentProps<\"button\">, \"value\">, UserAskKnobStyle> & {\n  value: string;\n  recommended?: boolean;\n};\n\nexport function UserAskOption({\n  value,\n  recommended = false,\n  disabled = false,\n  className,\n  children,\n  onClick,\n  ...props\n}: UserAskOptionProps) {\n  const ask = useUserAskContext();\n  const question = useUserAskQuestionContext();\n  const key = useId();\n\n  useEffect(() => ask.registerOption(question.id, { key, value, disabled }), [ask.registerOption, question.id, key, value, disabled]);\n  useEffect(() => () => ask.unregisterOption(question.id, key), [ask.unregisterOption, question.id, key]);\n\n  const index = ask.optionsFor(question.id).findIndex((entry) => entry.key === key);\n  const selected = ask.isSelected(question.id, key);\n\n  function handleClick(event: MouseEvent<HTMLButtonElement>) {\n    onClick?.(event);\n    if (!event.defaultPrevented) ask.select(question.id, key);\n  }\n\n  return (\n    <UserAskOptionContext.Provider value={{ selected, recommended }}>\n      {/* biome-ignore lint/a11y/useSemanticElements: WAI-ARIA radio pattern on a rich multi-line row; a native input cannot carry the indicator/label/description anatomy. */}\n      <button\n        type=\"button\"\n        role=\"radio\"\n        aria-checked={selected}\n        disabled={disabled}\n        data-control-ui=\"user-ask\"\n        data-control-family=\"user-ask\"\n        data-slot=\"option\"\n        data-selected={selected ? \"\" : undefined}\n        data-recommended={recommended ? \"\" : undefined}\n        onClick={handleClick}\n        className={cn(optionRowClasses, \"disabled:cursor-not-allowed\", className)}\n        {...props}\n      >\n        <span\n          aria-hidden=\"true\"\n          data-control-ui=\"user-ask\"\n          data-control-family=\"user-ask\"\n          data-slot=\"option-indicator\"\n          data-selected={selected ? \"\" : undefined}\n          className=\"mt-px inline-flex size-5 shrink-0 items-center justify-center\"\n        >\n          {selected ? <Check className=\"size-3\" /> : index + 1}\n        </span>\n        <span className=\"min-w-0\">{children}</span>\n      </button>\n    </UserAskOptionContext.Provider>\n  );\n}\n\nexport type UserAskOptionLabelProps = UserAskStyleProps<ComponentProps<\"span\">, UserAskKnobStyle>;\n\nexport function UserAskOptionLabel({ className, children, ...props }: UserAskOptionLabelProps) {\n  const option = useUserAskOptionContext();\n\n  return (\n    <span data-control-ui=\"user-ask\" data-control-family=\"user-ask\" data-slot=\"option-label\" className={cn(\"block\", className)} {...props}>\n      {children}\n      {option.recommended ? (\n        <span data-control-ui=\"user-ask\" data-control-family=\"user-ask\" data-slot=\"recommended\">\n          {\" \"}\n          (Recommended)\n        </span>\n      ) : null}\n    </span>\n  );\n}\n\nexport type UserAskOptionDescriptionProps = UserAskStyleProps<ComponentProps<\"span\">, UserAskKnobStyle>;\n\nexport function UserAskOptionDescription({ className, ...props }: UserAskOptionDescriptionProps) {\n  return (\n    <span\n      data-control-ui=\"user-ask\"\n      data-control-family=\"user-ask\"\n      data-slot=\"option-description\"\n      className={cn(\"block pt-0.5\", className)}\n      {...props}\n    />\n  );\n}\n\nexport type UserAskOptionInputProps = UserAskStyleProps<Omit<ComponentProps<\"input\">, \"value\">, UserAskKnobStyle> & {\n  /** Row label while unselected (option's \"Other\" affordance). */\n  label?: string;\n};\n\nexport function UserAskOptionInput({\n  label = \"Other\",\n  placeholder = \"Type your own answer\",\n  disabled = false,\n  className,\n  onChange,\n  style,\n  ...props\n}: UserAskOptionInputProps) {\n  const ask = useUserAskContext();\n  const question = useUserAskQuestionContext();\n  const key = useId();\n  const inputRef = useRef<HTMLInputElement>(null);\n\n  useEffect(\n    () => ask.registerOption(question.id, { key, value: \"\", disabled, freeform: true }),\n    [ask.registerOption, question.id, key, disabled],\n  );\n  useEffect(() => () => ask.unregisterOption(question.id, key), [ask.unregisterOption, question.id, key]);\n\n  const index = ask.optionsFor(question.id).findIndex((entry) => entry.key === key);\n  const selected = ask.isSelected(question.id, key);\n\n  // digits, arrows, and clicks all land here — move caret in so typing starts immediately\n  useEffect(() => {\n    if (selected && question.active) inputRef.current?.focus();\n  }, [selected, question.active]);\n\n  function handleChange(event: ChangeEvent<HTMLInputElement>) {\n    onChange?.(event);\n    if (!event.defaultPrevented) ask.setFreeformText(question.id, event.currentTarget.value);\n  }\n\n  if (!selected) {\n    return (\n      // biome-ignore lint/a11y/useSemanticElements: WAI-ARIA radio pattern on a rich row; selecting it swaps in the freeform input.\n      <button\n        type=\"button\"\n        role=\"radio\"\n        aria-checked={false}\n        disabled={disabled}\n        data-control-ui=\"user-ask\"\n        data-control-family=\"user-ask\"\n        data-slot=\"option\"\n        data-freeform=\"\"\n        onClick={() => ask.select(question.id, key)}\n        className={cn(optionRowClasses, \"disabled:cursor-not-allowed\", className)}\n        style={style}\n      >\n        <span\n          aria-hidden=\"true\"\n          data-control-ui=\"user-ask\"\n          data-control-family=\"user-ask\"\n          data-slot=\"option-indicator\"\n          className=\"mt-px inline-flex size-5 shrink-0 items-center justify-center\"\n        >\n          {index + 1}\n        </span>\n        <span data-control-ui=\"user-ask\" data-control-family=\"user-ask\" data-slot=\"freeform-label\" className=\"min-w-0\">\n          {label}\n        </span>\n      </button>\n    );\n  }\n\n  return (\n    <div\n      data-control-ui=\"user-ask\"\n      data-control-family=\"user-ask\"\n      data-slot=\"option\"\n      data-selected=\"\"\n      data-freeform=\"\"\n      className={cn(optionRowClasses, className)}\n      style={style}\n    >\n      <span\n        aria-hidden=\"true\"\n        data-control-ui=\"user-ask\"\n        data-control-family=\"user-ask\"\n        data-slot=\"option-indicator\"\n        data-selected=\"\"\n        className=\"mt-px inline-flex size-5 shrink-0 items-center justify-center\"\n      >\n        <PencilLine className=\"size-3\" />\n      </span>\n      <input\n        {...props}\n        ref={inputRef}\n        type=\"text\"\n        aria-label={label}\n        data-control-ui=\"user-ask\"\n        data-control-family=\"user-ask\"\n        data-slot=\"option-input\"\n        value={ask.freeformTextFor(question.id)}\n        onChange={handleChange}\n        placeholder={placeholder}\n        className=\"min-w-0\"\n        style={style}\n      />\n    </div>\n  );\n}\n\nexport type UserAskFooterProps = ComponentProps<\"footer\"> & { style?: CSSProperties & UserAskKnobStyle };\n\nexport function UserAskFooter({ className, ...props }: UserAskFooterProps) {\n  return (\n    <footer\n      data-control-ui=\"user-ask\"\n      data-control-family=\"user-ask\"\n      data-slot=\"footer\"\n      className={cn(\"flex items-center justify-end gap-2 px-1 pt-3\", className)}\n      {...props}\n    />\n  );\n}\n\nexport type UserAskDismissProps = ComponentProps<typeof Button>;\n\nexport function UserAskDismiss({ className, children, onClick, ...props }: UserAskDismissProps) {\n  const ask = useUserAskContext();\n\n  function handleClick(event: MouseEvent<HTMLButtonElement>) {\n    onClick?.(event);\n    if (!event.defaultPrevented) ask.dismiss();\n  }\n\n  return (\n    <Button\n      data-control-ui=\"user-ask\"\n      data-user-ask-dismiss=\"true\"\n      data-slot=\"dismiss\"\n      type=\"button\"\n      variant=\"quiet\"\n      size=\"xs\"\n      onClick={handleClick}\n      className={cn(\"gap-1.5\", className)}\n      {...props}\n    >\n      {children ?? (\n        <>\n          Dismiss\n          <Kbd>esc</Kbd>\n        </>\n      )}\n    </Button>\n  );\n}\n\nexport type UserAskSubmitProps = ComponentProps<typeof Button>;\n\nexport function UserAskSubmit({ className, children, disabled, onClick, ...props }: UserAskSubmitProps) {\n  const ask = useUserAskContext();\n\n  function handleClick(event: MouseEvent<HTMLButtonElement>) {\n    onClick?.(event);\n    if (!event.defaultPrevented) ask.advance();\n  }\n\n  return (\n    <Button\n      data-control-ui=\"user-ask\"\n      data-user-ask-submit=\"true\"\n      data-slot=\"submit\"\n      type=\"button\"\n      variant=\"solid\"\n      tone=\"primary\"\n      size=\"xs\"\n      disabled={disabled ?? !ask.canContinue}\n      onClick={handleClick}\n      className={className}\n      {...props}\n    >\n      {children ?? (\n        <>\n          Continue\n          <Kbd data-user-ask-submit-kbd=\"true\">⏎</Kbd>\n        </>\n      )}\n    </Button>\n  );\n}\n"
    }
  ],
  "css": {
    "@import \"../components/control-ui/styles/recipes/user-ask.css\"": {},
    "@import \"../components/control-ui/styles/recipes/user-ask-details.css\"": {}
  },
  "meta": {}
}
