# Value

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

A plain object that describes the crop. Read it to save a crop, and set it to restore or control one.

A square crop of a 4032 × 3024 photo, half its height, right of the middle and a little above it, straightened 3°:

```ts
import type { CropValue } from "@zriz/react";

const value: CropValue = {
  center: { x: 0.6, y: 0.45 },
  size: { width: 0.375, height: 0.5 },
  rotation: 3,
  quarterTurns: 0,
  mirrored: false,
  perspective: { vertical: 0, horizontal: 0 },
  aspect: 1,
};
```

Outside React, import the type from `@zriz/core`, installed as in [exporting](https://zriz.dev/docs/exporting.md).

`onValueCommit`, `getValue()` and `useCropperValue` all hand you this object. `Root` and `createCropper` take the whole object as `value`, or part of it as `defaultValue`.

## Fields

- `center` `Point` = `{ x: 0.5, y: 0.5 }`

  Marks the middle of the crop. 0.5, 0.5 is the middle of the photo
- `size` `Size` = `{ width: 1, height: 1 }`

  Sets the crop's width and height as fractions of the photo's sides
- `rotation` `number` = `0`

  Straightens the photo, in degrees from −45 to 45. A `minSize` with a diagonal longer than a side of the photo lowers the limit, see `getRotationLimit()` on [cropper](https://zriz.dev/docs/cropper.md)
- `quarterTurns` `0 | 1 | 2 | 3` = `0`

  Counts clockwise 90° turns. One counter-clockwise turn reads 3
- `mirrored` `boolean` = `false`

  Swaps left and right. Each flip toggles it and negates `rotation`. A left to right flip also turns a `quarterTurns` of 1 into 3, and a top to bottom flip turns 0 into 2
- `perspective` `object` = `{ vertical: 0, horizontal: 0 }`

  Tilts the photo by `vertical` and `horizontal` degrees. No part or gesture sets it, only `setValue()`, `setCropState()`, `value` and `defaultValue`. Keep each within −30 to 30. `parse` clamps to that range, `setValue()` does not
- `aspect` `Aspect` = `"free"`

  Locks the crop's shape to a ratio such as `16 / 9`, to `"original"` for the photo's own shape, or to nothing with `"free"`

`center` and `size` are fractions of the photo after its quarter turns, never pixels. The same value fits a 400 px preview and the 4032 px original.

zriz measures each side on its own. The largest square crop of a 4032 × 3024 photo reads `size: { width: 0.75, height: 1 }`.

There is no zoom field. Zoom follows from `size`, and `getZoom()` returns it: 1 is the largest crop that fits.

## See how zriz fixes a bad value

zriz keeps every value you set valid. It clamps the angle, reshapes a `size` that misses its `aspect` at the same area, grows a crop below `minSize` or past `maxZoom`, and shrinks or moves a crop that would show an empty corner. Ask for a 90° straighten and you get the limit:

```ts
import { createCropper } from "@zriz/core";

const cropper = createCropper({
  image: { width: 4032, height: 3024 },
});

cropper.setValue({ rotation: 90 });
cropper.getValue().rotation; // 45
```

A field that is not a usable number keeps the value it had, and a write with a usable field that still cannot make a crop, such as `setRotation(NaN)`, is ignored. An `aspect` that is not a number from 0.000001 to 1000000 counts as `"free"`. An option that is not a positive number falls back to its default, but any `maxZoom` below 1, zero and negatives included, counts as 1.

A value from zriz is frozen, so one reader cannot change it for the next. Copy it to change a field: `setValue({ ...value, rotation: 2 })`.

Before a photo loads, zriz stores a value as you set it and fixes it when the photo arrives.

## Control the angle from React state

Pass `rotation` and update it in `onValueChange`:

```tsx
"use client";

import * as Cropper from "@zriz/react/cropper";
import { useState } from "react";

export const Editor = ({ src }: { src: string }) => {
  const [rotation, setRotation] = useState(0);

  return (
    <Cropper.Root
      src={src}
      rotation={rotation}
      onValueChange={(value) => setRotation(value.rotation)}
    >
      <Cropper.Viewport style={{ maxWidth: 480, height: 360 }}>
        <Cropper.Image />
        <Cropper.Mask />
        <Cropper.Area aria-label="Crop area" />
      </Cropper.Viewport>
      <Cropper.Dial
        aria-label="Straighten"
        style={{ maxWidth: 361 }}
      >
        <Cropper.DialTicks />
      </Cropper.Dial>
    </Cropper.Root>
  );
};
```

`rotation`, `zoom` and `aspect` each control one part of the crop. A new prop moves the crop and commits, so `onValueCommit` runs with the whole new value. `onValueChange` stays quiet when each controlled prop lands as passed, even when the crop size moves with it, as it does for a new `aspect`. It runs when zriz applied something else: a rotation of 60 reports 45. To mirror the whole value, write it from both callbacks. A value zriz reported comes back as an answer and does not move the crop, for 2 s after the last report, so a slow store cannot rewind it. If no other answer follows and the crop has not moved since, that value applies when the 2 s end, or 500 ms after it arrived if that is later. A preset equal to a recent report lands late this way, and a value never reported lands at once. Mirror `rotation`, `zoom` or `aspect` from `onValueChange`, not `onValueCommit`: a store slower than 2 s that mirrors commits still flips the crop. Zoom runs from 1 up to `maxZoom`. A prop applied as the photo arrives is silent.

`value` sets the whole object without calling either callback. Each new object moves the crop, unless zriz reported that same object in the last 2 s, so keep it stable or update it from `onValueChange`. `Reset` still goes to `defaultValue`, not to the last `value`.

During a gesture and until it commits, zriz ignores a new `value`, `rotation` or `zoom`, so a slow render cannot fight the finger. To snap the crop after a gesture, write it in `onValueCommit`. A new `aspect` applies at once. The `aspect`, `rotation` and `zoom` from the first render become the reset target.

A prop acts when it changes. `zoom` also acts when a photo of another size arrives: with `zoom={3}` held, the crop goes back to 3 and commits with reason `coverage`. A write ignored during a gesture is not replayed. To limit a gesture, call `details.cancel()`. `rotation={5}` with no handler sets the angle once and the user can move away from it.

A photo after `null` counts as a first photo: zriz fits your `value` to it and sends nothing. Read `getValue()` if you keep the fitted crop.

## Turn it into text and back

Save a crop as text and read it back:

```ts
import { createCropper, parse, serialize } from "@zriz/core";

const cropper = createCropper();
const text = serialize(cropper.getValue());
const saved = parse(text);
```

`serialize` adds a version field, `v: 1`. `parse` takes the text or an object you already parsed, and returns `null` when it is not JSON, not an object or carries a `v` other than 1. A value with no `v` still reads. It fills missing fields with defaults, clamps `rotation` to 45 and `perspective` to 30 each way, and turns a bad `aspect` into `"free"`. The full flow is on [saving state](https://zriz.dev/docs/saving-state.md).

## Helpers

A constant and pure functions from `@zriz/core`:

| Export | What it does |
| - | - |
| `defaultValue` | Is the whole photo, level, with a free aspect |
| `valuesEqual` | Takes `(a, b)` and returns `true` when every field matches |
| `changedFields` | Takes `(a, b)` and returns the names of the fields that differ |
| `aspectRatio` | Takes `(aspect, image, quarterTurns)` and returns the ratio as a number, or `null` for a free aspect |
| `valueToState` | Takes `(value, image, focal?)` and returns the crop in image pixels. `focal` is a length in pixels and defaults to a 26 mm lens on the photo's long side |
| `stateToValue` | Takes `(state, aspect)` and turns a crop in image pixels back into a value |
