{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "file-diff",
  "title": "File Diff",
  "description": "A compact code-change surface with old and new line numbers plus live change counts.",
  "dependencies": [
    "@solar-icons/react@^2.2.0",
    "cn@^0.3.0",
    "motion@^13.4.0"
  ],
  "registryDependencies": [
    "https://pastaui.com/r/pastaui-styles.json"
  ],
  "files": [
    {
      "path": "src/components/ai/file-diff.tsx",
      "content": "import { AltArrowRightIcon as ChevronRightIcon } from '@solar-icons/react/linear/alt-arrow-right'\nimport { CheckCircleIcon as CheckIcon } from '@solar-icons/react/linear/check-circle'\nimport { CopyIcon } from '@solar-icons/react/linear/copy'\nimport { FileCodeIcon as FileCode2Icon } from '@solar-icons/react/linear/file-code'\nimport { SortVerticalIcon as ExpandIcon } from '@solar-icons/react/linear/sort-vertical'\nimport { AnimatePresence, MotionConfig, motion } from 'motion/react'\nimport * as React from 'react'\n\nimport { cn } from 'cn'\n\ninterface DiffLine {\n  content: string\n  newNumber?: number\n  oldNumber?: number\n  type: 'addition' | 'context' | 'deletion'\n}\n\ninterface DiffHunk {\n  header?: string\n  hidden?: DiffLine[]\n  id: string\n  lines: DiffLine[]\n}\n\ninterface FileDiffProps extends Omit<React.ComponentProps<'figure'>, 'onCopy'> {\n  additions?: number\n  defaultOpen?: boolean\n  deletions?: number\n  filename: string\n  hunks?: DiffHunk[]\n  lines?: DiffLine[]\n  onCopy?: (patch: string) => void\n  onViewedChange?: (viewed: boolean) => void\n  viewed?: boolean\n}\n\nconst ease = [0.23, 1, 0.32, 1] as const\nconst sign = { addition: '+', context: ' ', deletion: '−' }\n\nfunction Row({ line }: { line: DiffLine }) {\n  return (\n    <span\n      className={cn(\n        'grid min-w-max grid-cols-[2rem_2rem_1rem_1fr] px-2 transition-colors duration-100 hover:brightness-95 dark:hover:brightness-125',\n        line.type === 'addition' && 'bg-emerald-500/10',\n        line.type === 'deletion' && 'bg-red-500/10',\n      )}\n    >\n      <span className=\"text-right text-muted-foreground/60 select-none\">\n        {line.oldNumber ?? ''}\n      </span>\n      <span className=\"text-right text-muted-foreground/60 select-none\">\n        {line.newNumber ?? ''}\n      </span>\n      <span className=\"text-center text-muted-foreground select-none\">{sign[line.type]}</span>\n      <span className=\"pr-4 text-foreground\">{line.content || ' '}</span>\n    </span>\n  )\n}\n\nfunction FileDiff({\n  additions,\n  className,\n  defaultOpen = true,\n  deletions,\n  filename,\n  hunks,\n  lines = [],\n  onCopy,\n  onViewedChange,\n  viewed,\n  ...props\n}: FileDiffProps) {\n  const bodyId = React.useId()\n  const groups = hunks ?? [{ id: 'all', lines }]\n  const all = groups.flatMap((hunk) => hunk.lines)\n  const added = additions ?? all.filter((line) => line.type === 'addition').length\n  const removed = deletions ?? all.filter((line) => line.type === 'deletion').length\n  const [internalViewed, setInternalViewed] = React.useState(false)\n  const isViewed = viewed ?? internalViewed\n  const [open, setOpen] = React.useState(defaultOpen && !isViewed)\n  const [expanded, setExpanded] = React.useState<Set<string>>(() => new Set())\n  const [copied, setCopied] = React.useState(false)\n\n  React.useEffect(() => {\n    if (!copied) return\n    const timer = setTimeout(() => setCopied(false), 1400)\n    return () => clearTimeout(timer)\n  }, [copied])\n\n  function toggleViewed() {\n    const next = !isViewed\n    if (viewed === undefined) setInternalViewed(next)\n    if (next) setOpen(false)\n    onViewedChange?.(next)\n  }\n\n  function copy() {\n    const patch = all\n      .map((line) => `${sign[line.type] === '−' ? '-' : sign[line.type]}${line.content}`)\n      .join('\\n')\n    void navigator.clipboard?.writeText(patch).catch(() => undefined)\n    setCopied(true)\n    onCopy?.(patch)\n  }\n\n  const iconButton =\n    'flex size-6 items-center justify-center rounded-md text-muted-foreground outline-none transition-colors hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring/40'\n\n  return (\n    <MotionConfig reducedMotion=\"user\">\n      <figure\n        data-slot=\"file-diff\"\n        data-viewed={isViewed}\n        className={cn(\n          'overflow-hidden rounded-[16px] bg-card font-sans text-sm font-[450] tracking-[-0.05px] text-muted-foreground shadow-card',\n          className,\n        )}\n        {...props}\n      >\n        <figcaption className=\"flex h-10 items-center gap-2 pr-2 pl-1.5 metadata-label\">\n          <button\n            aria-controls={bodyId}\n            aria-expanded={open}\n            aria-label={open ? `Collapse ${filename}` : `Expand ${filename}`}\n            className={iconButton}\n            onClick={() => setOpen((value) => !value)}\n            type=\"button\"\n          >\n            <ChevronRightIcon\n              className={cn(\n                'size-3.5 transition-[rotate] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] motion-reduce:transition-none',\n                open && 'rotate-90',\n              )}\n            />\n          </button>\n          {/\\.tsx?$/i.test(filename) ? (\n            <TypeScriptIcon aria-hidden=\"true\" className=\"size-4 shrink-0 text-[#3178c6]\" />\n          ) : (\n            <FileCode2Icon aria-hidden=\"true\" className=\"size-4 shrink-0\" />\n          )}\n          <span className={cn('min-w-0 truncate transition-opacity', isViewed && 'opacity-60')}>\n            {filename}\n          </span>\n          <span className=\"ml-auto text-emerald-600 tabular-nums\">+{added}</span>\n          <span className=\"text-red-500 tabular-nums\">−{removed}</span>\n          <button\n            aria-label={copied ? 'Copied' : 'Copy patch'}\n            className={iconButton}\n            onClick={copy}\n            type=\"button\"\n          >\n            <span className=\"grid *:col-start-1 *:row-start-1 *:transition-[opacity,scale] *:duration-150 motion-reduce:*:transition-opacity\">\n              <CopyIcon className={cn('size-3.5', copied && 'scale-75 opacity-0')} />\n              <CheckIcon className={cn('size-3.5', !copied && 'scale-75 opacity-0')} />\n            </span>\n          </button>\n          <label className=\"flex cursor-pointer items-center gap-1.5 rounded-md px-1.5 py-0.5 transition-colors hover:bg-muted\">\n            <input\n              checked={isViewed}\n              className=\"size-3.5 accent-current\"\n              onChange={toggleViewed}\n              type=\"checkbox\"\n            />\n            Viewed\n          </label>\n        </figcaption>\n        <AnimatePresence initial={false}>\n          {open && (\n            <motion.div\n              animate={{ height: 'auto', opacity: 1 }}\n              className=\"overflow-hidden\"\n              exit={{ height: 0, opacity: 0 }}\n              id={bodyId}\n              initial={{ height: 0, opacity: 0 }}\n              transition={{ duration: 0.22, ease }}\n            >\n              <pre className=\"overflow-x-auto border-t border-foreground/8 py-2 text-[12px] leading-5\">\n                <code className=\"block\">\n                  {groups.map((hunk) => {\n                    const showHidden = expanded.has(hunk.id)\n                    return (\n                      <React.Fragment key={hunk.id}>\n                        {hunk.header && (\n                          <span className=\"flex min-w-max items-center gap-2 px-2 text-muted-foreground/60\">\n                            {hunk.hidden?.length ? (\n                              <button\n                                aria-expanded={showHidden}\n                                className=\"-ml-0.5 flex items-center gap-1.5 rounded-md px-1 text-muted-foreground transition-colors outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring/40\"\n                                onClick={() =>\n                                  setExpanded((current) => {\n                                    const next = new Set(current)\n                                    if (next.has(hunk.id)) next.delete(hunk.id)\n                                    else next.add(hunk.id)\n                                    return next\n                                  })\n                                }\n                                type=\"button\"\n                              >\n                                <ExpandIcon aria-hidden=\"true\" className=\"size-3.5\" />\n                                {showHidden ? 'Hide' : 'Show'} {hunk.hidden.length} unchanged lines\n                              </button>\n                            ) : null}\n                            <span className=\"select-none\">{hunk.header}</span>\n                          </span>\n                        )}\n                        <AnimatePresence initial={false}>\n                          {showHidden && hunk.hidden && (\n                            <motion.span\n                              animate={{ height: 'auto', opacity: 1 }}\n                              className=\"block overflow-hidden\"\n                              exit={{ height: 0, opacity: 0 }}\n                              initial={{ height: 0, opacity: 0 }}\n                              transition={{ duration: 0.2, ease }}\n                            >\n                              {hunk.hidden.map((line, index) => (\n                                <Row key={`hidden-${index}-${line.content}`} line={line} />\n                              ))}\n                            </motion.span>\n                          )}\n                        </AnimatePresence>\n                        {hunk.lines.map((line, index) => (\n                          <Row key={`${line.type}-${index}-${line.content}`} line={line} />\n                        ))}\n                      </React.Fragment>\n                    )\n                  })}\n                </code>\n              </pre>\n            </motion.div>\n          )}\n        </AnimatePresence>\n      </figure>\n    </MotionConfig>\n  )\n}\n\nfunction TypeScriptIcon(props: React.ComponentProps<'svg'>) {\n  return (\n    <svg fill=\"currentColor\" height=\"1em\" viewBox=\"0 0 24 24\" width=\"1em\" {...props}>\n      <path d=\"M1.125 0C.502 0 0 .502 0 1.125v21.75C0 23.498.502 24 1.125 24h21.75c.623 0 1.125-.502 1.125-1.125V1.125C24 .502 23.498 0 22.875 0zm17.363 9.75q.918 0 1.627.111a6.4 6.4 0 0 1 1.306.34v2.458a4 4 0 0 0-.643-.361a5 5 0 0 0-.717-.26a5.5 5.5 0 0 0-1.426-.2q-.45 0-.819.086a2.1 2.1 0 0 0-.623.242q-.254.156-.393.374a.9.9 0 0 0-.14.49q0 .294.156.529q.156.234.443.444c.287.21.423.276.696.41q.41.203.926.416q.705.296 1.266.628q.561.333.963.753q.402.418.614.957q.213.538.214 1.253q0 .986-.373 1.656a3 3 0 0 1-1.012 1.085a4.4 4.4 0 0 1-1.487.596q-.85.18-1.79.18a10 10 0 0 1-1.84-.164a5.5 5.5 0 0 1-1.512-.493v-2.63a5.03 5.03 0 0 0 3.237 1.2q.5 0 .872-.09q.373-.09.623-.25q.249-.162.373-.38a1.02 1.02 0 0 0-.074-1.089a2.1 2.1 0 0 0-.537-.5a5.6 5.6 0 0 0-.807-.444a28 28 0 0 0-1.007-.436q-1.377-.575-2.053-1.405t-.676-2.005q0-.92.369-1.582q.368-.662 1.004-1.089a4.5 4.5 0 0 1 1.47-.629a7.5 7.5 0 0 1 1.77-.201m-15.113.188h9.563v2.166H9.506v9.646H6.789v-9.646H3.375z\" />\n    </svg>\n  )\n}\n\nexport { FileDiff }\nexport type { DiffHunk, DiffLine, FileDiffProps }\n",
      "type": "registry:component",
      "target": "@components/ai/file-diff.tsx"
    }
  ],
  "type": "registry:component"
}