{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"tool","title":"Tool","description":"Tool call component with state-aware header and JSON input/output blocks","dependencies":["@hugeicons/react","@hugeicons/core-free-icons","@react-symbols/icons","shiki"],"registryDependencies":["badge","collapsible"],"files":[{"path":"components/nexus-ui/tool.tsx","content":"\"use client\";\n\nimport {\n  createContext,\n  useContext,\n  type ComponentProps,\n  type CSSProperties,\n} from \"react\";\nimport {\n  ArrowDown01Icon,\n  CancelCircleIcon,\n  CheckmarkCircle01Icon,\n  Clock01Icon,\n  Loading03Icon,\n  ToolsIcon,\n} from \"@hugeicons/core-free-icons\";\nimport { HugeiconsIcon, type IconSvgElement } from \"@hugeicons/react\";\n\nimport { Badge } from \"@/components/ui/badge\";\nimport {\n  Collapsible,\n  CollapsibleContent,\n  CollapsibleTrigger,\n} from \"@/components/ui/collapsible\";\nimport { cn } from \"@/lib/utils\";\n\nimport {\n  CodeBlock,\n  CodeBlockContent,\n  CodeblockShiki,\n} from \"@/components/nexus-ui/codeblock-new\";\n\ntype ToolStatus = \"pending\" | \"ready\" | \"running\" | \"completed\" | \"error\";\n\ntype ToolMeta = {\n  label: string;\n  icon: IconSvgElement;\n  color: { bg: string; fg: string };\n  iconClassName?: string;\n};\n\nconst TOOL_META: Record<ToolStatus, ToolMeta> = {\n  pending: {\n    label: \"Pending\",\n    icon: ToolsIcon,\n    color: { bg: \"var(--color-gray-100)\", fg: \"var(--color-gray-500)\" },\n  },\n  ready: {\n    label: \"Ready\",\n    icon: Clock01Icon,\n    color: { bg: \"var(--color-orange-100)\", fg: \"var(--color-orange-600)\" },\n  },\n  running: {\n    label: \"Running\",\n    icon: Loading03Icon,\n    color: { bg: \"var(--color-blue-100)\", fg: \"var(--color-blue-600)\" },\n    iconClassName: \"animate-spin\",\n  },\n  completed: {\n    label: \"Completed\",\n    icon: CheckmarkCircle01Icon,\n    color: { bg: \"var(--color-green-100)\", fg: \"var(--color-green-600)\" },\n  },\n  error: {\n    label: \"Error\",\n    icon: CancelCircleIcon,\n    color: { bg: \"var(--color-red-100)\", fg: \"var(--color-red-600)\" },\n  },\n};\n\ntype ToolContextValue = {\n  status: ToolStatus;\n  meta: ToolMeta;\n};\n\nconst ToolContext = createContext<ToolContextValue | null>(null);\n\nfunction isToolStatus(value: unknown): value is ToolStatus {\n  return (\n    typeof value === \"string\" &&\n    Object.prototype.hasOwnProperty.call(TOOL_META, value)\n  );\n}\n\nfunction useToolContext(component: string): ToolContextValue {\n  const context = useContext(ToolContext);\n  if (!context) {\n    throw new Error(`${component} must be used within <Tool>`);\n  }\n  return context;\n}\n\nfunction stringifyToolPayload(payload: unknown): string {\n  if (typeof payload === \"string\") return payload;\n  if (payload === undefined) return \"\";\n\n  try {\n    return JSON.stringify(payload, null, 2);\n  } catch {\n    return String(payload);\n  }\n}\n\ntype ToolProps = ComponentProps<typeof Collapsible> & {\n  status: ToolStatus;\n};\n\nfunction Tool({ status, className, style, ...props }: ToolProps) {\n  const resolvedStatus = isToolStatus(status) ? status : \"pending\";\n  const meta = TOOL_META[resolvedStatus];\n\n  return (\n    <ToolContext.Provider value={{ status: resolvedStatus, meta }}>\n      <Collapsible\n        data-slot=\"tool\"\n        className={cn(\n          \"not-prose w-full max-w-100 border dark:border-accent bg-card\",\n          \"data-[state=closed]:rounded-xl data-[state=open]:rounded-xl\",\n          className,\n        )}\n        style={\n          {\n            \"--tool-color\": meta.color.fg,\n            \"--tool-bg\": meta.color.bg,\n            ...style,\n          } as CSSProperties\n        }\n        {...props}\n      />\n    </ToolContext.Provider>\n  );\n}\n\ntype ToolTriggerProps = Omit<\n  ComponentProps<typeof CollapsibleTrigger>,\n  \"children\"\n> & {\n  name: string;\n};\n\nfunction ToolTrigger({ name, className, ...props }: ToolTriggerProps) {\n  const { meta } = useToolContext(\"ToolTrigger\");\n\n  return (\n    <CollapsibleTrigger\n      data-slot=\"tool-trigger\"\n      className={cn(\n        \"group flex h-10 w-full cursor-pointer items-center justify-between px-3 py-2\",\n        className,\n      )}\n      {...props}\n    >\n      <div className=\"flex items-center gap-2\">\n        <HugeiconsIcon\n          data-slot=\"tool-trigger-icon\"\n          icon={meta.icon}\n          strokeWidth={2}\n          className={cn(\"size-4 text-(--tool-color)\", meta.iconClassName)}\n        />\n        <span\n          data-slot=\"tool-trigger-name\"\n          className=\"text-sm leading-6 font-[450] text-primary\"\n        >\n          {name}\n        </span>\n        <Badge\n          data-slot=\"tool-trigger-badge\"\n          className=\"h-6 bg-(--tool-bg)/60 font-[450] text-(--tool-color) dark:bg-(--tool-color)/10 dark:text-(--tool-color)\"\n        >\n          {meta.label}\n        </Badge>\n      </div>\n\n      <HugeiconsIcon\n        data-slot=\"tool-trigger-chevron\"\n        icon={ArrowDown01Icon}\n        strokeWidth={1.75}\n        className=\"size-4 transition-transform duration-200 group-data-[state=open]:rotate-180\"\n      />\n    </CollapsibleTrigger>\n  );\n}\n\ntype ToolContentProps = ComponentProps<typeof CollapsibleContent>;\n\nfunction ToolContent({ className, ...props }: ToolContentProps) {\n  return (\n    <CollapsibleContent\n      data-slot=\"tool-content\"\n      className={cn(\n        \"flex flex-col gap-6 p-3 pt-4\",\n        \"overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\ntype ToolPartProps = {\n  kind: \"input\" | \"output\";\n  payload: unknown;\n  errorText?: string;\n};\n\nfunction ToolPart({ kind, payload, errorText }: ToolPartProps) {\n  const { status } = useToolContext(\"ToolPart\");\n  const code = stringifyToolPayload(payload);\n  const isOutputError = kind === \"output\" && status === \"error\";\n  const hasPayload = payload !== undefined && payload !== null;\n  const shouldShowCodeblock = !isOutputError || hasPayload;\n  const title = kind === \"input\" ? \"Input\" : isOutputError ? \"Error\" : \"Output\";\n\n  return (\n    <div data-slot={`tool-${kind}`} className=\"flex flex-col gap-3\">\n      <span\n        data-slot={`tool-${kind}-title`}\n        className={cn(\n          \"text-xs leading-4 font-[450] text-muted-foreground uppercase\",\n          isOutputError && \"text-destructive\",\n        )}\n      >\n        {title}\n      </span>\n      {isOutputError ? (\n        <div\n          data-slot=\"tool-output-error\"\n          className=\"rounded-xl border border-destructive/20 bg-destructive/5 px-4 py-3 text-sm leading-6 text-destructive dark:bg-destructive/10\"\n        >\n          {errorText ?? \"Tool execution failed\"}\n        </div>\n      ) : null}\n      {shouldShowCodeblock ? (\n        <CodeBlock\n          data-slot=\"tool-output-error-codeblock\"\n          className=\"rounded-lg\"\n          keepBackground\n        >\n          <CodeBlockContent>\n            <CodeblockShiki language=\"json\">{code}</CodeblockShiki>\n          </CodeBlockContent>\n        </CodeBlock>\n      ) : null}\n    </div>\n  );\n}\n\ntype ToolPayloadProps = {\n  payload: unknown;\n};\n\nfunction ToolInput({ payload }: ToolPayloadProps) {\n  return <ToolPart kind=\"input\" payload={payload} />;\n}\n\ntype ToolOutputProps = ToolPayloadProps & {\n  showWhen?: ToolStatus[];\n  errorText?: string;\n};\n\nfunction ToolOutput({\n  payload,\n  showWhen = [\"completed\"],\n  errorText,\n}: ToolOutputProps) {\n  const { status } = useToolContext(\"ToolOutput\");\n  if (!showWhen.includes(status)) return null;\n\n  return <ToolPart kind=\"output\" payload={payload} errorText={errorText} />;\n}\n\nexport type { ToolStatus };\nexport { Tool, ToolTrigger, ToolContent, ToolInput, ToolOutput };\n","type":"registry:file","target":"~/components/nexus-ui/tool.tsx"},{"path":"components/nexus-ui/codeblock-new.tsx","content":"\"use client\";\n\nimport { FileIcon } from \"@react-symbols/icons/utils\";\nimport { Copy01Icon, Tick02Icon } from \"@hugeicons/core-free-icons\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport {\n  createContext,\n  useEffect,\n  useContext,\n  useState,\n  type CSSProperties,\n  type ComponentProps,\n  type ReactNode,\n} from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { highlight, Themes } from \"@/lib/shiki/highlighter\";\nimport type { BundledLanguage } from \"shiki/bundle/web\";\n\nconst highlighterPromise = highlight();\ntype DivProps = ComponentProps<\"div\">;\ntype CodeBlockProps = DivProps & {\n  keepBackground?: boolean;\n};\ntype CodeBlockCopyContextValue = {\n  content: string;\n  setContent: (value: string) => void;\n};\nconst CodeBlockCopyContext = createContext<CodeBlockCopyContextValue | null>(\n  null,\n);\n\ninterface CodeblockClientShikiProps extends DivProps {\n  code?: string;\n  language?: string;\n  lineNumbers?: boolean;\n  children?: ReactNode;\n}\n\ntype ShikiToken = {\n  content: string;\n  htmlStyle?: Record<string, string>;\n};\n\nconst buildRawTokenRows = (input: string): ShikiToken[][] =>\n  input.split(/\\r?\\n/).map((line) => [{ content: line || \"\\u00A0\" }]);\n\nconst EMPTY_TOKEN_ROW: ShikiToken[] = [{ content: \"\\u00A0\" }];\n\nconst resolveCodeToHighlight = (children: ReactNode, code?: string): string =>\n  typeof children === \"string\"\n    ? children\n    : Array.isArray(children) &&\n        children.length === 1 &&\n        typeof children[0] === \"string\"\n      ? children[0]\n      : (code ?? \"\");\n\nconst CodeBlock = ({\n  children,\n  className,\n  keepBackground = false,\n  ...props\n}: CodeBlockProps) => {\n  const [copyContent, setCopyContent] = useState(\"\");\n\n  return (\n    <CodeBlockCopyContext.Provider\n      value={{ content: copyContent, setContent: setCopyContent }}\n    >\n      <div\n        className={cn(\n          \"not-prose\",\n          \"my-0 flex w-full flex-col overflow-hidden rounded-xl\",\n          keepBackground\n            ? \"border-none bg-secondary dark:bg-background\"\n            : \"border bg-card dark:border-accent\",\n          \"text-[13px] font-[450]\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n      </div>\n    </CodeBlockCopyContext.Provider>\n  );\n};\n\ntype CodeBlockHeaderProps = DivProps;\n\nconst CodeBlockHeader = ({\n  children,\n  className,\n  ...props\n}: CodeBlockHeaderProps) => {\n  return (\n    <div\n      className={cn(\n        \"not-prose\", // Disable Markdown Styles\n        \"flex h-9.5 items-center justify-between gap-2 px-4\",\n        \"text-fd-muted-foreground\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n};\n\ninterface CodeBlockIconProps extends DivProps {\n  language?: string;\n}\n\nconst CodeBlockIcon = ({ language, className }: CodeBlockIconProps) => {\n  return (\n    <FileIcon\n      width={16}\n      height={16}\n      fileName={`.${language ?? \"\"}`}\n      autoAssign={true}\n      className={cn(className)}\n    />\n  );\n};\n\ntype CodeBlockGroupProps = DivProps;\n\nconst CodeBlockGroup = ({\n  children,\n  className,\n  ...props\n}: CodeBlockGroupProps) => {\n  return (\n    <div\n      className={cn(\n        \"flex items-center gap-2\",\n        \"text-fd-muted-foreground\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n};\n\nconst CodeBlockContent = ({ className, children, ...props }: DivProps) => {\n  return (\n    <div\n      className={cn(\n        \"no-scrollbar max-h-96 overflow-auto overscroll-x-none\",\n        \"rounded-xl px-4 text-sm leading-6\",\n        \"font-mono whitespace-pre\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n};\n\nconst CodeblockShiki = ({\n  code,\n  language = \"tsx\",\n  lineNumbers = false,\n  className,\n  children,\n  ...props\n}: CodeblockClientShikiProps) => {\n  const setCopyContent = useContext(CodeBlockCopyContext)?.setContent;\n  const codeToHighlight = resolveCodeToHighlight(children, code);\n  const [tokenRows, setTokenRows] = useState<ShikiToken[][]>(() =>\n    buildRawTokenRows(codeToHighlight),\n  );\n\n  useEffect(() => {\n    setCopyContent?.(codeToHighlight);\n  }, [codeToHighlight, setCopyContent]);\n\n  useEffect(() => {\n    let cancelled = false;\n\n    async function clientHighlight() {\n      const rawRows = buildRawTokenRows(codeToHighlight);\n      if (!cancelled) {\n        setTokenRows(rawRows);\n      }\n\n      if (!codeToHighlight) {\n        return;\n      }\n\n      try {\n        const highlighter = await highlighterPromise;\n        const result = await highlighter.codeToTokens(codeToHighlight, {\n          lang: language as BundledLanguage,\n          themes: {\n            light: Themes.light,\n            dark: Themes.dark,\n          },\n        });\n        if (!cancelled) {\n          setTokenRows((result.tokens ?? rawRows) as ShikiToken[][]);\n        }\n      } catch {\n        if (!cancelled) {\n          setTokenRows(rawRows);\n        }\n      }\n    }\n\n    void clientHighlight();\n\n    return () => {\n      cancelled = true;\n    };\n  }, [codeToHighlight, language]);\n\n  return (\n    <div\n      className={cn(\n        \"no-scrollbar w-full overflow-auto overscroll-x-none py-0\",\n        className,\n      )}\n      {...props}\n    >\n      <pre\n        className={cn(\n          \"shiki\",\n          lineNumbers ? \"shiki-line-numbers\" : \"nd-no-line-numbers\",\n        )}\n      >\n        <code>\n          {tokenRows.map((row, rowIndex) => (\n            <span key={`row-${rowIndex}`} className=\"line\">\n              {(row.length ? row : EMPTY_TOKEN_ROW).map((token, tokenIndex) => (\n                <span\n                  key={`token-${rowIndex}-${tokenIndex}`}\n                  style={token.htmlStyle as CSSProperties | undefined}\n                >\n                  {token.content || \"\\u00A0\"}\n                </span>\n              ))}\n              {rowIndex < tokenRows.length - 1 && \"\\n\"}\n            </span>\n          ))}\n        </code>\n      </pre>\n    </div>\n  );\n};\n\ntype CodeBlockCopyButtonProps = ComponentProps<\"button\">;\n\nconst CodeBlockCopyButton = ({\n  className,\n  ...props\n}: CodeBlockCopyButtonProps) => {\n  const content = useContext(CodeBlockCopyContext)?.content ?? \"\";\n  const [isCopied, setIsCopied] = useState<boolean>(false);\n\n  const copyToClipboard = async (text: string) => {\n    try {\n      await navigator.clipboard.writeText(text);\n      return true;\n    } catch (err) {\n      console.error(\"Failed to copy text: \", err);\n      return false;\n    }\n  };\n\n  useEffect(() => {\n    if (!isCopied) return;\n\n    const timeout = setTimeout(() => {\n      setIsCopied(false);\n    }, 2000);\n    return () => clearTimeout(timeout);\n  }, [isCopied]);\n\n  const handleCopy = async () => {\n    if (!content) return;\n    await copyToClipboard(content);\n    setIsCopied(true);\n  };\n\n  return (\n    <button\n      title=\"Copy to clipboard\"\n      className={cn(\n        \"relative flex size-7 cursor-pointer items-center justify-center rounded-full text-ring hover:text-primary\",\n        \"\",\n        className,\n      )}\n      onClick={handleCopy}\n      {...props}\n    >\n      {isCopied ? (\n        <HugeiconsIcon\n          icon={Tick02Icon}\n          strokeWidth={2}\n          className=\"size-3.5 animate-in text-green-900 duration-200 zoom-in-50 dark:text-green-400\"\n        />\n      ) : (\n        <HugeiconsIcon\n          icon={Copy01Icon}\n          strokeWidth={2}\n          className=\"size-3.5 animate-in duration-200 zoom-in-50\"\n        />\n      )}\n    </button>\n  );\n};\n\nexport {\n  CodeBlock,\n  CodeBlockHeader,\n  CodeBlockIcon,\n  CodeBlockGroup,\n  CodeBlockContent,\n  CodeblockShiki,\n  CodeBlockCopyButton,\n};\n","type":"registry:file","target":"~/components/nexus-ui/codeblock-new.tsx"},{"path":"lib/shiki/highlighter.ts","content":"import { createJavaScriptRegexEngine } from \"shiki/engine/javascript\";\nimport {\n  bundledLanguages,\n  type Highlighter,\n  type RegexEngine,\n  createHighlighter,\n} from \"shiki/bundle/web\";\n\nlet jsEngine: RegexEngine | null = null;\nlet highlighter: Promise<Highlighter> | null = null;\n\n// Settings for UI components\nconst Themes = {\n  light: \"github-light\",\n  dark: \"github-dark\",\n};\n\nconst allBundledLanguageIds = Object.keys(bundledLanguages);\n\n\nconst getJsEngine = (): RegexEngine => {\n  jsEngine ??= createJavaScriptRegexEngine();\n  return jsEngine;\n};\n\nconst highlight = async (): Promise<Highlighter> => {\n  highlighter ??= createHighlighter({\n    langs: allBundledLanguageIds,\n    themes: [\"github-light\", \"github-dark\"],\n    engine: getJsEngine(),\n  });\n  return highlighter;\n};\nexport { highlight, Themes };\n","type":"registry:file","target":"~/lib/shiki/highlighter.ts"}],"type":"registry:ui"}