{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chat-reasoning",
  "title": "Chat Reasoning",
  "description": "The reasoning behind a reply, open while it streams and folded once the answer starts.",
  "registryDependencies": [
    "https://ui.ndekocode.dev/r/theme.json",
    "https://ui.ndekocode.dev/r/utils.json",
    "https://ui.ndekocode.dev/r/icons.json",
    "https://ui.ndekocode.dev/r/chat-markdown.json",
    "https://ui.ndekocode.dev/r/chat-motion.json"
  ],
  "files": [
    {
      "path": "registry/ndeko/ui/chat-reasoning.tsx",
      "content": "// Adapted from prompt-kit (MIT, Copyright (c) 2025 Julien Thibeaut).\n'use client';\n\nimport { createContext, useContext, useId, useState } from 'react';\n\nimport { ChevronDownIcon } from '@/registry/ndeko/lib/icons';\nimport { cn } from '@/registry/ndeko/lib/utils';\nimport { Markdown } from '@/registry/ndeko/ui/chat-markdown';\nimport { Fold } from '@/registry/ndeko/ui/chat-motion';\n\ntype ReasoningContextValue = {\n  isOpen: boolean;\n  onOpenChange: (open: boolean) => void;\n  contentId: string;\n};\n\nconst ReasoningContext = createContext<ReasoningContextValue | null>(null);\n\nfunction useReasoningContext() {\n  const context = useContext(ReasoningContext);\n  if (!context) {\n    throw new Error('useReasoningContext must be used within a Reasoning');\n  }\n  return context;\n}\n\ntype ReasoningProps = {\n  children: React.ReactNode;\n  className?: string;\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  /** Opens the reasoning while it streams and folds it once the answer starts. Only when uncontrolled. */\n  isStreaming?: boolean;\n};\n\nfunction Reasoning({\n  children,\n  className,\n  open,\n  defaultOpen = false,\n  onOpenChange,\n  isStreaming = false,\n}: ReasoningProps) {\n  const [internalOpen, setInternalOpen] = useState(defaultOpen || isStreaming);\n  const [wasStreaming, setWasStreaming] = useState(isStreaming);\n  const contentId = useId();\n  const isControlled = open !== undefined;\n  const isOpen = open ?? internalOpen;\n\n  if (wasStreaming !== isStreaming) {\n    setWasStreaming(isStreaming);\n    if (!isControlled) {\n      setInternalOpen(isStreaming);\n    }\n  }\n\n  function changeOpen(nextOpen: boolean) {\n    if (!isControlled) {\n      setInternalOpen(nextOpen);\n    }\n    onOpenChange?.(nextOpen);\n  }\n\n  const context: ReasoningContextValue = {\n    isOpen,\n    onOpenChange: changeOpen,\n    contentId,\n  };\n\n  return (\n    <ReasoningContext.Provider value={context}>\n      <div data-slot='reasoning' className={className}>\n        {children}\n      </div>\n    </ReasoningContext.Provider>\n  );\n}\n\nfunction chevronTurnOf(isOpen: boolean) {\n  if (isOpen) {\n    return 'rotate-180';\n  }\n  return 'rotate-0';\n}\n\ntype ReasoningTriggerProps = Omit<\n  React.ComponentProps<'button'>,\n  'onClick' | 'type'\n>;\n\nfunction ReasoningTrigger({\n  children,\n  className,\n  ...props\n}: ReasoningTriggerProps) {\n  const { isOpen, onOpenChange, contentId } = useReasoningContext();\n\n  function toggle() {\n    onOpenChange(!isOpen);\n  }\n\n  return (\n    <button\n      type='button'\n      data-slot='reasoning-trigger'\n      aria-expanded={isOpen}\n      aria-controls={contentId}\n      onClick={toggle}\n      className={cn(\n        'text-muted-foreground hover:text-foreground focus-visible:ring-ring/25 inline-flex cursor-pointer items-center gap-1 rounded-md text-[13px] transition-colors outline-none focus-visible:ring-[3px]',\n        className\n      )}\n      {...props}\n    >\n      {children}\n      <ChevronDownIcon\n        className={cn(\n          'size-3.5 transition-transform duration-200 ease-(--ease-out-strong)',\n          chevronTurnOf(isOpen)\n        )}\n      />\n    </button>\n  );\n}\n\ntype ReasoningContentProps = {\n  children: React.ReactNode;\n  /** Applies to string children only. */\n  markdown?: boolean;\n  className?: string;\n  contentClassName?: string;\n};\n\nfunction isMarkdownText(\n  children: React.ReactNode,\n  markdown: boolean\n): children is string {\n  const isText = typeof children === 'string';\n  return markdown && isText;\n}\n\nfunction ReasoningBody({\n  children,\n  markdown,\n}: Pick<ReasoningContentProps, 'children' | 'markdown'>) {\n  const shouldRenderMarkdown = isMarkdownText(children, Boolean(markdown));\n  if (shouldRenderMarkdown) {\n    return (\n      <Markdown className='text-muted-foreground text-[13px]'>\n        {children}\n      </Markdown>\n    );\n  }\n  return children;\n}\n\nfunction ReasoningContent({\n  children,\n  markdown = false,\n  className,\n  contentClassName,\n}: ReasoningContentProps) {\n  const { isOpen, contentId } = useReasoningContext();\n\n  return (\n    <Fold open={isOpen} id={contentId} className={className}>\n      <div\n        data-slot='reasoning-content'\n        className={cn(\n          'border-foreground/10 text-muted-foreground mt-2 border-s-2 ps-3 text-[13px] leading-relaxed',\n          contentClassName\n        )}\n      >\n        <ReasoningBody markdown={markdown}>{children}</ReasoningBody>\n      </div>\n    </Fold>\n  );\n}\n\nexport {\n  Reasoning,\n  ReasoningContent,\n  ReasoningTrigger,\n  useReasoningContext,\n  type ReasoningProps,\n};\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "primitives",
    "chat"
  ],
  "type": "registry:ui"
}