{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "prompt-diff",
  "title": "AI Prompt Diff",
  "description": "Compare system prompts with side-by-side or unified diff views. Shows added/removed line highlighting, line numbers, and copy buttons. Essential for prompt engineering and version tracking.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/blocks/ai/ai-prompt-diff/components/elements/ai-prompt-diff.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport {\n  AlignJustify,\n  CheckCheck,\n  Copy,\n  FileText,\n  SplitSquareHorizontal,\n} from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\ntype DiffView = \"side-by-side\" | \"unified\";\n\ninterface DiffLine {\n  type: \"unchanged\" | \"added\" | \"removed\";\n  content: string;\n  lineNumber: { before?: number; after?: number };\n}\n\ninterface AiPromptDiffContextValue {\n  before: string;\n  after: string;\n  view: DiffView;\n  setView: (view: DiffView) => void;\n  diffLines: DiffLine[];\n}\n\nconst AiPromptDiffContext =\n  React.createContext<AiPromptDiffContextValue | null>(null);\n\nfunction usePromptDiffContext() {\n  const context = React.useContext(AiPromptDiffContext);\n  if (!context) {\n    throw new Error(\n      \"AiPromptDiff components must be used within <AiPromptDiff>\",\n    );\n  }\n  return context;\n}\n\nfunction computeDiff(before: string, after: string): DiffLine[] {\n  const beforeLines = before.split(\"\\n\");\n  const afterLines = after.split(\"\\n\");\n  const result: DiffLine[] = [];\n\n  const lcs = computeLCS(beforeLines, afterLines);\n  let beforeIdx = 0;\n  let afterIdx = 0;\n  let lcsIdx = 0;\n\n  while (beforeIdx < beforeLines.length || afterIdx < afterLines.length) {\n    if (\n      lcsIdx < lcs.length &&\n      beforeIdx < beforeLines.length &&\n      afterIdx < afterLines.length &&\n      beforeLines[beforeIdx] === lcs[lcsIdx] &&\n      afterLines[afterIdx] === lcs[lcsIdx]\n    ) {\n      result.push({\n        type: \"unchanged\",\n        content: beforeLines[beforeIdx],\n        lineNumber: { before: beforeIdx + 1, after: afterIdx + 1 },\n      });\n      beforeIdx++;\n      afterIdx++;\n      lcsIdx++;\n    } else if (\n      beforeIdx < beforeLines.length &&\n      (lcsIdx >= lcs.length || beforeLines[beforeIdx] !== lcs[lcsIdx])\n    ) {\n      result.push({\n        type: \"removed\",\n        content: beforeLines[beforeIdx],\n        lineNumber: { before: beforeIdx + 1 },\n      });\n      beforeIdx++;\n    } else if (\n      afterIdx < afterLines.length &&\n      (lcsIdx >= lcs.length || afterLines[afterIdx] !== lcs[lcsIdx])\n    ) {\n      result.push({\n        type: \"added\",\n        content: afterLines[afterIdx],\n        lineNumber: { after: afterIdx + 1 },\n      });\n      afterIdx++;\n    }\n  }\n\n  return result;\n}\n\nfunction computeLCS(a: string[], b: string[]): string[] {\n  const m = a.length;\n  const n = b.length;\n  const dp: number[][] = Array.from({ length: m + 1 }, () =>\n    Array(n + 1).fill(0),\n  );\n\n  for (let i = 1; i <= m; i++) {\n    for (let j = 1; j <= n; j++) {\n      if (a[i - 1] === b[j - 1]) {\n        dp[i][j] = dp[i - 1][j - 1] + 1;\n      } else {\n        dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);\n      }\n    }\n  }\n\n  const result: string[] = [];\n  let i = m;\n  let j = n;\n  while (i > 0 && j > 0) {\n    if (a[i - 1] === b[j - 1]) {\n      result.unshift(a[i - 1]);\n      i--;\n      j--;\n    } else if (dp[i - 1][j] > dp[i][j - 1]) {\n      i--;\n    } else {\n      j--;\n    }\n  }\n\n  return result;\n}\n\ninterface AiPromptDiffProps {\n  before: string;\n  after: string;\n  title?: string;\n  view?: DiffView;\n  onViewChange?: (view: DiffView) => void;\n  className?: string;\n  children?: React.ReactNode;\n}\n\nfunction AiPromptDiff({\n  before,\n  after,\n  view: controlledView,\n  onViewChange,\n  className,\n  children,\n}: AiPromptDiffProps) {\n  const [uncontrolledView, setUncontrolledView] =\n    React.useState<DiffView>(\"unified\");\n\n  const isControlled = controlledView !== undefined;\n  const view = isControlled ? controlledView : uncontrolledView;\n\n  const setView = React.useCallback(\n    (newView: DiffView) => {\n      if (!isControlled) {\n        setUncontrolledView(newView);\n      }\n      onViewChange?.(newView);\n    },\n    [isControlled, onViewChange],\n  );\n\n  const diffLines = React.useMemo(\n    () => computeDiff(before, after),\n    [before, after],\n  );\n\n  const contextValue = React.useMemo(\n    () => ({ before, after, view, setView, diffLines }),\n    [before, after, view, setView, diffLines],\n  );\n\n  return (\n    <AiPromptDiffContext.Provider value={contextValue}>\n      <div\n        data-slot=\"ai-prompt-diff\"\n        className={cn(\n          \"rounded-lg border border-border bg-card text-card-foreground overflow-hidden\",\n          className,\n        )}\n      >\n        {children}\n      </div>\n    </AiPromptDiffContext.Provider>\n  );\n}\n\ninterface AiPromptDiffHeaderProps {\n  title?: string;\n  children?: React.ReactNode;\n  className?: string;\n}\n\nfunction AiPromptDiffHeader({\n  title = \"Prompt Diff\",\n  children,\n  className,\n}: AiPromptDiffHeaderProps) {\n  const { diffLines } = usePromptDiffContext();\n\n  const stats = React.useMemo(() => {\n    const added = diffLines.filter((l) => l.type === \"added\").length;\n    const removed = diffLines.filter((l) => l.type === \"removed\").length;\n    return { added, removed };\n  }, [diffLines]);\n\n  return (\n    <div\n      data-slot=\"ai-prompt-diff-header\"\n      className={cn(\n        \"flex items-center gap-3 px-4 py-3 border-b border-border\",\n        className,\n      )}\n    >\n      <div className=\"flex size-8 shrink-0 items-center justify-center rounded-md bg-orange-100 dark:bg-orange-950\">\n        <FileText className=\"size-4 text-orange-600 dark:text-orange-400\" />\n      </div>\n      <div className=\"flex flex-1 items-center gap-2\">\n        <span className=\"font-medium text-sm\">{title}</span>\n        <div className=\"flex items-center gap-1.5 text-xs\">\n          {stats.added > 0 && (\n            <span className=\"text-green-600 dark:text-green-400\">\n              +{stats.added}\n            </span>\n          )}\n          {stats.removed > 0 && (\n            <span className=\"text-red-600 dark:text-red-400\">\n              -{stats.removed}\n            </span>\n          )}\n        </div>\n      </div>\n      {children}\n    </div>\n  );\n}\n\ninterface AiPromptDiffViewToggleProps {\n  className?: string;\n}\n\nfunction AiPromptDiffViewToggle({ className }: AiPromptDiffViewToggleProps) {\n  const { view, setView } = usePromptDiffContext();\n\n  return (\n    <div\n      data-slot=\"ai-prompt-diff-view-toggle\"\n      className={cn(\n        \"inline-flex items-center rounded-md border border-border bg-muted/50 p-0.5\",\n        className,\n      )}\n    >\n      <button\n        type=\"button\"\n        onClick={() => setView(\"unified\")}\n        className={cn(\n          \"inline-flex items-center gap-1.5 rounded px-2 py-1 text-xs font-medium transition-colors\",\n          view === \"unified\"\n            ? \"bg-background text-foreground shadow-sm\"\n            : \"text-muted-foreground hover:text-foreground\",\n        )}\n      >\n        <AlignJustify className=\"size-3.5\" />\n        Unified\n      </button>\n      <button\n        type=\"button\"\n        onClick={() => setView(\"side-by-side\")}\n        className={cn(\n          \"inline-flex items-center gap-1.5 rounded px-2 py-1 text-xs font-medium transition-colors\",\n          view === \"side-by-side\"\n            ? \"bg-background text-foreground shadow-sm\"\n            : \"text-muted-foreground hover:text-foreground\",\n        )}\n      >\n        <SplitSquareHorizontal className=\"size-3.5\" />\n        Split\n      </button>\n    </div>\n  );\n}\n\ninterface AiPromptDiffContentProps {\n  className?: string;\n}\n\nfunction AiPromptDiffContent({ className }: AiPromptDiffContentProps) {\n  const { view, diffLines, before, after } = usePromptDiffContext();\n\n  if (view === \"side-by-side\") {\n    return (\n      <AiPromptDiffSideBySide\n        before={before}\n        after={after}\n        className={className}\n      />\n    );\n  }\n\n  return (\n    <div\n      data-slot=\"ai-prompt-diff-content\"\n      className={cn(\"overflow-x-auto\", className)}\n    >\n      <table className=\"w-full text-xs font-mono\">\n        <tbody>\n          {diffLines.map((line, idx) => (\n            <tr\n              key={idx}\n              className={cn(\n                line.type === \"added\" && \"bg-green-50 dark:bg-green-950/30\",\n                line.type === \"removed\" && \"bg-red-50 dark:bg-red-950/30\",\n              )}\n            >\n              <td className=\"w-10 select-none px-2 py-0.5 text-right text-muted-foreground border-r border-border\">\n                {line.lineNumber.before ?? \"\"}\n              </td>\n              <td className=\"w-10 select-none px-2 py-0.5 text-right text-muted-foreground border-r border-border\">\n                {line.lineNumber.after ?? \"\"}\n              </td>\n              <td className=\"w-6 select-none px-2 py-0.5 text-center\">\n                {line.type === \"added\" && (\n                  <span className=\"text-green-600 dark:text-green-400\">+</span>\n                )}\n                {line.type === \"removed\" && (\n                  <span className=\"text-red-600 dark:text-red-400\">-</span>\n                )}\n              </td>\n              <td className=\"px-3 py-0.5 whitespace-pre\">\n                <span\n                  className={cn(\n                    line.type === \"added\" &&\n                      \"text-green-700 dark:text-green-300\",\n                    line.type === \"removed\" && \"text-red-700 dark:text-red-300\",\n                  )}\n                >\n                  {line.content}\n                </span>\n              </td>\n            </tr>\n          ))}\n        </tbody>\n      </table>\n    </div>\n  );\n}\n\ninterface AiPromptDiffSideBySideProps {\n  before: string;\n  after: string;\n  className?: string;\n}\n\nfunction AiPromptDiffSideBySide({\n  before,\n  after,\n  className,\n}: AiPromptDiffSideBySideProps) {\n  const beforeLines = before.split(\"\\n\");\n  const afterLines = after.split(\"\\n\");\n  const maxLines = Math.max(beforeLines.length, afterLines.length);\n\n  return (\n    <div\n      data-slot=\"ai-prompt-diff-side-by-side\"\n      className={cn(\"grid grid-cols-2 divide-x divide-border\", className)}\n    >\n      <div className=\"overflow-x-auto\">\n        <div className=\"px-3 py-2 bg-muted/30 border-b border-border text-xs font-medium text-muted-foreground\">\n          Before\n        </div>\n        <table className=\"w-full text-xs font-mono\">\n          <tbody>\n            {Array.from({ length: maxLines }).map((_, idx) => {\n              const line = beforeLines[idx];\n              const afterLine = afterLines[idx];\n              const isRemoved = line !== afterLine && line !== undefined;\n              return (\n                <tr\n                  key={idx}\n                  className={cn(isRemoved && \"bg-red-50 dark:bg-red-950/30\")}\n                >\n                  <td className=\"w-10 select-none px-2 py-0.5 text-right text-muted-foreground border-r border-border\">\n                    {line !== undefined ? idx + 1 : \"\"}\n                  </td>\n                  <td className=\"px-3 py-0.5 whitespace-pre\">\n                    <span\n                      className={cn(\n                        isRemoved && \"text-red-700 dark:text-red-300\",\n                      )}\n                    >\n                      {line ?? \"\"}\n                    </span>\n                  </td>\n                </tr>\n              );\n            })}\n          </tbody>\n        </table>\n      </div>\n      <div className=\"overflow-x-auto\">\n        <div className=\"px-3 py-2 bg-muted/30 border-b border-border text-xs font-medium text-muted-foreground\">\n          After\n        </div>\n        <table className=\"w-full text-xs font-mono\">\n          <tbody>\n            {Array.from({ length: maxLines }).map((_, idx) => {\n              const line = afterLines[idx];\n              const beforeLine = beforeLines[idx];\n              const isAdded = line !== beforeLine && line !== undefined;\n              return (\n                <tr\n                  key={idx}\n                  className={cn(isAdded && \"bg-green-50 dark:bg-green-950/30\")}\n                >\n                  <td className=\"w-10 select-none px-2 py-0.5 text-right text-muted-foreground border-r border-border\">\n                    {line !== undefined ? idx + 1 : \"\"}\n                  </td>\n                  <td className=\"px-3 py-0.5 whitespace-pre\">\n                    <span\n                      className={cn(\n                        isAdded && \"text-green-700 dark:text-green-300\",\n                      )}\n                    >\n                      {line ?? \"\"}\n                    </span>\n                  </td>\n                </tr>\n              );\n            })}\n          </tbody>\n        </table>\n      </div>\n    </div>\n  );\n}\n\ninterface AiPromptDiffCopyButtonProps {\n  variant: \"before\" | \"after\";\n  className?: string;\n}\n\nfunction AiPromptDiffCopyButton({\n  variant,\n  className,\n}: AiPromptDiffCopyButtonProps) {\n  const { before, after } = usePromptDiffContext();\n  const [copied, setCopied] = React.useState(false);\n\n  const content = variant === \"before\" ? before : after;\n\n  const handleCopy = React.useCallback(async () => {\n    await navigator.clipboard.writeText(content);\n    setCopied(true);\n    setTimeout(() => setCopied(false), 2000);\n  }, [content]);\n\n  return (\n    <button\n      type=\"button\"\n      data-slot=\"ai-prompt-diff-copy-button\"\n      onClick={handleCopy}\n      className={cn(\n        \"inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors\",\n        className,\n      )}\n    >\n      {copied ? (\n        <>\n          <CheckCheck className=\"size-3\" />\n          Copied\n        </>\n      ) : (\n        <>\n          <Copy className=\"size-3\" />\n          Copy {variant}\n        </>\n      )}\n    </button>\n  );\n}\n\nexport {\n  AiPromptDiff,\n  AiPromptDiffHeader,\n  AiPromptDiffViewToggle,\n  AiPromptDiffContent,\n  AiPromptDiffSideBySide,\n  AiPromptDiffCopyButton,\n};\nexport type { AiPromptDiffProps, DiffView, DiffLine };\n",
      "type": "registry:component"
    }
  ],
  "docs": "A compound component for comparing prompts. Supports unified and side-by-side diff views with LCS-based diff algorithm. Use AiPromptDiff as root with AiPromptDiffHeader, AiPromptDiffViewToggle, AiPromptDiffContent, and AiPromptDiffCopyButton sub-components.",
  "categories": [
    "ai"
  ],
  "type": "registry:ui"
}