{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "rewind",
  "type": "registry:component",
  "title": "Rewind",
  "description": "Scrub back through everything an agent changed on one timeline: later changes ghost out and show what they'd go back to, the ones that can't be undone pin the playhead with a lock and say why, and Rewind to here comes with an Undo.",
  "categories": [
    "component",
    "Trust & Control"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://coss.com/ui/r/button.json"
  ],
  "files": [
    {
      "path": "registry/devl/rewind.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport {\n  ArrowRightIcon,\n  CheckIcon,\n  DatabaseIcon,\n  FileCodeIcon,\n  HistoryIcon,\n  LockIcon,\n  MailIcon,\n  MessageSquareIcon,\n  RocketIcon,\n  SlidersHorizontalIcon,\n  Undo2Icon,\n} from \"lucide-react\";\nimport { AnimatePresence, MotionConfig, motion, useReducedMotion, useSpring, useTransform } from \"motion/react\";\nimport { type KeyboardEvent, type PointerEvent, type ReactNode, useEffect, useLayoutEffect, useRef, useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\nexport type RewindKind = \"file\" | \"database\" | \"setting\" | \"message\" | \"email\" | \"deploy\";\n\nexport interface RewindChange {\n  id: string;\n  kind: RewindKind;\n  title: string;\n  target: string;\n  before: string;\n  after: string;\n  at: string;\n  irreversible?: string;\n}\n\nexport interface RewindProps {\n  changes: RewindChange[];\n  agent?: string;\n  task?: string;\n  defaultAt?: number;\n  onRewind?: (changes: RewindChange[]) => void | Promise<void>;\n  onUndo?: (changes: RewindChange[]) => void | Promise<void>;\n  className?: string;\n}\n\nconst kindIcons: Record<RewindKind, typeof FileCodeIcon> = {\n  file: FileCodeIcon,\n  database: DatabaseIcon,\n  setting: SlidersHorizontalIcon,\n  message: MessageSquareIcon,\n  email: MailIcon,\n  deploy: RocketIcon,\n};\n\nconst ease = [0.23, 1, 0.32, 1] as const;\nconst swap = { duration: 0.16, ease };\nconst press = \"transition-[scale,box-shadow,background-color] duration-150 ease-out active:scale-[0.97]\";\nconst clamp = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v));\n\ntype Phase = { name: \"idle\" } | { name: \"confirm\" } | { name: \"working\" } | { name: \"done\"; batch: RewindChange[]; to: string };\n\nexport function Rewind({ changes, agent = \"Agent\", task, defaultAt, onRewind, onUndo, className }: RewindProps) {\n  const reduce = useReducedMotion();\n  const [gone, setGone] = useState<Set<string>>(() => new Set());\n  const live = changes.filter((c) => !gone.has(c.id));\n  const n = live.length;\n  let lockAt = -1;\n  live.forEach((c, i) => {\n    if (c.irreversible) lockAt = i;\n  });\n  const minSlot = lockAt + 1;\n\n  const [slot, setSlot] = useState(() => clamp(defaultAt ?? n, minSlot, n));\n  const [phase, setPhase] = useState<Phase>({ name: \"idle\" });\n  const [hover, setHover] = useState<string | null>(null);\n  const [bump, setBump] = useState(0);\n\n  const track = useRef<HTMLDivElement>(null);\n  const handle = useRef<HTMLDivElement>(null);\n  const [width, setWidth] = useState(0);\n  const dragging = useRef(false);\n  const x = useSpring(0, { stiffness: 700, damping: 48, mass: 0.6 });\n  const hatch = useTransform(x, (v) => `inset(0 0 0 ${v}px)`);\n  const blockedRef = useRef(false);\n\n  const at = (k: number) => ((k + 0.5) / (n + 1)) * width;\n  const markAt = (i: number) => ((i + 1) / (n + 1)) * width;\n\n  useLayoutEffect(() => {\n    const el = track.current;\n    if (!el) return;\n    const ro = new ResizeObserver(() => setWidth(el.clientWidth));\n    ro.observe(el);\n    setWidth(el.clientWidth);\n    return () => ro.disconnect();\n  }, []);\n\n  const placed = useRef(false);\n  useLayoutEffect(() => {\n    if (!width || dragging.current) return;\n    const target = ((slot + 0.5) / (n + 1)) * width;\n    if (!placed.current || reduce) x.jump(target);\n    else x.set(target);\n    placed.current = true;\n  }, [slot, width, n, x, reduce]);\n\n  const pushBack = () => {\n    setBump((b) => b + 1);\n    if (reduce) return;\n    const home = at(minSlot);\n    x.set(home - 10);\n    window.setTimeout(() => !dragging.current && x.set(home), 90);\n  };\n\n  const busy = phase.name === \"working\";\n  const move = (k: number) => {\n    if (busy) return;\n    if (k < minSlot) {\n      if (slot !== minSlot) setSlot(minSlot);\n      pushBack();\n      return;\n    }\n    setSlot(clamp(k, minSlot, n));\n    if (phase.name === \"confirm\" || phase.name === \"done\") setPhase({ name: \"idle\" });\n  };\n\n  const fromPointer = (e: PointerEvent) => {\n    const rect = track.current?.getBoundingClientRect();\n    if (!rect || !width) return;\n    const px = e.clientX - rect.left;\n    const raw = Math.round((px / width) * (n + 1) - 0.5);\n    const k = clamp(raw, 0, n);\n    if (k < minSlot) {\n      if (slot !== minSlot) setSlot(minSlot);\n      if (phase.name !== \"idle\") setPhase({ name: \"idle\" });\n      const home = at(minSlot);\n      x.set(reduce ? home : home - Math.min(14, Math.max(0, home - px) * 0.25));\n      if (!blockedRef.current) setBump((b) => b + 1);\n      blockedRef.current = true;\n      return;\n    }\n    blockedRef.current = false;\n    if (k !== slot) move(k);\n    x.set(at(k));\n  };\n\n  const onKeyDown = (e: KeyboardEvent) => {\n    const keys: Record<string, number> = { ArrowLeft: slot - 1, ArrowDown: slot - 1, ArrowRight: slot + 1, ArrowUp: slot + 1, Home: minSlot, End: n };\n    const k = keys[e.key];\n    if (k === undefined) return;\n    e.preventDefault();\n    if (k > n) return;\n    move(k);\n  };\n\n  const revert = live.slice(slot);\n  const label = (k: number) => (k === n ? \"Now\" : k === 0 ? \"Start\" : live[k - 1].at);\n  const locked = lockAt >= 0 && slot === minSlot;\n\n  const commit = async () => {\n    const batch = revert;\n    const to = label(slot);\n    setPhase({ name: \"working\" });\n    try {\n      await onRewind?.(batch);\n      setGone((g) => new Set([...g, ...batch.map((c) => c.id)]));\n      setPhase({ name: \"done\", batch, to });\n      handle.current?.focus();\n    } catch {\n      setPhase({ name: \"confirm\" });\n    }\n  };\n\n  const undo = async (batch: RewindChange[]) => {\n    setPhase({ name: \"working\" });\n    try {\n      await onUndo?.(batch);\n      setGone((g) => {\n        const next = new Set(g);\n        for (const c of batch) next.delete(c.id);\n        return next;\n      });\n    } finally {\n      setPhase({ name: \"idle\" });\n    }\n  };\n\n  const prevN = useRef(n);\n  useEffect(() => {\n    if (prevN.current !== n) setSlot(n);\n    prevN.current = n;\n  }, [n]);\n\n  let hint: { key: string; tone?: string; node: ReactNode };\n  if (n === 0) hint = { key: \"empty\", node: \"Nothing to rewind yet.\" };\n  else if (locked && slot < n)\n    hint = {\n      key: `lock-${live[lockAt].id}`,\n      tone: \"text-warning-foreground\",\n      node: (\n        <>\n          <LockIcon aria-hidden className=\"mt-px size-3.5 shrink-0\" />\n          <span>\n            <span className=\"font-medium\">Can’t go back past “{live[lockAt].title}”.</span> {live[lockAt].irreversible}\n          </span>\n        </>\n      ),\n    };\n  else if (slot < n)\n    hint = {\n      key: \"preview\",\n      node: (\n        <span>\n          Rewinding to <span className=\"font-medium font-mono text-foreground tabular-nums\">{label(slot)}</span> undoes {revert.length === 1 ? \"1 change\" : `${revert.length} changes`}.\n        </span>\n      ),\n    };\n  else if (lockAt >= 0 && lockAt === n - 1)\n    hint = {\n      key: \"stuck\",\n      tone: \"text-warning-foreground\",\n      node: (\n        <>\n          <LockIcon aria-hidden className=\"mt-px size-3.5 shrink-0\" />\n          <span>The last change can’t be taken back, so there’s nothing to rewind.</span>\n        </>\n      ),\n    };\n  else hint = { key: \"idle\", node: \"Drag the playhead back to see what a rewind would undo.\" };\n\n  return (\n    <MotionConfig reducedMotion=\"user\">\n      <section aria-label={`${agent}'s changes`} className={cn(\"w-full max-w-[520px] rounded-2xl border bg-card text-card-foreground shadow-xs/5\", className)}>\n        <header className=\"flex items-start justify-between gap-3 px-4 pt-4\">\n          <div className=\"min-w-0\">\n            <h3 className=\"flex items-center gap-2 font-medium text-sm\">\n              <HistoryIcon aria-hidden className=\"size-4 text-muted-foreground\" />\n              Rewind {agent}\n            </h3>\n            {task && <p className=\"mt-0.5 truncate text-muted-foreground text-xs\">{task}</p>}\n          </div>\n          <span className=\"shrink-0 pt-0.5 text-muted-foreground text-xs tabular-nums\">{n === 1 ? \"1 change\" : `${n} changes`}</span>\n        </header>\n\n        <div className=\"px-4 pt-3\">\n          <div\n            className={cn(\"relative h-16 touch-none select-none\", n > 0 && !busy ? \"cursor-ew-resize\" : \"pointer-events-none\")}\n            onLostPointerCapture={() => {\n              dragging.current = false;\n              x.set(at(slot));\n            }}\n            onPointerDown={(e) => {\n              if (e.button !== 0 || !n) return;\n              dragging.current = true;\n              e.currentTarget.setPointerCapture(e.pointerId);\n              fromPointer(e);\n              handle.current?.focus({ preventScroll: true });\n            }}\n            onPointerMove={(e) => dragging.current && fromPointer(e)}\n            onPointerUp={(e) => {\n              dragging.current = false;\n              e.currentTarget.releasePointerCapture(e.pointerId);\n              x.set(at(slot));\n            }}\n            ref={track}\n          >\n            <div className=\"absolute inset-x-0 top-10 h-1 -translate-y-1/2 rounded-full bg-foreground/12\" />\n            {width > 0 && n > 0 && (\n              <>\n                {minSlot > 0 && (\n                  <div className=\"absolute top-10 left-0 h-1 -translate-y-1/2 rounded-full bg-warning/40\" style={{ width: markAt(lockAt) }} />\n                )}\n                <motion.div\n                  className=\"absolute top-10 left-0 h-3 -translate-y-1/2 rounded-full bg-[repeating-linear-gradient(135deg,var(--color-info)_0_1.5px,transparent_1.5px_5px)] opacity-50\"\n                  style={{ clipPath: hatch, width: markAt(n - 1) + 10 }}\n                />\n              </>\n            )}\n\n            {width > 0 &&\n              live.map((c, i) => {\n                const undone = i >= slot;\n                const Icon = c.irreversible ? LockIcon : null;\n                return (\n                  <motion.div\n                    animate={{ x: markAt(i), opacity: 1 }}\n                    className=\"absolute top-10 left-0\"\n                    initial={false}\n                    key={c.id}\n                    transition={{ type: \"spring\", stiffness: 420, damping: 40 }}\n                  >\n                    {Icon ? (\n                      <span\n                        className={cn(\n                          \"absolute top-0 left-0 grid size-5 -translate-1/2 place-items-center rounded-full border bg-card text-warning-foreground shadow-xs/5 transition-transform duration-150\",\n                          hover === c.id && \"scale-110\",\n                        )}\n                      >\n                        <Icon className=\"size-3\" />\n                      </span>\n                    ) : (\n                      <span\n                        className={cn(\n                          \"absolute top-0 left-0 size-2.5 -translate-1/2 rounded-full transition-[background-color,box-shadow,scale] duration-150 ease-out\",\n                          undone ? \"bg-card shadow-[inset_0_0_0_1.5px_var(--color-info)]\" : \"bg-foreground/70\",\n                          hover === c.id && \"scale-125\",\n                        )}\n                      />\n                    )}\n                  </motion.div>\n                );\n              })}\n\n            {width > 0 && n > 0 && (\n              <motion.div className=\"absolute top-0 left-0 h-full\" style={{ x }}>\n                <div className=\"absolute top-0 left-0 -translate-x-1/2 whitespace-nowrap rounded-md bg-foreground px-1.5 py-0.5 font-medium font-mono text-[11px] text-background tabular-nums leading-4\">\n                  {label(slot)}\n                </div>\n                <div\n                  aria-label=\"Rewind to\"\n                  aria-valuemax={n}\n                  aria-valuemin={minSlot}\n                  aria-valuenow={slot}\n                  aria-valuetext={slot === n ? \"Now, nothing undone\" : `${label(slot)}, undoes ${revert.length} of ${n} changes`}\n                  className=\"absolute top-[26px] left-0 h-7 w-3.5 -translate-x-1/2 cursor-grab rounded-full border bg-background shadow-sm outline-none transition-shadow before:absolute before:-inset-x-3 before:-inset-y-1.5 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-card active:cursor-grabbing\"\n                  onKeyDown={onKeyDown}\n                  ref={handle}\n                  role=\"slider\"\n                  tabIndex={0}\n                >\n                  <span className=\"absolute inset-y-2 left-1/2 w-px -translate-x-1/2 bg-foreground/30\" />\n                </div>\n              </motion.div>\n            )}\n          </div>\n\n          <div className=\"flex justify-between font-mono text-[11px] text-muted-foreground tabular-nums\">\n            <span>{live[0]?.at ?? \"\"}</span>\n            <span>Now</span>\n          </div>\n\n          <div aria-live=\"polite\" className=\"mt-2 min-h-10 text-xs\">\n            <AnimatePresence initial={false} mode=\"wait\">\n              <motion.div\n                animate={{ opacity: 1, x: 0 }}\n                className={cn(\"flex gap-1.5 text-pretty text-muted-foreground\", hint.tone)}\n                exit={{ opacity: 0, transition: { duration: 0.08 } }}\n                initial={{ opacity: 0 }}\n                key={hint.key}\n                transition={swap}\n              >\n                <motion.span\n                  animate={hint.key.startsWith(\"lock\") && bump > 0 ? { x: [0, -3, 3, -2, 0] } : undefined}\n                  className=\"flex gap-1.5\"\n                  key={bump}\n                  transition={{ duration: 0.28, ease: \"easeOut\" }}\n                >\n                  {hint.node}\n                </motion.span>\n              </motion.div>\n            </AnimatePresence>\n          </div>\n        </div>\n\n        <ol className=\"mx-2 border-t px-0 py-1.5\">\n          <AnimatePresence initial={false}>\n            {live.map((c, i) => {\n              const undone = i >= slot;\n              const KindIcon = kindIcons[c.kind];\n              const pickable = i >= minSlot && !busy;\n              return (\n                <motion.li\n                  animate={{ opacity: 1, height: \"auto\" }}\n                  exit={{ opacity: 0, height: 0 }}\n                  initial={{ opacity: 0, height: 0 }}\n                  key={c.id}\n                  transition={{ duration: 0.22, ease }}\n                >\n                  <button\n                    aria-label={`${c.title} at ${c.at}. ${undone ? `Would go back to ${c.before}.` : c.irreversible ? \"Can't be undone.\" : `Rewind to before this.`}`}\n                    className={cn(\n                      \"group flex w-full items-center gap-3 rounded-lg px-2 py-1.5 text-left outline-none transition-colors duration-150 focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-default\",\n                      pickable && \"cursor-pointer [@media(hover:hover)]:hover:bg-accent/60\",\n                      hover === c.id && pickable && \"bg-accent/60\",\n                    )}\n                    disabled={!pickable}\n                    onClick={() => move(i)}\n                    onPointerEnter={() => setHover(c.id)}\n                    onPointerLeave={() => setHover((h) => (h === c.id ? null : h))}\n                    type=\"button\"\n                  >\n                    <span\n                      className={cn(\n                        \"grid size-7 shrink-0 place-items-center rounded-full transition-[opacity,background-color,color] duration-150\",\n                        c.irreversible ? \"bg-warning/10 text-warning-foreground\" : undone ? \"bg-info/10 text-info-foreground\" : \"bg-muted text-muted-foreground\",\n                      )}\n                    >\n                      {c.irreversible ? <LockIcon className=\"size-3.5\" /> : <KindIcon className=\"size-3.5\" />}\n                    </span>\n                    <span className=\"min-w-0 flex-1\">\n                      <span className=\"flex items-baseline justify-between gap-3\">\n                        <span className={cn(\"truncate text-sm transition-colors duration-150\", undone ? \"text-muted-foreground\" : \"text-foreground\")}>{c.title}</span>\n                        <span className=\"shrink-0 font-mono text-[11px] text-muted-foreground tabular-nums\">{c.at}</span>\n                      </span>\n                      <span className=\"mt-0.5 flex items-center justify-between gap-3 font-mono text-[11px]\">\n                        <span className=\"min-w-0 truncate text-muted-foreground\">{c.target}</span>\n                        <span className=\"flex min-w-0 max-w-[60%] shrink-0 items-center gap-1\">\n                          {undone ? (\n                            <>\n                              <span className=\"truncate text-muted-foreground line-through decoration-muted-foreground/60\">{c.after}</span>\n                              <ArrowRightIcon aria-hidden className=\"size-3 shrink-0 text-info-foreground\" />\n                              <span className=\"truncate text-info-foreground\">{c.before}</span>\n                            </>\n                          ) : (\n                            <span className=\"truncate text-foreground/80\">{c.after}</span>\n                          )}\n                        </span>\n                      </span>\n                    </span>\n                  </button>\n                </motion.li>\n              );\n            })}\n          </AnimatePresence>\n        </ol>\n\n        <footer className=\"flex min-h-14 items-center justify-between gap-3 border-t px-4 py-3\">\n          <AnimatePresence initial={false} mode=\"wait\">\n            {phase.name === \"done\" ? (\n              <motion.div animate={{ opacity: 1, y: 0 }} className=\"flex w-full items-center justify-between gap-3\" exit={{ opacity: 0 }} initial={{ opacity: 0, y: 2 }} key=\"done\" transition={swap}>\n                <span className=\"flex min-w-0 items-center gap-1.5 text-xs\">\n                  <CheckIcon aria-hidden className=\"size-3.5 shrink-0 text-success-foreground\" />\n                  <span className=\"truncate\">\n                    Back to <span className=\"font-mono tabular-nums\">{phase.to}</span> · {phase.batch.length === 1 ? \"1 change\" : `${phase.batch.length} changes`} undone\n                  </span>\n                </span>\n                <Button className={press} onClick={() => undo(phase.batch)} size=\"sm\" variant=\"outline\">\n                  <Undo2Icon />\n                  Undo\n                </Button>\n              </motion.div>\n            ) : phase.name === \"confirm\" || phase.name === \"working\" ? (\n              <motion.div animate={{ opacity: 1, y: 0 }} className=\"flex w-full items-center justify-between gap-3\" exit={{ opacity: 0 }} initial={{ opacity: 0, y: 2 }} key=\"confirm\" transition={swap}>\n                <span className=\"min-w-0 text-pretty text-xs\">\n                  Undo {revert.length === 1 ? \"1 change\" : `${revert.length} changes`} made after <span className=\"font-mono tabular-nums\">{label(slot)}</span>?\n                </span>\n                <span className=\"flex shrink-0 gap-2\">\n                  <Button className={press} disabled={busy} onClick={() => setPhase({ name: \"idle\" })} size=\"sm\" variant=\"ghost\">\n                    Cancel\n                  </Button>\n                  <Button autoFocus className={press} loading={busy} onClick={commit} size=\"sm\">\n                    Rewind\n                  </Button>\n                </span>\n              </motion.div>\n            ) : (\n              <motion.div animate={{ opacity: 1, y: 0 }} className=\"flex w-full items-center justify-between gap-3\" exit={{ opacity: 0 }} initial={{ opacity: 0, y: 2 }} key=\"idle\" transition={swap}>\n                <span className=\"text-muted-foreground text-xs tabular-nums\">\n                  {slot < n ? `Keeps ${slot} · undoes ${revert.length}` : \"Up to date\"}\n                </span>\n                <Button className={press} disabled={slot >= n} onClick={() => setPhase({ name: \"confirm\" })} size=\"sm\">\n                  <Undo2Icon />\n                  Rewind to here\n                </Button>\n              </motion.div>\n            )}\n          </AnimatePresence>\n        </footer>\n      </section>\n    </MotionConfig>\n  );\n}\n"
    }
  ]
}