{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ai-stack-trace",
  "title": "AI: Stack Trace",
  "description": "A collapsible error stack with parsed file locations, frames and copy support.",
  "dependencies": [
    "@untitledui/icons@^0.0.23"
  ],
  "registryDependencies": [
    "@nx-ui/utils",
    "@nx-ui/button",
    "@nx-ui/collapsible",
    "@nx-ui/ai-use-controllable-state"
  ],
  "files": [
    {
      "path": "components/ai/stack-trace.tsx",
      "content": "// Ported from vercel/ai-elements@6a9d5b1822ffb10bba4bd97175f01edd7d8651cd.\n\"use client\";\n\nimport { useControllableState } from \"./use-controllable-state\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Collapsible,\n  CollapsibleContent,\n  CollapsibleTrigger,\n} from \"@/components/ui/collapsible\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  AlertTriangle as AlertTriangleIcon,\n  Check as CheckIcon,\n  ChevronDown as ChevronDownIcon,\n  Copy01 as CopyIcon,\n} from \"@untitledui/icons\";\nimport type { ComponentProps } from \"react\";\nimport {\n  createContext,\n  memo,\n  useCallback,\n  useContext,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\n\n// Regex patterns for parsing stack traces\nconst STACK_FRAME_WITH_PARENS_REGEX = /^at\\s+(.+?)\\s+\\((.+):(\\d+):(\\d+)\\)$/;\nconst STACK_FRAME_WITHOUT_FN_REGEX = /^at\\s+(.+):(\\d+):(\\d+)$/;\nconst ERROR_TYPE_REGEX = /^(\\w+Error|Error):\\s*(.*)$/;\nconst AT_PREFIX_REGEX = /^at\\s+/;\n\ninterface StackFrame {\n  raw: string;\n  functionName: string | null;\n  filePath: string | null;\n  lineNumber: number | null;\n  columnNumber: number | null;\n  isInternal: boolean;\n}\n\ninterface ParsedStackTrace {\n  errorType: string | null;\n  errorMessage: string;\n  frames: StackFrame[];\n  raw: string;\n}\n\ninterface StackTraceContextValue {\n  trace: ParsedStackTrace;\n  raw: string;\n  isOpen: boolean;\n  setIsOpen: (open: boolean) => void;\n  onFilePathClick?: (filePath: string, line?: number, column?: number) => void;\n}\n\nconst StackTraceContext = createContext<StackTraceContextValue | null>(null);\n\nconst useStackTrace = () => {\n  const context = useContext(StackTraceContext);\n  if (!context) {\n    throw new Error(\"StackTrace components must be used within StackTrace\");\n  }\n  return context;\n};\n\nconst parseStackFrame = (line: string): StackFrame => {\n  const trimmed = line.trim();\n\n  // Pattern: at functionName (filePath:line:column)\n  const withParensMatch = trimmed.match(STACK_FRAME_WITH_PARENS_REGEX);\n  if (withParensMatch) {\n    const [, functionName, filePath, lineNum, colNum] = withParensMatch;\n    const path = filePath ?? \"\";\n    const isInternal =\n      path.includes(\"node_modules\") ||\n      path.startsWith(\"node:\") ||\n      path.includes(\"internal/\");\n    return {\n      columnNumber: colNum ? Number.parseInt(colNum, 10) : null,\n      filePath: path || null,\n      functionName: functionName ?? null,\n      isInternal,\n      lineNumber: lineNum ? Number.parseInt(lineNum, 10) : null,\n      raw: trimmed,\n    };\n  }\n\n  // Pattern: at filePath:line:column (no function name)\n  const withoutFnMatch = trimmed.match(STACK_FRAME_WITHOUT_FN_REGEX);\n  if (withoutFnMatch) {\n    const [, filePath, lineNum, colNum] = withoutFnMatch;\n    const isInternal =\n      (filePath?.includes(\"node_modules\") ?? false) ||\n      (filePath?.startsWith(\"node:\") ?? false) ||\n      (filePath?.includes(\"internal/\") ?? false);\n    return {\n      columnNumber: colNum ? Number.parseInt(colNum, 10) : null,\n      filePath: filePath ?? null,\n      functionName: null,\n      isInternal,\n      lineNumber: lineNum ? Number.parseInt(lineNum, 10) : null,\n      raw: trimmed,\n    };\n  }\n\n  // Fallback: unparseable line\n  return {\n    columnNumber: null,\n    filePath: null,\n    functionName: null,\n    isInternal: trimmed.includes(\"node_modules\") || trimmed.includes(\"node:\"),\n    lineNumber: null,\n    raw: trimmed,\n  };\n};\n\nconst parseStackTrace = (trace: string): ParsedStackTrace => {\n  const lines = trace.split(\"\\n\").filter((line) => line.trim());\n\n  if (lines.length === 0) {\n    return {\n      errorMessage: trace,\n      errorType: null,\n      frames: [],\n      raw: trace,\n    };\n  }\n\n  const firstLine = (lines[0] ?? \"\").trim();\n  let errorType: string | null = null;\n  let errorMessage = firstLine;\n\n  // Try to extract error type from \"ErrorType: message\" format\n  const errorMatch = firstLine.match(ERROR_TYPE_REGEX);\n  if (errorMatch) {\n    const [, type, msg] = errorMatch;\n    errorType = type ?? null;\n    errorMessage = msg || \"\";\n  }\n\n  // Parse stack frames (lines starting with \"at\")\n  const frames = lines\n    .slice(1)\n    .filter((line) => line.trim().startsWith(\"at \"))\n    .map(parseStackFrame);\n\n  return {\n    errorMessage,\n    errorType,\n    frames,\n    raw: trace,\n  };\n};\n\nexport type StackTraceProps = ComponentProps<\"div\"> & {\n  trace: string;\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  onFilePathClick?: (filePath: string, line?: number, column?: number) => void;\n};\n\nexport const StackTrace = memo(\n  ({\n    trace,\n    className,\n    open,\n    defaultOpen = false,\n    onOpenChange,\n    onFilePathClick,\n    children,\n    ...props\n  }: StackTraceProps) => {\n    const [isOpen, setIsOpen] = useControllableState({\n      defaultProp: defaultOpen,\n      onChange: onOpenChange,\n      prop: open,\n    });\n\n    const parsedTrace = useMemo(() => parseStackTrace(trace), [trace]);\n\n    const contextValue = useMemo(\n      () => ({\n        isOpen: isOpen ?? false,\n        onFilePathClick,\n        raw: trace,\n        setIsOpen,\n        trace: parsedTrace,\n      }),\n      [parsedTrace, trace, isOpen, setIsOpen, onFilePathClick]\n    );\n\n    return (\n      <StackTraceContext.Provider value={contextValue}>\n        <div\n          className={cn(\n            \"not-prose w-full overflow-hidden rounded-lg border bg-background font-mono text-sm\",\n            className\n          )}\n          {...props}\n        >\n          {children}\n        </div>\n      </StackTraceContext.Provider>\n    );\n  }\n);\n\nexport type StackTraceHeaderProps = ComponentProps<typeof CollapsibleTrigger>;\n\nexport const StackTraceHeader = memo(\n  ({ className, children, ...props }: StackTraceHeaderProps) => {\n    const { isOpen, setIsOpen } = useStackTrace();\n\n    return (\n      <Collapsible onOpenChange={setIsOpen} open={isOpen}>\n        <CollapsibleTrigger\n          {...props}\n          render={<button type=\"button\" />}\n          className={cn(\n              \"flex w-full cursor-pointer items-center gap-3 p-3 text-left transition-colors hover:bg-muted/50\",\n              className\n            )}\n        >\n            {children}\n        </CollapsibleTrigger>\n      </Collapsible>\n    );\n  }\n);\n\nexport type StackTraceErrorProps = ComponentProps<\"div\">;\n\nexport const StackTraceError = memo(\n  ({ className, children, ...props }: StackTraceErrorProps) => (\n    <div\n      className={cn(\n        \"flex flex-1 items-center gap-2 overflow-hidden\",\n        className\n      )}\n      {...props}\n    >\n      <AlertTriangleIcon className=\"size-4 shrink-0 text-destructive\" />\n      {children}\n    </div>\n  )\n);\n\nexport type StackTraceErrorTypeProps = ComponentProps<\"span\">;\n\nexport const StackTraceErrorType = memo(\n  ({ className, children, ...props }: StackTraceErrorTypeProps) => {\n    const { trace } = useStackTrace();\n\n    return (\n      <span\n        className={cn(\"shrink-0 font-semibold text-destructive\", className)}\n        {...props}\n      >\n        {children ?? trace.errorType}\n      </span>\n    );\n  }\n);\n\nexport type StackTraceErrorMessageProps = ComponentProps<\"span\">;\n\nexport const StackTraceErrorMessage = memo(\n  ({ className, children, ...props }: StackTraceErrorMessageProps) => {\n    const { trace } = useStackTrace();\n\n    return (\n      <span className={cn(\"truncate text-foreground\", className)} {...props}>\n        {children ?? trace.errorMessage}\n      </span>\n    );\n  }\n);\n\nexport type StackTraceActionsProps = ComponentProps<\"div\">;\n\nconst handleActionsClick = (e: React.MouseEvent) => e.stopPropagation();\nconst handleActionsKeyDown = (e: React.KeyboardEvent) => {\n  if (e.key === \"Enter\" || e.key === \" \") {\n    e.stopPropagation();\n  }\n};\n\nexport const StackTraceActions = memo(\n  ({ className, children, ...props }: StackTraceActionsProps) => (\n    <div\n      className={cn(\"flex shrink-0 items-center gap-1\", className)}\n      onClick={handleActionsClick}\n      onKeyDown={handleActionsKeyDown}\n      role=\"group\"\n      {...props}\n    >\n      {children}\n    </div>\n  )\n);\n\nexport type StackTraceCopyButtonProps = ComponentProps<typeof Button> & {\n  onCopy?: () => void;\n  onError?: (error: Error) => void;\n  timeout?: number;\n};\n\nexport const StackTraceCopyButton = memo(\n  ({\n    onCopy,\n    onError,\n    timeout = 2000,\n    className,\n    children,\n    \"aria-label\": ariaLabel,\n    ...props\n  }: StackTraceCopyButtonProps) => {\n    const [isCopied, setIsCopied] = useState(false);\n    const timeoutRef = useRef<number>(0);\n    const { raw } = useStackTrace();\n\n    const copyToClipboard = useCallback(async () => {\n      if (typeof window === \"undefined\" || !navigator?.clipboard?.writeText) {\n        onError?.(new Error(\"Clipboard API not available\"));\n        return;\n      }\n\n      try {\n        await navigator.clipboard.writeText(raw);\n        setIsCopied(true);\n        onCopy?.();\n        timeoutRef.current = window.setTimeout(\n          () => setIsCopied(false),\n          timeout\n        );\n      } catch (error) {\n        onError?.(error as Error);\n      }\n    }, [raw, onCopy, onError, timeout]);\n\n    useEffect(\n      () => () => {\n        window.clearTimeout(timeoutRef.current);\n      },\n      []\n    );\n\n    const Icon = isCopied ? CheckIcon : CopyIcon;\n\n    return (\n      <Button\n        className={cn(\"size-7\", className)}\n        onClick={copyToClipboard}\n        size=\"icon\"\n        variant=\"ghost\"\n        {...props}\n        aria-label={ariaLabel ?? \"Copy stack trace\"}\n      >\n        {children ?? <Icon size={14} />}\n      </Button>\n    );\n  }\n);\n\nexport type StackTraceExpandButtonProps = ComponentProps<\"div\">;\n\nexport const StackTraceExpandButton = memo(\n  ({ className, ...props }: StackTraceExpandButtonProps) => {\n    const { isOpen } = useStackTrace();\n\n    return (\n      <div\n        className={cn(\"flex size-7 items-center justify-center\", className)}\n        {...props}\n      >\n        <ChevronDownIcon\n          className={cn(\n            \"size-4 text-muted-foreground transition-transform\",\n            isOpen ? \"rotate-180\" : \"rotate-0\"\n          )}\n        />\n      </div>\n    );\n  }\n);\n\nexport type StackTraceContentProps = ComponentProps<\n  typeof CollapsibleContent\n> & {\n  maxHeight?: number;\n};\n\nexport const StackTraceContent = memo(\n  ({\n    className,\n    maxHeight = 400,\n    children,\n    ...props\n  }: StackTraceContentProps) => {\n    const { isOpen } = useStackTrace();\n\n    return (\n      <Collapsible open={isOpen}>\n        <CollapsibleContent\n          className={cn(\n            \"overflow-auto border-t bg-muted/30\",\n            \"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:animate-out data-[state=open]:animate-in\",\n            className\n          )}\n          style={{ maxHeight }}\n          {...props}\n        >\n          {children}\n        </CollapsibleContent>\n      </Collapsible>\n    );\n  }\n);\n\nexport type StackTraceFramesProps = ComponentProps<\"div\"> & {\n  showInternalFrames?: boolean;\n};\n\ninterface FilePathButtonProps {\n  frame: StackFrame;\n  onFilePathClick?: (\n    filePath: string,\n    lineNumber?: number,\n    columnNumber?: number\n  ) => void;\n}\n\nconst FilePathButton = memo(\n  ({ frame, onFilePathClick }: FilePathButtonProps) => {\n    const handleClick = useCallback(() => {\n      if (frame.filePath) {\n        onFilePathClick?.(\n          frame.filePath,\n          frame.lineNumber ?? undefined,\n          frame.columnNumber ?? undefined\n        );\n      }\n    }, [frame, onFilePathClick]);\n\n    return (\n      <button\n        className={cn(\n          \"underline decoration-dotted hover:text-primary\",\n          onFilePathClick && \"cursor-pointer\"\n        )}\n        disabled={!onFilePathClick}\n        onClick={handleClick}\n        type=\"button\"\n        aria-label={\n          frame.filePath ? `Open ${frame.filePath}` : \"File path unavailable\"\n        }\n      >\n        {frame.filePath}\n        {frame.lineNumber !== null && `:${frame.lineNumber}`}\n        {frame.columnNumber !== null && `:${frame.columnNumber}`}\n      </button>\n    );\n  }\n);\n\nFilePathButton.displayName = \"FilePathButton\";\n\nexport const StackTraceFrames = memo(\n  ({\n    className,\n    showInternalFrames = true,\n    ...props\n  }: StackTraceFramesProps) => {\n    const { trace, onFilePathClick } = useStackTrace();\n\n    const framesToShow = showInternalFrames\n      ? trace.frames\n      : trace.frames.filter((f) => !f.isInternal);\n\n    return (\n      <div className={cn(\"space-y-1 p-3\", className)} {...props}>\n        {framesToShow.map((frame) => (\n          <div\n            className={cn(\n              \"text-xs\",\n              frame.isInternal\n                ? \"text-muted-foreground/50\"\n                : \"text-foreground/90\"\n            )}\n            key={frame.raw}\n          >\n            <span className=\"text-muted-foreground\">at </span>\n            {frame.functionName && (\n              <span className={frame.isInternal ? \"\" : \"text-foreground\"}>\n                {frame.functionName}{\" \"}\n              </span>\n            )}\n            {frame.filePath && (\n              <>\n                <span className=\"text-muted-foreground\">(</span>\n                <FilePathButton\n                  frame={frame}\n                  onFilePathClick={onFilePathClick}\n                />\n                <span className=\"text-muted-foreground\">)</span>\n              </>\n            )}\n            {!(frame.filePath || frame.functionName) && (\n              <span>{frame.raw.replace(AT_PREFIX_REGEX, \"\")}</span>\n            )}\n          </div>\n        ))}\n        {framesToShow.length === 0 && (\n          <div className=\"text-muted-foreground text-xs\">No stack frames</div>\n        )}\n      </div>\n    );\n  }\n);\n\nStackTrace.displayName = \"StackTrace\";\nStackTraceHeader.displayName = \"StackTraceHeader\";\nStackTraceError.displayName = \"StackTraceError\";\nStackTraceErrorType.displayName = \"StackTraceErrorType\";\nStackTraceErrorMessage.displayName = \"StackTraceErrorMessage\";\nStackTraceActions.displayName = \"StackTraceActions\";\nStackTraceCopyButton.displayName = \"StackTraceCopyButton\";\nStackTraceExpandButton.displayName = \"StackTraceExpandButton\";\nStackTraceContent.displayName = \"StackTraceContent\";\nStackTraceFrames.displayName = \"StackTraceFrames\";\n",
      "type": "registry:ui",
      "target": "components/ai/stack-trace.tsx"
    }
  ],
  "type": "registry:ui"
}