{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"questions","title":"Questions","description":"Follow-up clarification questions with single or multiple choice, carousel navigation, and batch submission","dependencies":["@hugeicons/react","@hugeicons/core-free-icons"],"registryDependencies":["carousel","button","card","checkbox"],"files":[{"path":"components/nexus-ui/questions.tsx","content":"\"use client\";\n\nimport * as React from \"react\";\nimport {\n  ArrowLeft01Icon,\n  ArrowRight01Icon,\n  Cancel01Icon,\n  Edit03Icon,\n} from \"@hugeicons/core-free-icons\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Card,\n  CardContent,\n  CardFooter,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport {\n  Carousel,\n  CarouselContent,\n  CarouselItem,\n  type CarouselApi,\n} from \"@/components/ui/carousel\";\nimport { cn } from \"@/lib/utils\";\n\nexport type QuestionType = \"single\" | \"multiple\";\n\nexport type QuestionOptionInput = {\n  value: string;\n  label: React.ReactNode;\n};\n\nexport type QuestionInput = {\n  id: string;\n  type: QuestionType;\n  prompt: React.ReactNode;\n  options: QuestionOptionInput[];\n  required?: boolean;\n};\n\nexport const QUESTION_OTHER_VALUE = \"__other__\";\nexport const QUESTION_NO_PREFERENCE_VALUE = \"__no_preference__\";\nexport const QUESTION_NO_PREFERENCE_LABEL = \"[No Preference]\";\n\nexport type RegisteredQuestion = {\n  id: string;\n  type: QuestionType;\n  prompt: React.ReactNode;\n  required?: boolean;\n  index: number;\n  options?: QuestionOptionInput[];\n};\n\ntype QuestionScope = {\n  id: string;\n  type: QuestionType;\n};\n\nexport type QuestionSubmissionAnswer = {\n  value: string;\n  label: React.ReactNode;\n};\n\nconst NO_PREFERENCE_ANSWER: QuestionSubmissionAnswer = {\n  value: QUESTION_NO_PREFERENCE_VALUE,\n  label: QUESTION_NO_PREFERENCE_LABEL,\n};\n\nexport type SingleQuestionAnswerState = {\n  type: \"single\";\n  value: string;\n  other?: string;\n};\n\nexport type MultipleQuestionAnswerState = {\n  type: \"multiple\";\n  value: string[];\n  other?: string;\n};\n\nexport type QuestionAnswerState =\n  | SingleQuestionAnswerState\n  | MultipleQuestionAnswerState;\n\nexport type QuestionsSubmission = Array<\n  | {\n      questionId: string;\n      prompt: React.ReactNode;\n      type: \"single\";\n      status: \"answered\";\n      answer: QuestionSubmissionAnswer;\n    }\n  | {\n      questionId: string;\n      prompt: React.ReactNode;\n      type: \"single\";\n      status: \"skipped\";\n      answer: QuestionSubmissionAnswer;\n    }\n  | {\n      questionId: string;\n      prompt: React.ReactNode;\n      type: \"multiple\";\n      status: \"answered\";\n      answer: QuestionSubmissionAnswer[];\n    }\n  | {\n      questionId: string;\n      prompt: React.ReactNode;\n      type: \"multiple\";\n      status: \"skipped\";\n      answer: QuestionSubmissionAnswer[];\n    }\n>;\n\nfunction isQuestionAnswered(\n  question: RegisteredQuestion,\n  answer: QuestionAnswerState | undefined,\n): boolean {\n  if (!answer || answer.type !== question.type) return false;\n\n  if (question.type === \"single\") {\n    if (answer.value === QUESTION_OTHER_VALUE) {\n      return Boolean(answer.other?.trim());\n    }\n    return Boolean(answer.value);\n  }\n\n  return answer.value.length > 0 || Boolean(answer.other?.trim());\n}\n\nfunction isBlockedByRequired(\n  question: RegisteredQuestion | undefined,\n  answers: Record<string, QuestionAnswerState>,\n): boolean {\n  return Boolean(\n    question?.required && !isQuestionAnswered(question, answers[question.id]),\n  );\n}\n\nfunction isOtherAnswer(\n  answer: QuestionAnswerState | undefined,\n  type: QuestionType,\n): boolean {\n  if (!answer || answer.type !== type) return false;\n  if (type === \"single\") return answer.value === QUESTION_OTHER_VALUE;\n  return (\n    answer.value.includes(QUESTION_OTHER_VALUE) || Boolean(answer.other?.trim())\n  );\n}\n\nfunction canSubmitQuestions(\n  questions: RegisteredQuestion[],\n  answers: Record<string, QuestionAnswerState>,\n): boolean {\n  if (questions.length === 0) return false;\n  return !questions.some(\n    (question) =>\n      question.required && !isQuestionAnswered(question, answers[question.id]),\n  );\n}\n\nfunction optionLabel(\n  question: RegisteredQuestion,\n  value: string,\n  other?: string,\n): React.ReactNode {\n  if (value === QUESTION_OTHER_VALUE) {\n    return other?.trim() || \"Other\";\n  }\n  return question.options?.find((option) => option.value === value)?.label ?? value;\n}\n\nfunction skippedSubmission(\n  question: RegisteredQuestion,\n): QuestionsSubmission[number] {\n  return question.type === \"single\"\n    ? {\n        questionId: question.id,\n        prompt: question.prompt,\n        type: \"single\",\n        status: \"skipped\",\n        answer: NO_PREFERENCE_ANSWER,\n      }\n    : {\n        questionId: question.id,\n        prompt: question.prompt,\n        type: \"multiple\",\n        status: \"skipped\",\n        answer: [NO_PREFERENCE_ANSWER],\n      };\n}\n\nfunction buildSubmission(\n  questions: RegisteredQuestion[],\n  answers: Record<string, QuestionAnswerState>,\n): QuestionsSubmission {\n  return questions.map((question) => {\n    const answer = answers[question.id];\n    if (!isQuestionAnswered(question, answer)) {\n      return skippedSubmission(question);\n    }\n\n    if (question.type === \"single\" && answer?.type === \"single\") {\n      return {\n        questionId: question.id,\n        prompt: question.prompt,\n        type: \"single\",\n        status: \"answered\",\n        answer: {\n          value: answer.value,\n          label: optionLabel(question, answer.value, answer.other),\n        },\n      };\n    }\n\n    if (question.type === \"multiple\" && answer?.type === \"multiple\") {\n      const submissionAnswers = answer.value.map((value) => ({\n        value,\n        label: optionLabel(question, value, answer.other),\n      }));\n\n      if (answer.other?.trim() && !answer.value.includes(QUESTION_OTHER_VALUE)) {\n        submissionAnswers.push({\n          value: QUESTION_OTHER_VALUE,\n          label: answer.other.trim(),\n        });\n      }\n\n      return {\n        questionId: question.id,\n        prompt: question.prompt,\n        type: \"multiple\",\n        status: \"answered\",\n        answer: submissionAnswers,\n      };\n    }\n\n    return skippedSubmission(question);\n  });\n}\n\nfunction createQuestionsFromItems(items: QuestionInput[]): RegisteredQuestion[] {\n  return items.map((item, index) => ({\n    id: item.id,\n    type: item.type,\n    prompt: item.prompt,\n    required: item.required ?? false,\n    index,\n    options: item.options,\n  }));\n}\n\ntype QuestionsRootContextValue = {\n  questions: RegisteredQuestion[];\n  index: number;\n  answers: Record<string, QuestionAnswerState>;\n  selectSingle: (\n    questionId: string,\n    value: string,\n    other?: string,\n    options?: { autoAdvance?: boolean },\n  ) => void;\n  toggleMultiple: (questionId: string, value: string) => void;\n  setMultipleOther: (questionId: string, other: string) => void;\n  clearAnswer: (questionId: string) => void;\n  skip: () => void;\n  submit: () => void;\n  goNext: () => void;\n  goPrev: () => void;\n  carouselApi: CarouselApi | null;\n  setCarouselApi: (api: CarouselApi | undefined) => void;\n  onDismiss?: () => void;\n};\n\nconst QuestionsRootContext =\n  React.createContext<QuestionsRootContextValue | null>(null);\n\nconst QuestionContext = React.createContext<QuestionScope | null>(null);\n\nfunction useQuestionsRoot(component: string): QuestionsRootContextValue {\n  const ctx = React.useContext(QuestionsRootContext);\n  if (!ctx) {\n    throw new Error(`${component} must be used within Questions`);\n  }\n  return ctx;\n}\n\nfunction useQuestion(component: string): QuestionScope {\n  const ctx = React.useContext(QuestionContext);\n  if (!ctx) {\n    throw new Error(`${component} must be used within Question`);\n  }\n  return ctx;\n}\n\nexport type QuestionsProps = Omit<React.ComponentProps<typeof Card>, \"onSubmit\"> & {\n  items: QuestionInput[];\n  autoAdvance?: boolean;\n  onSubmit?: (submission: QuestionsSubmission) => void;\n  onSkip?: (questionId: string) => void;\n  onDismiss?: () => void;\n};\n\nfunction Questions({\n  className,\n  items,\n  autoAdvance = true,\n  onSubmit,\n  onSkip,\n  onDismiss,\n  children,\n  ...props\n}: QuestionsProps) {\n  const questions = React.useMemo(() => createQuestionsFromItems(items), [items]);\n  const [index, setIndex] = React.useState(0);\n  const [answers, setAnswers] = React.useState<Record<string, QuestionAnswerState>>(\n    {},\n  );\n  const answersRef = React.useRef(answers);\n  const [carouselApi, setCarouselApi] = React.useState<CarouselApi | null>(null);\n\n  const questionCount = questions.length;\n  const clampedIndex =\n    questionCount === 0 ? 0 : Math.min(Math.max(0, index), questionCount - 1);\n\n  const goToIndex = React.useCallback(\n    (nextIndex: number) => {\n      if (questionCount === 0) return;\n      const target = Math.min(Math.max(0, nextIndex), questionCount - 1);\n      setIndex(target);\n      carouselApi?.scrollTo(target);\n    },\n    [carouselApi, questionCount],\n  );\n\n  const clearAnswer = React.useCallback((questionId: string) => {\n    setAnswers((prev) => {\n      if (!(questionId in prev)) return prev;\n      const next = { ...prev };\n      delete next[questionId];\n      answersRef.current = next;\n      return next;\n    });\n  }, []);\n\n  const selectSingle = React.useCallback(\n    (\n      questionId: string,\n      value: string,\n      other?: string,\n      options?: { autoAdvance?: boolean },\n    ) => {\n      setAnswers((prev) => {\n        const next = {\n          ...prev,\n          [questionId]: {\n            type: \"single\" as const,\n            value,\n            ...(other ? { other } : {}),\n          },\n        };\n        answersRef.current = next;\n        return next;\n      });\n\n      if (options?.autoAdvance === false || !autoAdvance) return;\n\n      const questionIndex = questions.findIndex((q) => q.id === questionId);\n      if (questionIndex < 0 || questionIndex >= questions.length - 1) return;\n      goToIndex(questionIndex + 1);\n    },\n    [autoAdvance, goToIndex, questions],\n  );\n\n  const toggleMultiple = React.useCallback((questionId: string, value: string) => {\n    setAnswers((prev) => {\n      const existing = prev[questionId];\n      const currentValues =\n        existing?.type === \"multiple\" ? existing.value : [];\n      const nextValues = currentValues.includes(value)\n        ? currentValues.filter((item) => item !== value)\n        : [...currentValues, value];\n\n      return {\n        ...prev,\n        [questionId]: {\n          type: \"multiple\",\n          value: nextValues,\n          ...(existing?.type === \"multiple\" && existing.other\n            ? { other: existing.other }\n            : {}),\n        },\n      };\n    });\n  }, []);\n\n  const setMultipleOther = React.useCallback((questionId: string, other: string) => {\n    setAnswers((prev) => {\n      const existing = prev[questionId];\n      const currentValues = existing?.type === \"multiple\" ? existing.value : [];\n\n      return {\n        ...prev,\n        [questionId]: {\n          type: \"multiple\",\n          value: currentValues,\n          other,\n        },\n      };\n    });\n  }, []);\n\n  const goNext = React.useCallback(() => {\n    const current = questions[clampedIndex];\n    if (!current || isBlockedByRequired(current, answers)) return;\n    if (clampedIndex < questionCount - 1) goToIndex(clampedIndex + 1);\n  }, [answers, clampedIndex, goToIndex, questionCount, questions]);\n\n  const goPrev = React.useCallback(() => {\n    if (clampedIndex > 0) {\n      goToIndex(clampedIndex - 1);\n    }\n  }, [clampedIndex, goToIndex]);\n\n  const skip = React.useCallback(() => {\n    const current = questions[clampedIndex];\n    if (!current || clampedIndex >= questionCount - 1) return;\n    if (isBlockedByRequired(current, answers)) return;\n    onSkip?.(current.id);\n    goToIndex(clampedIndex + 1);\n  }, [answers, clampedIndex, goToIndex, onSkip, questionCount, questions]);\n\n  const submit = React.useCallback(() => {\n    if (!canSubmitQuestions(questions, answers)) return;\n\n    onSubmit?.(buildSubmission(questions, answers));\n    answersRef.current = {};\n    setAnswers({});\n    goToIndex(0);\n  }, [answers, goToIndex, onSubmit, questions]);\n\n  React.useEffect(() => {\n    if (!carouselApi) return;\n\n    const onSelect = () => {\n      const newIndex = carouselApi.selectedScrollSnap();\n      const oldIndex = carouselApi.previousScrollSnap();\n      if (newIndex === oldIndex) return;\n\n      const oldQuestion = questions[oldIndex];\n      if (\n        newIndex > oldIndex &&\n        isBlockedByRequired(oldQuestion, answersRef.current)\n      ) {\n        carouselApi.scrollTo(oldIndex);\n        return;\n      }\n\n      setIndex(newIndex);\n    };\n\n    carouselApi.on(\"select\", onSelect);\n    return () => {\n      carouselApi.off(\"select\", onSelect);\n    };\n  }, [carouselApi, questions]);\n\n  React.useEffect(() => {\n    if (!carouselApi) return;\n    if (carouselApi.selectedScrollSnap() !== clampedIndex) {\n      carouselApi.scrollTo(clampedIndex);\n    }\n  }, [carouselApi, clampedIndex]);\n\n  const rootValue = React.useMemo<QuestionsRootContextValue>(\n    () => ({\n      questions,\n      index: clampedIndex,\n      answers,\n      selectSingle,\n      toggleMultiple,\n      setMultipleOther,\n      clearAnswer,\n      skip,\n      submit,\n      goNext,\n      goPrev,\n      carouselApi,\n      setCarouselApi: (api) => setCarouselApi(api ?? null),\n      onDismiss,\n    }),\n    [\n      answers,\n      clampedIndex,\n      carouselApi,\n      clearAnswer,\n      goNext,\n      goPrev,\n      onDismiss,\n      questions,\n      selectSingle,\n      setMultipleOther,\n      skip,\n      submit,\n      toggleMultiple,\n    ],\n  );\n\n  return (\n    <QuestionsRootContext.Provider value={rootValue}>\n      <Card\n        data-slot=\"questions\"\n        className={cn(\n          \"mx-auto w-full max-w-xl gap-0 rounded-3xl px-1 pt-4 pb-1! shadow-none shadow-border/50 dark:border-accent dark:shadow-background/50\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n      </Card>\n    </QuestionsRootContext.Provider>\n  );\n}\n\nexport type QuestionProps = {\n  id: string;\n  children?: React.ReactNode;\n};\n\nfunction Question({ id, children }: QuestionProps) {\n  const { questions } = useQuestionsRoot(\"Question\");\n  const registered = questions.find((question) => question.id === id);\n\n  if (!registered) {\n    throw new Error(`Question \"${id}\" is not in Questions items`);\n  }\n\n  const scope = React.useMemo<QuestionScope>(\n    () => ({ id: registered.id, type: registered.type }),\n    [registered.id, registered.type],\n  );\n\n  return (\n    <QuestionContext.Provider value={scope}>\n      <CardContent data-slot=\"question\" className=\"w-full p-1.5\">\n        {children}\n      </CardContent>\n    </QuestionContext.Provider>\n  );\n}\n\nconst questionOptionsListClassName = cn(\n  \"flex w-full flex-col\",\n  // Change gap here only — dividers stay centered in the spacing.\n  \"[--question-options-gap:--spacing(0)] gap-[length:var(--question-options-gap)]\",\n  \"[&>*:not(:last-child)]:relative\",\n  \"[&>*:not(:last-child)]:after:pointer-events-none\",\n  \"[&>*:not(:last-child)]:after:absolute\",\n  \"[&>*:not(:last-child)]:after:top-[calc(100%+var(--question-options-gap)/2)]\",\n  \"[&>*:not(:last-child)]:after:-translate-y-1/2\",\n  \"[&>*:not(:last-child)]:after:right-2.5\",\n  \"[&>*:not(:last-child)]:after:left-2.5\",\n  \"[&>*:not(:last-child)]:after:z-10\",\n  \"[&>*:not(:last-child)]:after:h-px\",\n  \"[&>*:not(:last-child)]:after:bg-border/20\",\n  \"[&>*:not(:last-child)]:after:content-['']\",\n);\n\nconst questionRowClassName =\n  \"group/row flex h-11 w-full items-center gap-2.5 rounded-lg bg-transparent px-2.5 text-left transition-all hover:bg-muted\";\n\nconst questionOptionRowClassName = cn(questionRowClassName, \"active:scale-99\");\n\nexport type QuestionOptionsProps = React.HTMLAttributes<HTMLDivElement>;\n\nfunction QuestionOptions({ className, children, ...props }: QuestionOptionsProps) {\n  const question = useQuestion(\"QuestionOptions\");\n\n  return (\n    <div\n      data-slot=\"question-options\"\n      role={question.type === \"single\" ? \"listbox\" : \"group\"}\n      className={cn(questionOptionsListClassName, className)}\n      {...props}\n    >\n      {React.Children.map(children, (child, optionIndex) => {\n        if (!React.isValidElement(child)) return child;\n        if (child.type !== QuestionOption) return child;\n        return React.cloneElement(\n          child as React.ReactElement<{ optionIndex?: number }>,\n          { optionIndex },\n        );\n      })}\n    </div>\n  );\n}\n\nexport type QuestionOptionProps = Omit<\n  React.ButtonHTMLAttributes<HTMLButtonElement>,\n  \"value\"\n> & {\n  value: string;\n  optionIndex?: number;\n  children?: React.ReactNode;\n};\n\nfunction QuestionOption({\n  value,\n  optionIndex = 0,\n  className,\n  children,\n  onClick,\n  ...props\n}: QuestionOptionProps) {\n  const question = useQuestion(\"QuestionOption\");\n  const root = useQuestionsRoot(\"QuestionOption\");\n  const answer = root.answers[question.id];\n  const displayIndex = optionIndex + 1;\n\n  const isSelected =\n    question.type === \"single\"\n      ? answer?.type === \"single\" && answer.value === value\n      : answer?.type === \"multiple\" && answer.value.includes(value);\n\n  const handleSelect = () => {\n    if (question.type === \"single\") {\n      if (isSelected) {\n        root.clearAnswer(question.id);\n        return;\n      }\n      root.selectSingle(question.id, value);\n      return;\n    }\n    root.toggleMultiple(question.id, value);\n  };\n\n  if (question.type === \"multiple\") {\n    return (\n      <label\n        data-slot=\"question-option\"\n        className={cn(\n          questionOptionRowClassName,\n          \"cursor-pointer\",\n          isSelected && \"bg-muted\",\n          className,\n        )}\n      >\n        <Checkbox\n          checked={isSelected}\n          onCheckedChange={handleSelect}\n          className=\"mx-1.25 size-4.5 shadow-none transition-colors group-hover/row:data-[state=unchecked]:border-ring/50 cursor-pointer\"\n        />\n        <span className={cn(\"min-w-0 flex-1 truncate text-sm text-ring transition-all group-hover/row:text-primary\", isSelected && \"text-primary\")}>\n          {children}\n        </span>\n      </label>\n    );\n  }\n\n  return (\n    <button\n      type=\"button\"\n      role=\"option\"\n      aria-selected={isSelected}\n      data-slot=\"question-option\"\n      className={cn(\n        questionOptionRowClassName,\n        \"cursor-pointer\",\n        isSelected && \"bg-muted\",\n        className,\n      )}\n      onClick={(event) => {\n        onClick?.(event);\n        handleSelect();\n      }}\n      {...props}\n    >\n      <span\n        className={cn(\n          \"relative flex size-7 shrink-0 items-center justify-center overflow-hidden rounded-md bg-border/30 transition-all group-hover/row:bg-border/70\",\n          isSelected && \"bg-border/70\",\n        )}\n      >\n        <span\n          className={cn(\n            \"text-sm text-muted-foreground transition-all group-hover/row:text-primary\",\n            isSelected && \"text-primary\",\n          )}\n        >\n          {displayIndex}\n        </span>\n      </span>\n      <span\n        className={cn(\n          \"min-w-0 flex-1 truncate text-sm text-ring transition-all group-hover/row:text-primary\",\n          isSelected && \"text-primary\",\n        )}\n      >\n        {children}\n      </span>\n      <HugeiconsIcon\n        icon={ArrowRight01Icon}\n        strokeWidth={2.0}\n        className=\"size-4 text-muted-foreground opacity-0 transition-opacity group-hover/row:opacity-100\"\n      />\n    </button>\n  );\n}\n\nexport type QuestionOtherProps = Omit<\n  React.InputHTMLAttributes<HTMLInputElement>,\n  \"value\" | \"onChange\"\n>;\n\nfunction QuestionOther({\n  className,\n  placeholder = \"Other...\",\n  onKeyDown,\n  ...props\n}: QuestionOtherProps) {\n  const question = useQuestion(\"QuestionOther\");\n  const root = useQuestionsRoot(\"QuestionOther\");\n  const answer = root.answers[question.id];\n\n  const otherValue =\n    answer?.type === \"single\" || answer?.type === \"multiple\"\n      ? (answer.other ?? \"\")\n      : \"\";\n\n  const isOtherSelected = isOtherAnswer(answer, question.type);\n\n  const handleOtherToggle = () => {\n    if (question.type === \"single\") {\n      if (otherValue.trim()) {\n        root.selectSingle(\n          question.id,\n          QUESTION_OTHER_VALUE,\n          otherValue.trim(),\n          { autoAdvance: false },\n        );\n      }\n      return;\n    }\n    root.toggleMultiple(question.id, QUESTION_OTHER_VALUE);\n  };\n\n  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {\n    const next = event.target.value;\n    if (question.type === \"single\") {\n      if (!next.trim()) {\n        root.clearAnswer(question.id);\n        return;\n      }\n      root.selectSingle(question.id, QUESTION_OTHER_VALUE, next, {\n        autoAdvance: false,\n      });\n      return;\n    }\n    root.setMultipleOther(question.id, next);\n    if (next.trim() && !isOtherSelected) {\n      root.toggleMultiple(question.id, QUESTION_OTHER_VALUE);\n    }\n  };\n\n  if (question.type === \"multiple\") {\n    return (\n      <label\n        data-slot=\"question-other\"\n        className={cn(questionRowClassName, \"cursor-text\", className)}\n      >\n        <Checkbox\n          checked={isOtherSelected}\n          onCheckedChange={handleOtherToggle}\n          className=\"mx-1.25 size-4.5 shadow-none transition-colors group-hover/row:data-[state=unchecked]:border-ring/50\"\n        />\n        <input\n          type=\"text\"\n          value={otherValue}\n          placeholder={placeholder}\n          onChange={handleChange}\n          onKeyDown={onKeyDown}\n          className=\"text-primary h-full min-w-0 flex-1 truncate text-sm transition-all outline-none placeholder:text-ring/70\"\n          {...props}\n        />\n      </label>\n    );\n  }\n\n  return (\n    <div\n      data-slot=\"question-other\"\n      className={cn(\n        questionRowClassName,\n        \"cursor-text\",\n        isOtherSelected && \"bg-muted\",\n        className,\n      )}\n    >\n      <span\n        className={cn(\n          \"relative flex size-7 shrink-0 items-center justify-center overflow-hidden rounded-md bg-border/30 transition-all group-hover/row:bg-border/70\",\n          isOtherSelected && \"bg-border/70\",\n        )}\n      >\n        <HugeiconsIcon\n          icon={Edit03Icon}\n          strokeWidth={2.0}\n          className={cn(\n            \"size-4 text-muted-foreground transition-all group-hover/row:text-primary\",\n            isOtherSelected && \"text-primary\",\n          )}\n        />\n      </span>\n      <input\n        type=\"text\"\n        value={otherValue}\n        placeholder={placeholder}\n        onChange={handleChange}\n        onKeyDown={onKeyDown}\n        className={cn(\n          \"text-primary h-full min-w-0 flex-1 truncate text-sm transition-all outline-none placeholder:text-ring\",\n          isOtherSelected && \"text-primary\",\n        )}\n        {...props}\n      />\n    </div>\n  );\n}\n\nexport type QuestionsTitleProps = React.ComponentProps<typeof CardTitle>;\n\nfunction QuestionsTitle({ className, children, ...props }: QuestionsTitleProps) {\n  const root = useQuestionsRoot(\"QuestionsTitle\");\n  const current = root.questions[root.index];\n\n  return (\n    <CardTitle\n      data-slot=\"questions-title\"\n      className={cn(\"flex-1 text-sm font-normal\", className)}\n      {...props}\n    >\n      {children ?? current?.prompt}\n    </CardTitle>\n  );\n}\n\nexport type QuestionsDismissProps = React.ComponentProps<typeof Button>;\n\nfunction QuestionsDismiss({ className, onClick, ...props }: QuestionsDismissProps) {\n  const root = useQuestionsRoot(\"QuestionsDismiss\");\n\n  return (\n    <Button\n      type=\"button\"\n      size=\"icon-xs\"\n      variant=\"ghost\"\n      data-slot=\"questions-dismiss\"\n      className={cn(\n        \"cursor-pointer rounded-full bg-transparent text-[13px] text-muted-foreground backdrop-blur-lg hover:bg-secondary/80 active:scale-97\",\n        className,\n      )}\n      onClick={(event) => {\n        onClick?.(event);\n        root.onDismiss?.();\n      }}\n      {...props}\n    >\n      <HugeiconsIcon icon={Cancel01Icon} strokeWidth={2.0} className=\"size-3.5\" />\n    </Button>\n  );\n}\n\nexport type QuestionsHeaderProps = React.ComponentProps<typeof CardHeader>;\n\nfunction QuestionsHeader({ className, ...props }: QuestionsHeaderProps) {\n  return (\n    <CardHeader\n      data-slot=\"questions-header\"\n      className={cn(\n        \"flex w-full items-center justify-center gap-2.5 pr-3 pb-1.5 pl-4\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport type QuestionsFooterProps = React.ComponentProps<typeof CardFooter>;\n\nfunction QuestionsFooter({ className, ...props }: QuestionsFooterProps) {\n  return (\n    <CardFooter\n      data-slot=\"questions-footer\"\n      className={cn(\n        \"justify-end gap-2 border-none bg-transparent px-3 pt-0 pb-3\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport type QuestionsSkipProps = React.ComponentProps<typeof Button>;\n\nfunction QuestionsSkip({ className, disabled, onClick, ...props }: QuestionsSkipProps) {\n  const { questions, index, answers, skip } = useQuestionsRoot(\"QuestionsSkip\");\n  const current = questions[index];\n  const canSkip =\n    index < questions.length - 1 &&\n    Boolean(current) &&\n    !isBlockedByRequired(current, answers);\n\n  return (\n    <Button\n      type=\"button\"\n      variant=\"ghost\"\n      size=\"sm\"\n      data-slot=\"questions-skip\"\n      disabled={disabled ?? !canSkip}\n      className={cn(\n        \"text-muted-foreground hover:text-primary active:scale-99\",\n        className,\n      )}\n      onClick={(event) => {\n        onClick?.(event);\n        skip();\n      }}\n      {...props}\n    >\n      Skip\n    </Button>\n  );\n}\n\nexport type QuestionsSubmitProps = React.ComponentProps<typeof Button> & {\n  showOnLastQuestion?: boolean;\n  disableUntilLastQuestion?: boolean;\n};\n\nfunction QuestionsSubmit({\n  className,\n  disabled,\n  onClick,\n  children = \"Submit\",\n  showOnLastQuestion = false,\n  disableUntilLastQuestion = false,\n  ...props\n}: QuestionsSubmitProps) {\n  const { questions, index, answers, submit } = useQuestionsRoot(\"QuestionsSubmit\");\n  const canSubmit = canSubmitQuestions(questions, answers);\n  const onLastQuestion = index >= questions.length - 1;\n\n  if (showOnLastQuestion && !onLastQuestion) {\n    return null;\n  }\n\n  const isDisabled =\n    disabled ??\n    (!canSubmit || (disableUntilLastQuestion && !onLastQuestion));\n\n  return (\n    <Button\n      type=\"button\"\n      variant=\"default\"\n      size=\"sm\"\n      data-slot=\"questions-submit\"\n      disabled={isDisabled}\n      className={cn(\"active:scale-99\", className)}\n      onClick={(event) => {\n        onClick?.(event);\n        submit();\n      }}\n      {...props}\n    >\n      {children}\n    </Button>\n  );\n}\n\nfunction useCarouselViewportHeight(\n  wrapRef: React.RefObject<HTMLDivElement | null>,\n  carouselApi: CarouselApi | null,\n  activeIndex: number,\n) {\n  React.useLayoutEffect(() => {\n    const vp = wrapRef.current?.querySelector<HTMLElement>(\n      \"[data-slot=carousel-content]\",\n    );\n    if (!vp) return;\n    const clear = () => {\n      vp.style.height = \"\";\n      vp.style.transition = \"\";\n    };\n    if (!carouselApi) return clear();\n    vp.style.transition = \"height 500ms ease-out\";\n    const sync = () => {\n      const h = carouselApi.slideNodes()[activeIndex]?.offsetHeight ?? 0;\n      vp.style.height = h > 0 ? `${h}px` : \"\";\n    };\n    sync();\n    const slide = carouselApi.slideNodes()[activeIndex];\n    if (!slide) return clear;\n    const ro = new ResizeObserver(sync);\n    ro.observe(slide);\n    return () => {\n      ro.disconnect();\n      clear();\n    };\n  }, [wrapRef, carouselApi, activeIndex]);\n}\n\nexport type QuestionsCarouselProps = React.ComponentProps<typeof Carousel>;\n\nfunction QuestionsCarousel({\n  setApi: setApiProp,\n  className,\n  children,\n  ...props\n}: QuestionsCarouselProps) {\n  const { setCarouselApi } = useQuestionsRoot(\"QuestionsCarousel\");\n\n  return (\n    <Carousel\n      data-slot=\"questions-carousel\"\n      className={className}\n      setApi={(api) => {\n        setCarouselApi(api);\n        setApiProp?.(api);\n      }}\n      opts={{ watchDrag: false }}\n      {...props}\n    >\n      {children}\n    </Carousel>\n  );\n}\n\nexport type QuestionsCarouselContentProps = React.ComponentProps<\n  typeof CarouselContent\n>;\n\nfunction QuestionsCarouselContent({\n  className,\n  ...props\n}: QuestionsCarouselContentProps) {\n  const root = useQuestionsRoot(\"QuestionsCarouselContent\");\n  const wrapRef = React.useRef<HTMLDivElement>(null);\n  useCarouselViewportHeight(wrapRef, root.carouselApi, root.index);\n\n  return (\n    <div ref={wrapRef} className=\"contents\">\n      <CarouselContent\n        data-slot=\"questions-carousel-content\"\n        className={className}\n        {...props}\n      />\n    </div>\n  );\n}\n\nexport type QuestionsCarouselItemProps = React.ComponentProps<\n  typeof CarouselItem\n>;\n\nfunction QuestionsCarouselItem({\n  className,\n  children,\n  ...props\n}: QuestionsCarouselItemProps) {\n  return (\n    <CarouselItem\n      data-slot=\"questions-carousel-item\"\n      className={cn(\"w-full self-start p-0 pl-0\", className)}\n      {...props}\n    >\n      {children}\n    </CarouselItem>\n  );\n}\n\nexport type QuestionsCarouselPaginationProps = React.HTMLAttributes<HTMLDivElement>;\n\nfunction QuestionsCarouselPagination({\n  className,\n  ...props\n}: QuestionsCarouselPaginationProps) {\n  return (\n    <div\n      data-slot=\"questions-carousel-pagination\"\n      className={cn(\"flex items-center gap-0.5\", className)}\n      {...props}\n    />\n  );\n}\n\nconst carouselNavClassName =\n  \"flex size-6 cursor-pointer items-center justify-center rounded-full text-muted-foreground outline-0 transition-all hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring/50 active:scale-97 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 dark:hover:bg-accent/50\";\n\nexport type QuestionsCarouselNavButtonProps =\n  React.ButtonHTMLAttributes<HTMLButtonElement>;\n\nfunction QuestionsCarouselPrev({\n  className,\n  children,\n  ...props\n}: QuestionsCarouselNavButtonProps) {\n  const { index, goPrev } = useQuestionsRoot(\"QuestionsCarouselPrev\");\n\n  return (\n    <button\n      type=\"button\"\n      data-slot=\"questions-carousel-prev\"\n      disabled={index <= 0}\n      className={cn(carouselNavClassName, className)}\n      onClick={() => goPrev()}\n      {...props}\n    >\n      {children ?? (\n        <HugeiconsIcon icon={ArrowLeft01Icon} strokeWidth={2} className=\"size-4\" />\n      )}\n    </button>\n  );\n}\n\nfunction QuestionsCarouselNext({\n  className,\n  children,\n  ...props\n}: QuestionsCarouselNavButtonProps) {\n  const { questions, index, answers, goNext } =\n    useQuestionsRoot(\"QuestionsCarouselNext\");\n  const current = questions[index];\n  const canGoNext =\n    index < questions.length - 1 &&\n    Boolean(current) &&\n    !isBlockedByRequired(current, answers);\n\n  return (\n    <button\n      type=\"button\"\n      data-slot=\"questions-carousel-next\"\n      disabled={!canGoNext}\n      className={cn(carouselNavClassName, className)}\n      onClick={() => goNext()}\n      {...props}\n    >\n      {children ?? (\n        <HugeiconsIcon icon={ArrowRight01Icon} strokeWidth={2} className=\"size-4\" />\n      )}\n    </button>\n  );\n}\n\nexport type QuestionsCarouselIndexProps = React.HTMLAttributes<HTMLSpanElement> & {\n  format?: \"of\" | \"slash\";\n};\n\nfunction QuestionsCarouselIndex({\n  className,\n  format = \"of\",\n  ...props\n}: QuestionsCarouselIndexProps) {\n  const { questions, index } = useQuestionsRoot(\"QuestionsCarouselIndex\");\n  const count = questions.length;\n  const current = count === 0 ? 0 : index + 1;\n\n  return (\n    <span\n      data-slot=\"questions-carousel-index\"\n      className={cn(\n        \"text-xs leading-4.5 font-[350] text-muted-foreground tabular-nums\",\n        className,\n      )}\n      {...props}\n    >\n      {format === \"slash\" ? `${current}/${count}` : `${current} of ${count}`}\n    </span>\n  );\n}\n\nexport {\n  Questions,\n  Question,\n  QuestionOptions,\n  QuestionOption,\n  QuestionOther,\n  QuestionsTitle,\n  QuestionsDismiss,\n  QuestionsHeader,\n  QuestionsFooter,\n  QuestionsSkip,\n  QuestionsSubmit,\n  QuestionsCarousel,\n  QuestionsCarouselContent,\n  QuestionsCarouselItem,\n  QuestionsCarouselPagination,\n  QuestionsCarouselPrev,\n  QuestionsCarouselNext,\n  QuestionsCarouselIndex,\n};\n","type":"registry:file","target":"~/components/nexus-ui/questions.tsx"}],"type":"registry:ui"}