# Undo

> [llms.txt](https://zriz.dev/llms.txt)

An undo button for the crop, built from what each commit hands you.

The page at https\://zriz.dev/docs/undo runs this cropper: a 3:2 photo, the straighten dial with rotate and undo, and open, reset and download in the corners.

## Remember the crop before each gesture

`details.previous` holds the crop from before the gesture. One gesture is one commit, however many frames it took. The stack keeps the last 50.

`remember` skips commits with two reasons. `history` is the undo itself, and `coverage` is zriz fitting the crop to new limits.

```tsx
const skipped = new Set(["history", "coverage"]);

const [history, setHistory] = useState<CropValue[]>([]);

const remember = (_: CropValue, details: CommitDetails) => {
  if (skipped.has(details.reason)) return;

  setHistory((stack) =>
    [...stack, details.previous].slice(-50)
  );
};
```

## Step back

`setValue()` commits like any other change, with the reason you give it. A press during a gesture does nothing, because the gesture would overwrite the step and the stack would lose it. The button reads as off while the stack is empty, with `aria-disabled`, so it keeps focus when the last step goes.

For redo, keep a second stack. `undo` pushes `actions.current.getValue()` onto it before it steps back. A `redo` function pops it, pushes the current crop onto the undo stack and calls `setValue()` with `history` too. `remember` clears the redo stack after the reason check, so a new gesture drops it.

```tsx
const undo = () => {
  const previous = history.at(-1);

  if (!previous || actions.current?.getState().interacting) {
    return;
  }

  setHistory(history.slice(0, -1));
  actions.current?.setValue(previous, { reason: "history" });
};

<button
  type="button"
  className="undo"
  aria-disabled={history.length === 0}
  onClick={undo}
>
  Undo
</button>
```

The browser dims only `disabled`, so paint the off state yourself. Save this as `undo.css` next to the editor, which imports it:

```css
.undo[aria-disabled="true"] {
  opacity: 0.4;
}
```

## Wire it to the cropper

`onValueCommit` hands every commit to `remember`. Crops from the last photo make no sense on the next one, so `UndoEditor` keys `Editor` by `src`. Each photo gets a fresh cropper at the full frame and an empty stack, and a late commit from the old cropper has no stack to reach. The old photo leaves the screen at once.

```tsx
<Cropper.Root
  actionsRef={actions}
  src={src}
  onValueCommit={remember}
>

export const UndoEditor = ({ src }: { src: string }) => (
  <Editor key={src} src={src} />
);
```

## The whole file

`Editor` adds the button under the dial:

```tsx
"use client";

import type {
  CommitDetails,
  Cropper as CropperApi,
  CropValue,
} from "@zriz/react";
import * as Cropper from "@zriz/react/cropper";
import "@zriz/react/styles.css";
import { useRef, useState } from "react";
import "./undo.css";

const skipped = new Set(["history", "coverage"]);

const Editor = ({ src }: { src: string }) => {
  const actions = useRef<CropperApi>(null);
  const [history, setHistory] = useState<CropValue[]>([]);

  const remember = (_: CropValue, details: CommitDetails) => {
    if (skipped.has(details.reason)) return;

    setHistory((stack) =>
      [...stack, details.previous].slice(-50)
    );
  };

  const undo = () => {
    const previous = history.at(-1);

    if (!previous || actions.current?.getState().interacting) {
      return;
    }

    setHistory(history.slice(0, -1));
    actions.current?.setValue(previous, { reason: "history" });
  };

  return (
    <Cropper.Root
      actionsRef={actions}
      src={src}
      onValueCommit={remember}
    >
      <Cropper.Viewport style={{ maxWidth: 640, height: 480 }}>
        <Cropper.Image />
        <Cropper.Mask />
        <Cropper.Area aria-label="Crop area" />
      </Cropper.Viewport>
      <Cropper.Dial
        aria-label="Straighten"
        style={{ maxWidth: 361 }}
      >
        <Cropper.DialTicks />
      </Cropper.Dial>
      <button
        type="button"
        className="undo"
        aria-disabled={history.length === 0}
        onClick={undo}
      >
        Undo
      </button>
    </Cropper.Root>
  );
};

export const UndoEditor = ({ src }: { src: string }) => (
  <Editor key={src} src={src} />
);
```

The demo above runs this file with a rotate button, so a turn is one more step to undo. It adds handles, a grid and reset, shows the angle to one decimal and uses the styles from the zriz site.
