{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "checkpoints",
  "type": "registry:ui",
  "title": "Checkpoints",
  "description": "Restore points across an agent run: a compact rail of auto and saved checkpoints with relative times, a current marker and inline-confirmed Restore.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "https://kuro.abstergo.fyi/r/kura-theme.json",
    "https://kuro.abstergo.fyi/r/button.json",
    "utils"
  ],
  "files": [
    {
      "path": "registry/kura/checkpoints/checkpoints.tsx",
      "type": "registry:ui",
      "target": "components/kura/checkpoints/checkpoints.tsx",
      "content": "\"use client\";\n\nimport { Plus, RotateCcw } from \"lucide-react\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/components/kura/button\";\n\nexport interface Checkpoint {\n  /** Stable id passed to `onRestore`. */\n  id: string;\n  /** What the run had just done, e.g. \"Patched src/middleware.ts\". */\n  label: string;\n  /** When the checkpoint was taken. */\n  at: Date | number;\n  /** Files changed since the previous checkpoint. */\n  filesChanged?: number;\n  /** `auto` = taken by the harness before edits; `manual` = saved by a person. */\n  kind: \"auto\" | \"manual\";\n  /** The workspace is at this checkpoint. Checkpoints newer than it are shown as undone. */\n  current?: boolean;\n}\n\nexport interface CheckpointsProps extends Omit<React.ComponentProps<\"div\">, \"title\" | \"children\"> {\n  /** Restore points, newest first. */\n  checkpoints: Checkpoint[];\n  /** Panel title. Default \"Checkpoints\". */\n  title?: React.ReactNode;\n  /** Called after the inline confirm. Return a promise to show a pending state until it settles. */\n  onRestore?: (id: string) => void | Promise<void>;\n  /** Shows a \"Save\" button in the header that creates a manual checkpoint. */\n  onCreate?: () => void;\n  /** Ask \"Restore to …?\" inline before calling `onRestore`. Default true. */\n  confirm?: boolean;\n  /** Shown when `checkpoints` is empty. */\n  empty?: React.ReactNode;\n}\n\n/** Wall clock, ticking every 30s; null until mounted so SSR markup is stable. */\nfunction useNow(interval = 30_000) {\n  const [now, setNow] = React.useState<number | null>(null);\n  React.useEffect(() => {\n    setNow(Date.now());\n    const id = setInterval(() => setNow(Date.now()), interval);\n    return () => clearInterval(id);\n  }, [interval]);\n  return now;\n}\n\nfunction formatAgo(ms: number): string {\n  const s = Math.max(0, Math.round(ms / 1000));\n  if (s < 45) return \"just now\";\n  const m = Math.round(s / 60);\n  if (m < 60) return `${m}m ago`;\n  const h = Math.floor(m / 60);\n  if (h < 24) return `${h}h ago`;\n  return `${Math.floor(h / 24)}d ago`;\n}\n\n/**\n * Restore points across an agent run, newest first: what the agent had just done, how many\n * files changed, when, and whether it was automatic or saved by hand. \"Restore\" opens an inline\n * confirm (no modal) that says what will be undone. The current position is marked; checkpoints\n * after it read as undone and can be restored again.\n */\nexport function Checkpoints({\n  checkpoints,\n  title = \"Checkpoints\",\n  onRestore,\n  onCreate,\n  confirm = true,\n  empty,\n  className,\n  ...props\n}: CheckpointsProps) {\n  const id = React.useId();\n  const now = useNow();\n  const [confirmId, setConfirmId] = React.useState<string | null>(null);\n  const [pendingId, setPendingId] = React.useState<string | null>(null);\n  const [message, setMessage] = React.useState(\"\");\n  const triggers = React.useRef(new Map<string, HTMLButtonElement>());\n\n  const current = checkpoints.find((c) => c.current);\n  const currentAt = current ? +new Date(current.at) : null;\n\n  const restore = async (c: Checkpoint) => {\n    setPendingId(c.id);\n    try {\n      await onRestore?.(c.id);\n      setMessage(`Restored to “${c.label}”`);\n    } finally {\n      setPendingId(null);\n      setConfirmId(null);\n      triggers.current.get(c.id)?.focus();\n    }\n  };\n\n  const cancel = (c: Checkpoint) => {\n    setConfirmId(null);\n    requestAnimationFrame(() => triggers.current.get(c.id)?.focus());\n  };\n\n  return (\n    <div\n      data-slot=\"checkpoints\"\n      className={cn(\n        \"@container flex min-w-0 flex-col overflow-hidden rounded-kura-xl bg-kura-surface font-kura-sans text-kura-fg shadow-kura-sm\",\n        className,\n      )}\n      {...props}\n    >\n      <div\n        data-slot=\"checkpoints-header\"\n        className=\"flex h-10 shrink-0 items-center gap-2 border-kura-border-subtle border-b pr-2 pl-3.5\"\n      >\n        <h3 id={`${id}-title`} className=\"flex items-baseline gap-2 font-medium text-[13px]\">\n          {title}\n          <span className=\"font-kura-mono font-normal text-[12px] text-kura-fg-subtle tabular-nums\">\n            {checkpoints.length}\n          </span>\n        </h3>\n        {onCreate && (\n          <Button\n            data-slot=\"checkpoints-create\"\n            variant=\"ghost\"\n            size=\"xs\"\n            className=\"ml-auto\"\n            onClick={onCreate}\n          >\n            <Plus />\n            Save checkpoint\n          </Button>\n        )}\n      </div>\n\n      {checkpoints.length === 0 ? (\n        <div\n          data-slot=\"checkpoints-empty\"\n          className=\"flex min-h-24 flex-col items-center justify-center gap-1 px-6 py-6 text-center\"\n        >\n          {empty ?? (\n            <>\n              <p className=\"font-medium text-[13px] text-kura-fg-muted\">No checkpoints yet</p>\n              <p className=\"text-[12px] text-kura-fg-subtle\">\n                One is saved automatically before the agent edits files.\n              </p>\n            </>\n          )}\n        </div>\n      ) : (\n        <ol aria-labelledby={`${id}-title`} className=\"flex flex-col py-1\">\n          {checkpoints.map((c, i) => {\n            const at = +new Date(c.at);\n            const ahead = currentAt != null && at > currentAt;\n            const open = confirmId === c.id;\n            // Checkpoints between this one and where the workspace is now.\n            const between = checkpoints.filter((o) => {\n              const t = +new Date(o.at);\n              if (ahead) return currentAt != null && t > currentAt && t <= at;\n              return t > at && (currentAt == null || t <= currentAt);\n            }).length;\n            const first = i === 0;\n            const last = i === checkpoints.length - 1;\n            return (\n              <li\n                key={c.id}\n                data-slot=\"checkpoints-item\"\n                data-kind={c.kind}\n                data-current={c.current || undefined}\n                data-ahead={ahead || undefined}\n                data-state={open ? \"confirming\" : undefined}\n                className=\"group/cp relative animate-[kura-rise-in_var(--kura-duration-base)_var(--ease-kura-out)_both] [--kura-rise-blur:0px] [--kura-rise-y:2px] motion-reduce:[--kura-rise-y:0px]\"\n              >\n                {checkpoints.length > 1 && (\n                  <span\n                    aria-hidden\n                    className={cn(\n                      \"absolute left-[21.5px] w-px bg-kura-border\",\n                      first ? \"top-[18px] bottom-0\" : last ? \"top-0 h-[18px]\" : \"inset-y-0\",\n                      ahead && \"bg-kura-border-subtle\",\n                    )}\n                  />\n                )}\n                <div\n                  className={cn(\n                    \"relative flex h-9 items-center gap-2.5 pr-2 pl-3.5 transition-colors duration-(--kura-duration-instant) ease-kura-out\",\n                    !c.current && \"hover:bg-kura-hover\",\n                    open && \"bg-kura-hover\",\n                  )}\n                >\n                  <span aria-hidden className=\"grid w-4 shrink-0 place-items-center\">\n                    <Node checkpoint={c} ahead={ahead} />\n                  </span>\n                  <span\n                    data-slot=\"checkpoints-label\"\n                    className={cn(\n                      \"min-w-0 truncate text-[13px]\",\n                      c.current && \"font-medium\",\n                      ahead && \"text-kura-fg-subtle\",\n                    )}\n                  >\n                    {c.label}\n                  </span>\n                  {c.kind === \"manual\" && (\n                    <span className=\"shrink-0 text-[12px] text-kura-fg-subtle\">Saved</span>\n                  )}\n                  {c.current && (\n                    <span\n                      data-slot=\"checkpoints-current\"\n                      className=\"inline-flex h-[18px] shrink-0 items-center rounded-full bg-kura-hover px-1.5 font-medium text-[11px] text-kura-fg-muted\"\n                    >\n                      Current\n                    </span>\n                  )}\n                  {ahead && <span className=\"sr-only\">(undone)</span>}\n                  <span className=\"ml-auto flex shrink-0 items-center gap-3 font-kura-mono text-[12px] text-kura-fg-subtle tabular-nums\">\n                    {c.filesChanged != null && (\n                      <span data-slot=\"checkpoints-files\" className=\"hidden @sm:inline\">\n                        {c.filesChanged} {c.filesChanged === 1 ? \"file\" : \"files\"}\n                      </span>\n                    )}\n                    <span className=\"grid w-16 justify-items-end [grid-template-areas:'cell'] @sm:w-[5.5rem]\">\n                      <time\n                        dateTime={now != null ? new Date(at).toISOString() : undefined}\n                        title={now != null ? new Date(at).toLocaleString() : undefined}\n                        className={cn(\n                          \"min-w-[7ch] text-right [grid-area:cell]\",\n                          !c.current &&\n                            onRestore &&\n                            !open &&\n                            \"group-hover/cp:opacity-0 group-focus-within/cp:opacity-0 [@media(hover:none)]:opacity-0\",\n                        )}\n                      >\n                        {now != null ? formatAgo(now - at) : \"\"}\n                      </time>\n                      {!c.current && onRestore && (\n                        <Button\n                          ref={(el) => {\n                            if (el) triggers.current.set(c.id, el);\n                            else triggers.current.delete(c.id);\n                          }}\n                          data-slot=\"checkpoints-restore\"\n                          aria-label={`Restore to ${c.label}`}\n                          variant=\"ghost\"\n                          size=\"xs\"\n                          aria-expanded={confirm ? open : undefined}\n                          aria-controls={confirm ? `${id}-confirm-${c.id}` : undefined}\n                          disabled={pendingId != null}\n                          onClick={() => (confirm ? setConfirmId(open ? null : c.id) : restore(c))}\n                          className={cn(\n                            \"-my-1 font-kura-sans opacity-0 [grid-area:cell] focus-visible:opacity-100\",\n                            !open && \"group-hover/cp:opacity-100 [@media(hover:none)]:opacity-100\",\n                          )}\n                        >\n                          <RotateCcw />\n                          <span className=\"hidden @sm:inline\">Restore</span>\n                        </Button>\n                      )}\n                    </span>\n                  </span>\n                </div>\n                {confirm && !c.current && onRestore && (\n                  <div\n                    id={`${id}-confirm-${c.id}`}\n                    data-slot=\"checkpoints-confirm\"\n                    inert={!open || undefined}\n                    className={cn(\n                      \"grid transition-[grid-template-rows,opacity] duration-(--kura-duration-base) ease-kura-out motion-reduce:transition-opacity\",\n                      open ? \"grid-rows-[1fr] opacity-100\" : \"grid-rows-[0fr] opacity-0\",\n                    )}\n                  >\n                    <div className=\"min-h-0 overflow-hidden\">\n                      {open && (\n                        <ConfirmBody\n                          checkpoint={c}\n                          between={between}\n                          forward={ahead}\n                          pending={pendingId === c.id}\n                          onCancel={() => cancel(c)}\n                          onConfirm={() => restore(c)}\n                        />\n                      )}\n                    </div>\n                  </div>\n                )}\n              </li>\n            );\n          })}\n        </ol>\n      )}\n      <span role=\"status\" className=\"sr-only\">\n        {message}\n      </span>\n    </div>\n  );\n}\n\nfunction Node({ checkpoint, ahead }: { checkpoint: Checkpoint; ahead: boolean }) {\n  if (checkpoint.current) {\n    return (\n      <span className=\"size-2.5 rounded-full bg-kura-fg shadow-[0_0_0_3px_var(--kura-surface)]\" />\n    );\n  }\n  if (checkpoint.kind === \"manual\") {\n    return (\n      <span\n        className={cn(\n          \"size-2 rotate-45 rounded-[2px]\",\n          ahead ? \"bg-kura-border-strong\" : \"bg-kura-fg-muted\",\n        )}\n      />\n    );\n  }\n  return (\n    <span\n      className={cn(\n        \"size-2 rounded-full bg-kura-surface\",\n        ahead ? \"ring-1 ring-kura-border-strong\" : \"ring-[1.5px] ring-kura-fg-subtle\",\n      )}\n    />\n  );\n}\n\nfunction ConfirmBody({\n  checkpoint,\n  between,\n  forward,\n  pending,\n  onCancel,\n  onConfirm,\n}: {\n  checkpoint: Checkpoint;\n  between: number;\n  forward: boolean;\n  pending: boolean;\n  onCancel: () => void;\n  onConfirm: () => void;\n}) {\n  const confirmRef = React.useRef<HTMLButtonElement>(null);\n  React.useEffect(() => {\n    confirmRef.current?.focus({ preventScroll: true });\n  }, []);\n  return (\n    <fieldset\n      aria-label={`Restore to ${checkpoint.label}`}\n      onKeyDown={(e) => {\n        if (e.key === \"Escape\") {\n          e.stopPropagation();\n          onCancel();\n        }\n      }}\n      className=\"m-0 flex min-w-0 flex-col gap-2.5 border-0 pt-1 pr-3.5 pb-3 pl-10\"\n    >\n      <p className=\"text-[12.5px] text-kura-fg-muted leading-[18px]\">\n        {forward ? (\n          <>\n            Restore files forward to this point? Re-applies edits from {between}{\" \"}\n            {between === 1 ? \"checkpoint\" : \"checkpoints\"}.\n          </>\n        ) : (\n          <>\n            Restore files to this point? Edits from{\" \"}\n            {between > 0\n              ? `${between} later ${between === 1 ? \"checkpoint\" : \"checkpoints\"}`\n              : \"later steps\"}{\" \"}\n            are undone. The conversation is kept, and you can restore forward again.\n          </>\n        )}\n      </p>\n      <div className=\"flex items-center justify-end gap-1.5\">\n        <Button variant=\"ghost\" size=\"xs\" onClick={onCancel} disabled={pending}>\n          Cancel\n        </Button>\n        <Button ref={confirmRef} variant=\"primary\" size=\"xs\" loading={pending} onClick={onConfirm}>\n          <RotateCcw />\n          Restore\n        </Button>\n      </div>\n    </fieldset>\n  );\n}\n"
    },
    {
      "path": "registry/kura/checkpoints/index.ts",
      "type": "registry:ui",
      "target": "components/kura/checkpoints/index.ts",
      "content": "export { type Checkpoint, Checkpoints, type CheckpointsProps } from \"./checkpoints\";\n"
    }
  ],
  "categories": [
    "agent",
    "agent",
    "checkpoint",
    "restore",
    "rewind"
  ],
  "docs": "Docs: https://kuro.abstergo.fyi/components/checkpoints · Markdown for agents: https://kuro.abstergo.fyi/components/checkpoints.md",
  "meta": {
    "kura": {
      "status": "experimental",
      "whenToUse": [
        "A coding agent snapshots the workspace before edits (Claude Code rewind, Cursor checkpoints) and the human should be able to roll files back.",
        "Showing where the workspace is after a rewind: checkpoints newer than the current one read as undone and can be restored forward.",
        "Letting people save a manual checkpoint before a risky step (onCreate)."
      ],
      "whenNotToUse": [
        "The agent's plan or steps — use task-list or agent-timeline.",
        "Reviewing what changed in files — use diff-view or file-changes.",
        "Git history or commits — this is for harness snapshots, not a commit log."
      ]
    }
  }
}
