{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"chain-of-thought","title":"Chain of Thought","description":"Structured multi-step thought timeline with step status, optional expandable output, and auto-close when steps finish","dependencies":["@hugeicons/react","@hugeicons/core-free-icons"],"registryDependencies":["collapsible"],"files":[{"path":"components/nexus-ui/chain-of-thought.tsx","content":"\"use client\";\n\nimport * as React from \"react\";\nimport { Alert02Icon, ArrowDown01Icon } from \"@hugeicons/core-free-icons\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\n\nimport {\n  Collapsible,\n  CollapsibleContent,\n  CollapsibleTrigger,\n} from \"@/components/ui/collapsible\";\nimport { TextShimmer } from \"@/components/nexus-ui/text-shimmer\";\nimport { useOnChange } from \"@/lib/use-on-change\";\nimport { cn } from \"@/lib/utils\";\n\ntype ChainOfThoughtRootContextValue = {\n  registerStep: (id: string, status: ChainOfThoughtStepStatus) => void;\n  allStepsComplete: boolean;\n  hasAnyError: boolean;\n};\n\nconst ChainOfThoughtRootContext =\n  React.createContext<ChainOfThoughtRootContextValue | null>(null);\n\nfunction useChainOfThoughtRootContext(component: string) {\n  const ctx = React.useContext(ChainOfThoughtRootContext);\n  if (!ctx) {\n    throw new Error(`${component} must be used within <ChainOfThought>`);\n  }\n  return ctx;\n}\n\ntype ChainOfThoughtStepContextValue = {\n  status: ChainOfThoughtStepStatus;\n  hasContent: boolean;\n};\n\nconst ChainOfThoughtStepContext =\n  React.createContext<ChainOfThoughtStepContextValue | null>(null);\n\nfunction useChainOfThoughtStepContext(component: string) {\n  const ctx = React.useContext(ChainOfThoughtStepContext);\n  if (!ctx) {\n    throw new Error(`${component} must be used within <ChainOfThoughtStep>`);\n  }\n  return ctx;\n}\n\ntype ChainOfThoughtStepStatus = \"pending\" | \"active\" | \"completed\" | \"error\";\n\ntype ChainOfThoughtProps = Omit<\n  React.ComponentProps<typeof Collapsible>,\n  \"open\" | \"defaultOpen\" | \"onOpenChange\"\n> & {\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  autoCloseOnAllComplete?: boolean;\n};\n\nfunction ChainOfThought({\n  className,\n  open: openProp,\n  defaultOpen = true,\n  onOpenChange,\n  autoCloseOnAllComplete = true,\n  children,\n  ...props\n}: ChainOfThoughtProps) {\n  const isControlled = openProp !== undefined;\n  const [internalOpen, setInternalOpen] = React.useState(defaultOpen);\n  const [stepStatuses, setStepStatuses] = React.useState<\n    Record<string, ChainOfThoughtStepStatus>\n  >({});\n\n  const { allStepsComplete, hasAnyError } = React.useMemo(() => {\n    const statuses = Object.values(stepStatuses);\n    return {\n      allStepsComplete:\n        statuses.length > 0 &&\n        statuses.every((status) => status === \"completed\"),\n      hasAnyError: statuses.some((status) => status === \"error\"),\n    };\n  }, [stepStatuses]);\n\n  const open = isControlled ? openProp : internalOpen;\n\n  const registerStep = React.useCallback(\n    (id: string, status: ChainOfThoughtStepStatus) => {\n      setStepStatuses((prev) => {\n        if (prev[id] === status) return prev;\n        return { ...prev, [id]: status };\n      });\n    },\n    [],\n  );\n\n  const contextValue = React.useMemo(\n    () => ({ registerStep, allStepsComplete, hasAnyError }),\n    [allStepsComplete, hasAnyError, registerStep],\n  );\n\n  const handleOpenChange = React.useCallback(\n    (nextOpen: boolean) => {\n      if (!isControlled) {\n        setInternalOpen(nextOpen);\n      }\n      onOpenChange?.(nextOpen);\n    },\n    [isControlled, onOpenChange],\n  );\n\n  useOnChange(allStepsComplete, (current, previous) => {\n    if (!autoCloseOnAllComplete || isControlled) return;\n    if (!previous && current) {\n      setInternalOpen(false);\n      onOpenChange?.(false);\n    }\n  });\n\n  return (\n    <ChainOfThoughtRootContext.Provider value={contextValue}>\n      <Collapsible\n        data-slot=\"chain-of-thought\"\n        className={cn(\"not-prose w-full\", className)}\n        open={open}\n        onOpenChange={handleOpenChange}\n        {...props}\n      >\n        {children}\n      </Collapsible>\n    </ChainOfThoughtRootContext.Provider>\n  );\n}\n\ntype ChainOfThoughtTriggerProps = React.ComponentProps<\n  typeof CollapsibleTrigger\n> & {\n  label?: React.ReactNode;\n  icon?: React.ReactNode;\n};\n\nfunction ChainOfThoughtTrigger({\n  className,\n  icon,\n  label,\n  children,\n  ...props\n}: ChainOfThoughtTriggerProps) {\n  const { allStepsComplete, hasAnyError } = useChainOfThoughtRootContext(\n    \"ChainOfThoughtTrigger\",\n  );\n  const isActive = !allStepsComplete && !hasAnyError;\n\n  return (\n    <CollapsibleTrigger\n      data-slot=\"chain-of-thought-trigger\"\n      data-active={String(isActive)}\n      className={cn(\n        \"group flex w-full cursor-pointer items-center gap-1.25 overflow-hidden text-muted-foreground transition-colors hover:text-foreground\",\n        className,\n      )}\n      {...props}\n    >\n      {icon}\n      <div className=\"flex min-w-0 flex-1 items-start gap-1.25 overflow-hidden\">\n        <TextShimmer\n          className=\"truncate text-left text-sm leading-4.5 text-ellipsis whitespace-nowrap\"\n          spread={10}\n          invertLight\n          disableShimmer={!isActive}\n        >\n          {children ?? label}\n        </TextShimmer>\n        <HugeiconsIcon\n          icon={ArrowDown01Icon}\n          strokeWidth={2}\n          className=\"ml-0.5 size-4 shrink-0 opacity-0 transition-all group-hover:opacity-100 group-data-[state=open]:rotate-180 group-data-[state=open]:opacity-100\"\n        />\n      </div>\n    </CollapsibleTrigger>\n  );\n}\n\ntype ChainOfThoughtContentProps = React.ComponentProps<\n  typeof CollapsibleContent\n>;\n\nfunction ChainOfThoughtContent({\n  className,\n  children,\n  ...props\n}: ChainOfThoughtContentProps) {\n  return (\n    <CollapsibleContent\n      data-slot=\"chain-of-thought-content\"\n      className={cn(\n        \"mt-3 space-y-3\",\n        \"overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </CollapsibleContent>\n  );\n}\n\nfunction hasIconInStepTitle(children: React.ReactNode): boolean {\n  return React.Children.toArray(children).some(\n    (child) =>\n      React.isValidElement<{ icon?: React.ReactNode }>(child) &&\n      child.props.icon != null,\n  );\n}\n\ntype ChainOfThoughtStepProps = Omit<\n  React.ComponentProps<typeof Collapsible>,\n  \"open\" | \"defaultOpen\" | \"onOpenChange\"\n> & {\n  status?: ChainOfThoughtStepStatus;\n  hasContent?: boolean;\n  showConnector?: boolean;\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  autoCloseOnComplete?: boolean;\n};\n\nfunction ChainOfThoughtStep({\n  className,\n  status = \"pending\",\n  hasContent = false,\n  showConnector,\n  open: openProp,\n  defaultOpen = true,\n  onOpenChange,\n  autoCloseOnComplete = true,\n  children,\n  ...props\n}: ChainOfThoughtStepProps) {\n  const { registerStep } = useChainOfThoughtRootContext(\"ChainOfThoughtStep\");\n  const stepId = React.useId();\n  const showConnectorResolved = showConnector ?? hasIconInStepTitle(children);\n  const isControlled = openProp !== undefined;\n  const canAutoManageOpen = !isControlled && hasContent;\n  const [internalOpen, setInternalOpen] = React.useState(\n    () =>\n      defaultOpen ||\n      (hasContent && (status === \"active\" || status === \"error\")),\n  );\n  const open = isControlled ? openProp : internalOpen;\n\n  React.useEffect(() => {\n    registerStep(stepId, status);\n  }, [registerStep, status, stepId]);\n\n  const handleOpenChange = React.useCallback(\n    (nextOpen: boolean) => {\n      if (!isControlled) {\n        setInternalOpen(nextOpen);\n      }\n      onOpenChange?.(nextOpen);\n    },\n    [isControlled, onOpenChange],\n  );\n\n  useOnChange(status, (current, previous) => {\n    if (\n      canAutoManageOpen &&\n      (current === \"active\" || current === \"error\") &&\n      previous !== current\n    ) {\n      setInternalOpen(true);\n      onOpenChange?.(true);\n      return;\n    }\n\n    if (!canAutoManageOpen || !autoCloseOnComplete || previous === undefined) {\n      return;\n    }\n\n    if (previous !== \"completed\" && current === \"completed\") {\n      setInternalOpen(false);\n      onOpenChange?.(false);\n    }\n  });\n\n  return (\n    <ChainOfThoughtStepContext.Provider value={{ status, hasContent }}>\n      <Collapsible\n        data-slot=\"chain-of-thought-step\"\n        className={cn(\"relative w-full fade-in-0\", className)}\n        open={open}\n        onOpenChange={handleOpenChange}\n        {...props}\n      >\n        {children}\n        {showConnectorResolved ? (\n          <div\n            className={cn(\n              \"absolute top-4.75 -bottom-2.75 left-2 -mx-px w-px\",\n              status === \"error\" ? \"bg-destructive/20\" : \"bg-border/50\",\n            )}\n          ></div>\n        ) : null}\n      </Collapsible>\n    </ChainOfThoughtStepContext.Provider>\n  );\n}\n\ntype ChainOfThoughtStepTitleSharedProps = {\n  label?: React.ReactNode;\n  icon?: React.ReactNode;\n  children?: React.ReactNode;\n  collapsible?: boolean;\n};\n\ntype ChainOfThoughtStepTitleCollapsibleProps =\n  ChainOfThoughtStepTitleSharedProps &\n    Omit<React.ComponentProps<typeof CollapsibleTrigger>, \"children\"> & {\n      collapsible: true;\n    };\n\ntype ChainOfThoughtStepTitleStaticProps = ChainOfThoughtStepTitleSharedProps &\n  React.HTMLAttributes<HTMLDivElement> & {\n    collapsible?: false;\n  };\n\ntype ChainOfThoughtStepTitleProps =\n  | ChainOfThoughtStepTitleCollapsibleProps\n  | ChainOfThoughtStepTitleStaticProps;\n\nfunction ChainOfThoughtStepTitle({\n  className,\n  label: labelProp,\n  icon,\n  collapsible,\n  children,\n  ...props\n}: ChainOfThoughtStepTitleProps) {\n  const { hasContent, status } = useChainOfThoughtStepContext(\n    \"ChainOfThoughtStepTitle\",\n  );\n  const isCollapsible = collapsible ?? hasContent;\n  const isActive = status === \"active\";\n  const isError = status === \"error\";\n  const resolvedIcon =\n    isError && icon ? (\n      <HugeiconsIcon icon={Alert02Icon} strokeWidth={1.75} className=\"size-4\" />\n    ) : (\n      icon\n    );\n  const label = children ?? labelProp;\n\n  if (!isCollapsible) {\n    const staticProps = props as React.HTMLAttributes<HTMLDivElement>;\n\n    return (\n      <div\n        data-slot=\"chain-of-thought-step-title\"\n        data-active={String(isActive)}\n        className={cn(\n          \"group flex items-center text-sm leading-4.5 text-muted-foreground\",\n          isError && \"text-destructive\",\n          resolvedIcon ? \"gap-2\" : \"gap-0\",\n          className,\n        )}\n        {...staticProps}\n      >\n        {resolvedIcon ? (\n          <div className=\"relative flex size-4 shrink-0 items-center justify-center\">\n            {resolvedIcon}\n          </div>\n        ) : null}\n        <TextShimmer\n          className=\"truncate text-left text-sm leading-4.5 text-ellipsis whitespace-nowrap\"\n          spread={10}\n          invertLight\n          disableShimmer={!isActive}\n        >\n          {label}\n        </TextShimmer>\n      </div>\n    );\n  }\n\n  return (\n    <CollapsibleTrigger\n      data-slot=\"chain-of-thought-step-title\"\n      data-active={String(isActive)}\n      className={cn(\n        \"group flex w-full cursor-pointer items-center text-sm text-muted-foreground transition-colors hover:text-foreground\",\n        isError && \"text-destructive hover:text-destructive/90\",\n        resolvedIcon ? \"gap-2\" : \"gap-0\",\n        className,\n      )}\n      {...(props as Omit<\n        React.ComponentProps<typeof CollapsibleTrigger>,\n        \"children\"\n      >)}\n    >\n      {resolvedIcon ? (\n        <div className=\"relative flex size-4 shrink-0 items-center justify-center\">\n          {resolvedIcon}\n        </div>\n      ) : null}\n      <div className=\"flex min-w-0 flex-1 items-start gap-1.25 overflow-hidden\">\n        <TextShimmer\n          className=\"truncate text-left text-sm leading-4.5 text-ellipsis whitespace-nowrap\"\n          spread={10}\n          invertLight\n          disableShimmer={!isActive}\n        >\n          {label}\n        </TextShimmer>\n        <HugeiconsIcon\n          icon={ArrowDown01Icon}\n          strokeWidth={2}\n          className=\"ml-0.5 size-4 shrink-0 opacity-0 transition-all group-hover:opacity-100 group-data-[state=open]:rotate-180 group-data-[state=open]:opacity-100\"\n        />\n      </div>\n    </CollapsibleTrigger>\n  );\n}\n\ntype ChainOfThoughtStepContentProps = React.ComponentProps<\n  typeof CollapsibleContent\n>;\n\nfunction ChainOfThoughtStepContent({\n  className,\n  children,\n  ...props\n}: ChainOfThoughtStepContentProps) {\n  return (\n    <CollapsibleContent\n      data-slot=\"chain-of-thought-step-content\"\n      className={cn(\n        \"mt-2 ml-6\",\n        \"overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </CollapsibleContent>\n  );\n}\n\ntype ChainOfThoughtCompleteProps = React.HTMLAttributes<HTMLDivElement> & {\n  icon?: React.ReactNode;\n  label: React.ReactNode;\n};\n\nfunction ChainOfThoughtComplete({\n  className,\n  icon,\n  label,\n  ...props\n}: ChainOfThoughtCompleteProps) {\n  return (\n    <div\n      data-slot=\"chain-of-thought-complete\"\n      className={cn(\n        \"mt-0 flex items-center gap-2 text-sm leading-4.5 text-muted-foreground fade-in-0\",\n        !icon && \"gap-0\",\n        className,\n      )}\n      {...props}\n    >\n      {icon}\n      <span>{label}</span>\n    </div>\n  );\n}\n\nexport type { ChainOfThoughtStepStatus };\nexport {\n  ChainOfThought,\n  ChainOfThoughtTrigger,\n  ChainOfThoughtContent,\n  ChainOfThoughtStep,\n  ChainOfThoughtStepTitle,\n  ChainOfThoughtStepContent,\n  ChainOfThoughtComplete,\n};\n","type":"registry:file","target":"~/components/nexus-ui/chain-of-thought.tsx"},{"path":"lib/use-on-change.ts","content":"\"use client\";\n\nimport * as React from \"react\";\n\n/**\n * Runs `onChange` when `value` changes (compared against previous render).\n */\nexport function useOnChange<T>(\n  value: T,\n  onChange: (current: T, previous: T) => void,\n  isUpdated: (previous: T, current: T) => boolean = Object.is,\n) {\n  const previousRef = React.useRef(value);\n\n  React.useEffect(() => {\n    const previous = previousRef.current;\n    if (!isUpdated(previous, value)) {\n      onChange(value, previous);\n    }\n    previousRef.current = value;\n  }, [value, onChange, isUpdated]);\n}\n","type":"registry:file","target":"~/lib/use-on-change.ts"}],"css":{"@import \"tw-shimmer\"":{},"@layer utilities":{"@keyframes reasoning-expand":{"from":{"height":"0"},"to":{"height":"var(--radix-collapsible-content-height)"}},"@keyframes reasoning-collapse":{"from":{"height":"var(--radix-collapsible-content-height)"},"to":{"height":"0"}},".animate-collapsible-down":{"animation":"reasoning-expand 0.2s ease-out"},".animate-collapsible-up":{"animation":"reasoning-collapse 0.2s ease-out"}}},"type":"registry:ui"}