{
  "name": "ai-diff-review",
  "type": "registry:component",
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "files": [
    {
      "type": "registry:component",
      "content": "\"use client\";\n\n/**\n * @author: @kokonutui\n * @description: AI Diff Review\n * @version: 1.1.0\n * @date: 2026-10-21\n * @license: MIT\n * @website: https://kokonutui.com\n * @github: https://github.com/kokonut-labs/kokonutui\n */\n\nimport { Check, ChevronRight, Undo2, X } from \"lucide-react\";\nimport {\n  AnimatePresence,\n  MotionConfig,\n  motion,\n  useReducedMotion,\n} from \"motion/react\";\nimport { type ReactNode, useEffect, useId, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface DiffLine {\n  type: \"add\" | \"remove\" | \"context\";\n  text: string;\n}\n\nexport interface DiffHunk {\n  /** First line number of the hunk in the new file. */\n  start: number;\n  lines: DiffLine[];\n}\n\nexport type FileDecision = \"pending\" | \"accepted\" | \"rejected\";\n\nexport interface FileChange {\n  path: string;\n  hunks: DiffHunk[];\n  /** True for a file the agent created. */\n  created?: boolean;\n}\n\ninterface AIDiffReviewProps {\n  /**\n   * The agent's edits. When omitted, a demo edit streams in, file by file,\n   * then waits for review.\n   */\n  files?: FileChange[];\n  /** Called when a file is accepted, rejected or put back to pending. */\n  onDecision?: (path: string, decision: FileDecision) => void;\n  className?: string;\n}\n\nconst EASE_OUT = [0.23, 1, 0.32, 1] as const;\nconst DEMO_LINE_MS = 90;\n/** Pause before the demo starts writing, so a replay clears first. */\nconst DEMO_START_MS = 320;\n/** Heights retarget on every streamed line, so they follow a spring. */\nconst HEIGHT_SPRING = { type: \"spring\", duration: 0.32, bounce: 0 } as const;\nconst ROW_HEIGHT = 40;\nconst SWAP_IN = { duration: 0.18, ease: EASE_OUT } as const;\nconst SWAP_OUT = { duration: 0.12, ease: EASE_OUT } as const;\n\nconst DEMO_FILES: FileChange[] = [\n  {\n    path: \"lib/validate.ts\",\n    created: true,\n    hunks: [\n      {\n        start: 1,\n        lines: [\n          {\n            type: \"add\",\n            text: \"const POSTCODE = /^[A-Z]{1,2}\\\\d[A-Z\\\\d]? ?\\\\d[A-Z]{2}$/i;\",\n          },\n          { type: \"add\", text: \"\" },\n          { type: \"add\", text: \"export function isPostcode(value: string) {\" },\n          { type: \"add\", text: \"  return POSTCODE.test(value.trim());\" },\n          { type: \"add\", text: \"}\" },\n        ],\n      },\n    ],\n  },\n  {\n    path: \"app/checkout/form.tsx\",\n    hunks: [\n      {\n        start: 18,\n        lines: [\n          { type: \"context\", text: \"  const onSubmit = (data: Address) => {\" },\n          { type: \"remove\", text: \"    if (!data.postcode) {\" },\n          { type: \"remove\", text: '      return setError(\"Required\");' },\n          { type: \"add\", text: \"    if (!isPostcode(data.postcode)) {\" },\n          {\n            type: \"add\",\n            text: '      return setError(\"Enter a valid postcode\");',\n          },\n          { type: \"context\", text: \"    }\" },\n          { type: \"context\", text: \"    submit(data);\" },\n        ],\n      },\n    ],\n  },\n  {\n    path: \"app/checkout/form.test.ts\",\n    hunks: [\n      {\n        start: 31,\n        lines: [\n          {\n            type: \"context\",\n            text: '  it(\"rejects an empty postcode\", () => {',\n          },\n          {\n            type: \"remove\",\n            text: '    expect(errorFor(\"\")).toBe(\"Required\");',\n          },\n          {\n            type: \"add\",\n            text: '    expect(errorFor(\"\")).toBe(\"Enter a valid postcode\");',\n          },\n          { type: \"add\", text: \"  });\" },\n          { type: \"add\", text: \"\" },\n          { type: \"add\", text: '  it(\"accepts a spaced postcode\", () => {' },\n          { type: \"add\", text: '    expect(errorFor(\"SW1A 1AA\")).toBeNull();' },\n          { type: \"context\", text: \"  });\" },\n        ],\n      },\n    ],\n  },\n];\n\nconst countLines = (hunks: DiffHunk[]) => {\n  let added = 0;\n  let removed = 0;\n  for (const hunk of hunks) {\n    for (const line of hunk.lines) {\n      if (line.type === \"add\") {\n        added += 1;\n      } else if (line.type === \"remove\") {\n        removed += 1;\n      }\n    }\n  }\n  return { added, removed };\n};\n\n/** Cuts the files down to their first `shown` lines, in order. */\nconst takeLines = (files: FileChange[], shown: number) => {\n  let left = shown;\n  const out: FileChange[] = [];\n  for (const file of files) {\n    if (left <= 0) {\n      break;\n    }\n    const hunks: DiffHunk[] = [];\n    for (const hunk of file.hunks) {\n      if (left <= 0) {\n        break;\n      }\n      hunks.push({ ...hunk, lines: hunk.lines.slice(0, left) });\n      left -= hunk.lines.length;\n    }\n    out.push({ ...file, hunks });\n  }\n  return out;\n};\n\nconst TOTAL_DEMO_LINES = DEMO_FILES.reduce(\n  (sum, file) =>\n    sum + file.hunks.reduce((lines, hunk) => lines + hunk.lines.length, 0),\n  0\n);\n\n/** Streams the demo edit in a line at a time, pausing offscreen. */\nfunction useDemoStream(\n  enabled: boolean,\n  rootRef: React.RefObject<HTMLElement | null>\n) {\n  const [shown, setShown] = useState(0);\n  const [visible, setVisible] = useState(false);\n  const [run, setRun] = useState(0);\n\n  useEffect(() => {\n    const element = rootRef.current;\n    if (!(enabled && element)) {\n      return;\n    }\n    const observer = new IntersectionObserver(([entry]) =>\n      setVisible(entry.isIntersecting)\n    );\n    observer.observe(element);\n    return () => observer.disconnect();\n  }, [enabled, rootRef]);\n\n  const writing = enabled && shown < TOTAL_DEMO_LINES;\n\n  useEffect(() => {\n    if (!(writing && visible)) {\n      return;\n    }\n    const id = setTimeout(\n      () => setShown((value) => Math.min(TOTAL_DEMO_LINES, value + 1)),\n      shown === 0 ? DEMO_START_MS : DEMO_LINE_MS\n    );\n    return () => clearTimeout(id);\n  }, [writing, visible, shown]);\n\n  return {\n    files: takeLines(DEMO_FILES, shown),\n    writing,\n    run,\n    replay: () => {\n      setShown(0);\n      setRun((value) => value + 1);\n    },\n  };\n}\n\nconst blockColor = (index: number, green: number, total: number) => {\n  if (total === 0) {\n    return \"bg-zinc-200 dark:bg-zinc-700\";\n  }\n  return index < green ? \"bg-emerald-500\" : \"bg-red-500\";\n};\n\n/** GitHub-style five-block bar: how much of the change adds vs. removes. */\nfunction ChangeBar({ added, removed }: { added: number; removed: number }) {\n  const total = added + removed;\n  const green = total === 0 ? 0 : Math.round((added / total) * 5);\n  return (\n    <span aria-hidden=\"true\" className=\"hidden gap-px sm:flex\">\n      {Array.from({ length: 5 }, (_, i) => (\n        <span\n          className={cn(\n            \"size-1.5 rounded-[1px] transition-colors duration-200\",\n            blockColor(i, green, total)\n          )}\n          // biome-ignore lint/suspicious/noArrayIndexKey: five fixed blocks\n          key={i}\n        />\n      ))}\n    </span>\n  );\n}\n\nconst LINE_STYLES: Record<DiffLine[\"type\"], string> = {\n  add: \"bg-emerald-500/10 text-emerald-900 dark:text-emerald-200\",\n  remove: \"bg-red-500/10 text-red-900 dark:text-red-200\",\n  context: \"text-zinc-600 dark:text-zinc-400\",\n};\nconst LINE_SIGNS: Record<DiffLine[\"type\"], string> = {\n  add: \"+\",\n  remove: \"−\",\n  context: \" \",\n};\n\nfunction Hunk({ hunk }: { hunk: DiffHunk }) {\n  let number = hunk.start;\n  return (\n    <div className=\"py-1\">\n      {hunk.lines.map((line, index) => {\n        const lineNumber = line.type === \"remove\" ? null : number;\n        if (line.type !== \"remove\") {\n          number += 1;\n        }\n        return (\n          <motion.div\n            animate={{ opacity: 1, y: 0 }}\n            className={cn(\n              \"flex font-mono text-xs leading-5\",\n              LINE_STYLES[line.type]\n            )}\n            initial={{ opacity: 0, y: 3 }}\n            // biome-ignore lint/suspicious/noArrayIndexKey: lines of a hunk only ever append\n            key={index}\n            transition={{ duration: 0.2, ease: EASE_OUT }}\n          >\n            <span className=\"w-9 shrink-0 select-none pr-2 text-right text-zinc-400 tabular-nums dark:text-zinc-600\">\n              {lineNumber}\n            </span>\n            <span className=\"w-4 shrink-0 select-none text-center opacity-60\">\n              {LINE_SIGNS[line.type]}\n            </span>\n            <span className=\"whitespace-pre pr-3\">{line.text || \" \"}</span>\n          </motion.div>\n        );\n      })}\n    </div>\n  );\n}\n\n/**\n * Animates to the measured height of its content, so a diff that grows a\n * line at a time glides open instead of stepping, and an expand that starts\n * mid-stream never snaps at the end. Content stays mounted while closed.\n */\nfunction Collapse({\n  open,\n  id,\n  reduce,\n  children,\n}: {\n  open: boolean;\n  id: string;\n  reduce: boolean;\n  children: ReactNode;\n}) {\n  const innerRef = useRef<HTMLDivElement>(null);\n  const [height, setHeight] = useState(0);\n\n  useEffect(() => {\n    const element = innerRef.current;\n    if (!element) {\n      return;\n    }\n    const observer = new ResizeObserver(() => setHeight(element.offsetHeight));\n    observer.observe(element);\n    return () => observer.disconnect();\n  }, []);\n\n  return (\n    <motion.div\n      animate={{ height: open ? height : 0, opacity: open ? 1 : 0 }}\n      className=\"overflow-hidden\"\n      id={id}\n      inert={!open}\n      initial={false}\n      transition={\n        reduce\n          ? { duration: 0 }\n          : {\n              height: HEIGHT_SPRING,\n              opacity: open ? SWAP_IN : SWAP_OUT,\n            }\n      }\n    >\n      <div ref={innerRef}>{children}</div>\n    </motion.div>\n  );\n}\n\nfunction FileRow({\n  file,\n  decision,\n  open,\n  writing,\n  onToggle,\n  onDecide,\n}: {\n  file: FileChange;\n  decision: FileDecision;\n  open: boolean;\n  writing: boolean;\n  onToggle: () => void;\n  onDecide: (decision: FileDecision) => void;\n}) {\n  const reduce = useReducedMotion() ?? false;\n  const diffId = useId();\n  const { added, removed } = countLines(file.hunks);\n  const slash = file.path.lastIndexOf(\"/\");\n  const dir = file.path.slice(0, slash + 1);\n  const name = file.path.slice(slash + 1);\n  const isOpen = open && decision === \"pending\";\n\n  return (\n    <motion.li\n      className=\"overflow-hidden border-zinc-100 border-t first:border-t-0 dark:border-white/5\"\n      exit={{ height: 0, opacity: 0 }}\n      transition={reduce ? { duration: 0 } : SWAP_IN}\n    >\n      <motion.div\n        animate={{ height: ROW_HEIGHT, opacity: 1 }}\n        className=\"group/row relative flex items-center gap-2 pr-1.5 pl-1\"\n        initial={{ height: 0, opacity: 0 }}\n        style={{ height: ROW_HEIGHT }}\n        transition={reduce ? { duration: 0 } : SWAP_IN}\n      >\n        <button\n          aria-controls={diffId}\n          aria-expanded={isOpen}\n          className=\"flex h-7 min-w-0 flex-1 items-center gap-2 rounded-md px-1.5 text-left outline-hidden focus-visible:ring-2 focus-visible:ring-zinc-400/40 disabled:cursor-default\"\n          disabled={decision !== \"pending\" || writing}\n          onClick={onToggle}\n          type=\"button\"\n        >\n          <ChevronRight\n            aria-hidden=\"true\"\n            className={cn(\n              \"size-3.5 shrink-0 text-zinc-400 transition-[rotate,opacity] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)]\",\n              isOpen && \"rotate-90\",\n              decision !== \"pending\" && \"opacity-0\"\n            )}\n          />\n          <span\n            className={cn(\n              \"min-w-0 truncate font-mono text-[13px] transition-colors duration-200\",\n              decision === \"rejected\" &&\n                \"text-zinc-400 line-through decoration-zinc-300 dark:text-zinc-500 dark:decoration-zinc-600\"\n            )}\n          >\n            <span className=\"text-zinc-400 dark:text-zinc-500\">{dir}</span>\n            <span\n              className={cn(\n                decision !== \"rejected\" && \"text-zinc-900 dark:text-zinc-100\"\n              )}\n            >\n              {name}\n            </span>\n          </span>\n          {file.created && (\n            <span\n              className={cn(\n                \"shrink-0 rounded bg-emerald-500/10 px-1 text-[10px] text-emerald-700 leading-4 transition-opacity duration-200 dark:text-emerald-300\",\n                decision === \"rejected\" && \"opacity-0\"\n              )}\n            >\n              new\n            </span>\n          )}\n        </button>\n\n        <AnimatePresence initial={false} mode=\"popLayout\">\n          {decision === \"pending\" ? (\n            <motion.div\n              animate={{ opacity: 1, filter: \"blur(0px)\", transition: SWAP_IN }}\n              className=\"flex items-center gap-2\"\n              exit={{ opacity: 0, filter: \"blur(2px)\", transition: SWAP_OUT }}\n              initial={{ opacity: 0, filter: \"blur(2px)\" }}\n              key=\"counts\"\n            >\n              <span className=\"font-mono text-[11px] tabular-nums\">\n                <span className=\"text-emerald-600 dark:text-emerald-400\">\n                  +{added}\n                </span>{\" \"}\n                <span className=\"text-red-600 dark:text-red-400\">\n                  −{removed}\n                </span>\n              </span>\n              <ChangeBar added={added} removed={removed} />\n              {/* Kept in the layout while writing so the counts never jump. */}\n              <span\n                className={cn(\n                  \"flex items-center gap-0.5 transition-opacity duration-200\",\n                  writing && \"opacity-0\"\n                )}\n                inert={writing}\n              >\n                <button\n                  aria-label={`Reject ${file.path}`}\n                  className=\"flex size-7 items-center justify-center rounded-md text-zinc-400 outline-hidden transition-colors duration-150 hover:bg-zinc-100 hover:text-red-600 focus-visible:ring-2 focus-visible:ring-zinc-400/50 dark:hover:bg-white/10 dark:hover:text-red-400\"\n                  onClick={() => onDecide(\"rejected\")}\n                  type=\"button\"\n                >\n                  <X aria-hidden=\"true\" className=\"size-3.5\" />\n                </button>\n                <button\n                  aria-label={`Accept ${file.path}`}\n                  className=\"flex size-7 items-center justify-center rounded-md text-zinc-400 outline-hidden transition-colors duration-150 hover:bg-zinc-100 hover:text-emerald-600 focus-visible:ring-2 focus-visible:ring-zinc-400/50 dark:hover:bg-white/10 dark:hover:text-emerald-400\"\n                  onClick={() => onDecide(\"accepted\")}\n                  type=\"button\"\n                >\n                  <Check aria-hidden=\"true\" className=\"size-3.5\" />\n                </button>\n              </span>\n            </motion.div>\n          ) : (\n            <motion.div\n              animate={{ opacity: 1, filter: \"blur(0px)\", transition: SWAP_IN }}\n              className=\"flex items-center gap-1.5\"\n              exit={{ opacity: 0, filter: \"blur(2px)\", transition: SWAP_OUT }}\n              initial={{ opacity: 0, filter: \"blur(2px)\" }}\n              key=\"decided\"\n            >\n              <span\n                className={cn(\n                  \"text-xs\",\n                  decision === \"accepted\"\n                    ? \"text-emerald-600 dark:text-emerald-400\"\n                    : \"text-zinc-400 dark:text-zinc-500\"\n                )}\n              >\n                {decision === \"accepted\" ? \"Accepted\" : \"Reverted\"}\n              </span>\n              <button\n                aria-label={`Undo for ${file.path}`}\n                className=\"flex size-7 items-center justify-center rounded-md text-zinc-400 outline-hidden transition-colors duration-150 hover:bg-zinc-100 hover:text-zinc-700 focus-visible:ring-2 focus-visible:ring-zinc-400/50 dark:hover:bg-white/10 dark:hover:text-zinc-200\"\n                onClick={() => onDecide(\"pending\")}\n                type=\"button\"\n              >\n                <Undo2 aria-hidden=\"true\" className=\"size-3.5\" />\n              </button>\n            </motion.div>\n          )}\n        </AnimatePresence>\n      </motion.div>\n\n      <Collapse id={diffId} open={isOpen} reduce={reduce}>\n        <div className=\"px-2 pb-2\">\n          <div className=\"overflow-x-auto rounded-lg bg-zinc-50 ring-1 ring-black/5 ring-inset dark:bg-white/[0.03] dark:ring-white/10\">\n            {file.hunks.map((hunk) => (\n              <Hunk hunk={hunk} key={hunk.start} />\n            ))}\n          </div>\n        </div>\n      </Collapse>\n    </motion.li>\n  );\n}\n\nexport default function AIDiffReview({\n  files: filesProp,\n  onDecision,\n  className,\n}: AIDiffReviewProps) {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const demo = useDemoStream(filesProp === undefined, rootRef);\n  const files = filesProp ?? demo.files;\n  const writing = filesProp === undefined && demo.writing;\n  const [decisions, setDecisions] = useState<Record<string, FileDecision>>({});\n  const [opened, setOpened] = useState<Record<string, boolean>>({});\n\n  // A replayed demo starts with a clean slate.\n  useEffect(() => {\n    if (demo.run > 0) {\n      setDecisions({});\n      setOpened({});\n    }\n  }, [demo.run]);\n\n  const decide = (path: string, decision: FileDecision) => {\n    setDecisions((current) => ({ ...current, [path]: decision }));\n    onDecision?.(path, decision);\n  };\n  const decisionOf = (path: string) => decisions[path] ?? \"pending\";\n\n  const totals = files.reduce(\n    (sum, file) => {\n      const { added, removed } = countLines(file.hunks);\n      return { added: sum.added + added, removed: sum.removed + removed };\n    },\n    { added: 0, removed: 0 }\n  );\n  const pending = files.filter((file) => decisionOf(file.path) === \"pending\");\n  const accepted = files.filter(\n    (file) => decisionOf(file.path) === \"accepted\"\n  ).length;\n  const allDecided = !writing && files.length > 0 && pending.length === 0;\n  // While streaming, only the file being written is open. When the demo\n  // finishes, the last file stays open rather than folding the card shut.\n  const writingPath = writing ? files.at(-1)?.path : undefined;\n  const restingPath = filesProp === undefined ? files.at(-1)?.path : undefined;\n  const isOpen = (path: string) =>\n    writingPath === undefined\n      ? (opened[path] ?? path === restingPath)\n      : path === writingPath;\n  let actions: \"review\" | \"replay\" | null = null;\n  if (allDecided && filesProp === undefined) {\n    actions = \"replay\";\n  } else if (!(writing || allDecided)) {\n    actions = \"review\";\n  }\n\n  let title = `${files.length} ${files.length === 1 ? \"file\" : \"files\"} changed`;\n  if (writing) {\n    title = \"Editing files\";\n  } else if (allDecided) {\n    title = `${accepted} of ${files.length} kept`;\n  }\n\n  return (\n    <MotionConfig reducedMotion=\"user\">\n      <div className={cn(\"w-full max-w-lg\", className)} ref={rootRef}>\n        <div className=\"rounded-2xl bg-black/5 p-1.5 dark:bg-white/5\">\n          <div className=\"flex h-9 items-center gap-2 px-2.5\">\n            <output className=\"relative font-medium text-sm text-zinc-900 dark:text-zinc-100\">\n              <AnimatePresence initial={false} mode=\"popLayout\">\n                <motion.span\n                  animate={{\n                    opacity: 1,\n                    filter: \"blur(0px)\",\n                    transition: SWAP_IN,\n                  }}\n                  className=\"inline-block whitespace-nowrap\"\n                  exit={{\n                    opacity: 0,\n                    filter: \"blur(2px)\",\n                    transition: SWAP_OUT,\n                  }}\n                  initial={{ opacity: 0, filter: \"blur(2px)\" }}\n                  key={title}\n                >\n                  {title}\n                </motion.span>\n              </AnimatePresence>\n            </output>\n            <motion.span\n              className=\"font-mono text-[11px] tabular-nums\"\n              layout=\"position\"\n              transition={SWAP_IN}\n            >\n              <span className=\"text-emerald-600 dark:text-emerald-400\">\n                +{totals.added}\n              </span>{\" \"}\n              <span className=\"text-red-600 dark:text-red-400\">\n                −{totals.removed}\n              </span>\n            </motion.span>\n            <div className=\"relative ml-auto flex items-center\">\n              <AnimatePresence initial={false} mode=\"popLayout\">\n                {actions === \"review\" && (\n                  <motion.div\n                    animate={{\n                      opacity: 1,\n                      filter: \"blur(0px)\",\n                      transition: SWAP_IN,\n                    }}\n                    className=\"flex items-center gap-1\"\n                    exit={{\n                      opacity: 0,\n                      filter: \"blur(2px)\",\n                      transition: SWAP_OUT,\n                    }}\n                    initial={{ opacity: 0, filter: \"blur(2px)\" }}\n                    key=\"review\"\n                  >\n                    <button\n                      className=\"h-7 rounded-md px-2 text-xs text-zinc-600 outline-hidden transition-colors duration-150 hover:bg-black/5 hover:text-zinc-900 focus-visible:ring-2 focus-visible:ring-zinc-400/50 dark:text-zinc-300 dark:hover:bg-white/10 dark:hover:text-white\"\n                      onClick={() => {\n                        for (const file of pending) {\n                          decide(file.path, \"rejected\");\n                        }\n                      }}\n                      type=\"button\"\n                    >\n                      Reject all\n                    </button>\n                    <button\n                      className=\"h-7 rounded-md bg-zinc-900 px-2.5 font-medium text-white text-xs outline-hidden transition-colors duration-150 hover:bg-zinc-700 focus-visible:ring-2 focus-visible:ring-zinc-400/60 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-200\"\n                      onClick={() => {\n                        for (const file of pending) {\n                          decide(file.path, \"accepted\");\n                        }\n                      }}\n                      type=\"button\"\n                    >\n                      Accept all\n                    </button>\n                  </motion.div>\n                )}\n                {actions === \"replay\" && (\n                  <motion.button\n                    animate={{\n                      opacity: 1,\n                      filter: \"blur(0px)\",\n                      transition: SWAP_IN,\n                    }}\n                    className=\"h-7 rounded-md px-2 text-xs text-zinc-500 outline-hidden transition-colors duration-150 hover:text-zinc-900 focus-visible:ring-2 focus-visible:ring-zinc-400/50 dark:text-zinc-400 dark:hover:text-white\"\n                    exit={{\n                      opacity: 0,\n                      filter: \"blur(2px)\",\n                      transition: SWAP_OUT,\n                    }}\n                    initial={{ opacity: 0, filter: \"blur(2px)\" }}\n                    key=\"replay\"\n                    onClick={demo.replay}\n                    type=\"button\"\n                  >\n                    Replay\n                  </motion.button>\n                )}\n              </AnimatePresence>\n            </div>\n          </div>\n          <ul className=\"rounded-xl bg-white shadow-[0_1px_2px_rgb(0_0_0/0.04)] ring-1 ring-black/5 dark:bg-zinc-900 dark:ring-white/10\">\n            <AnimatePresence initial={false}>\n              {files.map((file) => (\n                <FileRow\n                  decision={decisionOf(file.path)}\n                  file={file}\n                  key={`${demo.run}:${file.path}`}\n                  onDecide={(decision) => decide(file.path, decision)}\n                  onToggle={() =>\n                    setOpened((current) => ({\n                      ...current,\n                      [file.path]: !isOpen(file.path),\n                    }))\n                  }\n                  open={isOpen(file.path)}\n                  writing={file.path === writingPath}\n                />\n              ))}\n              {files.length === 0 && (\n                <motion.li\n                  animate={{ height: 44, opacity: 1 }}\n                  className=\"flex items-center overflow-hidden px-3 text-sm text-zinc-400\"\n                  exit={{ height: 0, opacity: 0 }}\n                  initial={{ height: 0, opacity: 0 }}\n                  key=\"waiting\"\n                  style={{ height: 44 }}\n                  transition={SWAP_IN}\n                >\n                  Waiting for edits…\n                </motion.li>\n              )}\n            </AnimatePresence>\n          </ul>\n        </div>\n      </div>\n    </MotionConfig>\n  );\n}\n",
      "path": "/components/kokonutui/ai-diff-review.tsx",
      "target": "components/kokonutui/ai-diff-review.tsx"
    }
  ]
}