# Introduction

> Photo editor components for React, built for shadcn/ui.

photocn is a photo editor you install with the shadcn CLI. Add the whole editor with one command, or pick the pieces you need and arrange them yourself. The files are copied into your project, so you can change anything.

```tsx
"use client";

import { ImageEditor } from "@/components/image-editor/image-editor";

export default function FullEditorExample() {
  return <ImageEditor className="h-[680px] overflow-hidden rounded-xl border" src="/samples/mountain-lake.jpg" />;
}
```

[Video](https://photocn.dev/assets/adjust.mp4)

## How it works

photocn has two parts:

- **The engine**: the `photocn` npm package. It does the image work: WebGL rendering in a background thread, filters, crop and rotate, EXIF, undo history, and export.
- **The components**: the editor UI, installed into `components/image-editor/` by the shadcn CLI. They use your theme and your shadcn/ui components.

You get engine fixes from npm updates, and the UI stays yours.

## Three ways to use it

- [The full editor](https://photocn.dev/docs/editor): one component with every tool.
- [Compose your own](https://photocn.dev/docs/composition): put the pieces you want in your own layout.
- [Headless](https://photocn.dev/docs/headless): use the `useImageEditorState` hook with your own UI.
- [Without React](https://photocn.dev/docs/without-react): edit and export photos from plain JavaScript.

## Features

- Light, color and effect adjustments
- Curves for RGB and each channel, over a live histogram
- 13 filters
- Crop, straighten, perspective, rotate and flip, all reversible
- Lens blur and image blending
- Undo and redo, and hold to compare with the original
- PNG, JPEG (keeps EXIF) and WebP export, at any size
- Works on phones: bottom tool bar, panels in a sheet, pinch to zoom
- Display P3 color


---

# Installation

> Add photocn to a React project that uses shadcn/ui.

## 1. Set up shadcn/ui

photocn uses the Radix version of shadcn/ui. New projects default to Base UI, so choose Radix when you set up:

```bash
npx shadcn@latest init -b radix
```

## 2. Add the photocn registry

Run this once per project. It tells the shadcn CLI where `@photocn/...` components live:

```bash
npx shadcn@latest registry add @photocn=https://photocn.dev/r/{name}.json
```

This adds the following to your `components.json`:

```json
{
  "registries": {
    "@photocn": "https://photocn.dev/r/{name}.json"
  }
}
```

## 3. Add the editor

```bash
npx shadcn@latest add @photocn/image-editor
```

This installs the `photocn` package, the shadcn/ui components the editor needs, and the editor files in `components/image-editor/`.

## 4. Use it

```tsx
import { ImageEditor } from "@/components/image-editor/image-editor";

export default function Page() {
  return <ImageEditor className="h-dvh" src="/photo.jpg" />;
}
```

The editor is a client component. `src` can be a URL, a `File`, a `Blob`, an `ArrayBuffer` or an `<img>`. Images from other domains need CORS headers.

## Only need some pieces?

Every piece can be installed on its own:

```bash
npx shadcn@latest add @photocn/image-editor-canvas @photocn/image-editor-filters @photocn/image-editor-export
```

## Content-Security-Policy

The renderer runs in a Web Worker started from a `blob:` URL, so no bundler setup is needed. If your CSP blocks `worker-src blob:`, start the worker yourself:

```tsx

    new Worker(new URL("photocn/worker/entry", import.meta.url), { type: "module" })
  }
/>
```


---

# API Reference

> The useImageEditor hook that every component is built on.

```tsx
import {
  ImageEditorProvider, // creates the editor, or shares one you pass in
  useImageEditor,      // reads the nearest provider
  useImageEditorState, // creates an editor without a provider
} from "photocn/react";
```

## Options

`useImageEditorState`, `<ImageEditorProvider>` and `<ImageEditor>` all take these options.

| Option | Type | Description |
| --- | --- | --- |
| `src` | `string \| File \| Blob \| ArrayBuffer \| HTMLImageElement` | The image to edit. |
| `defaultParams` | `EditorParams` | Edits to start with, e.g. a saved session. Older formats are converted. |
| `onParamsChange` | `(params) => void` | Called after every edit. |
| `tool` / `defaultTool` / `onToolChange` | `string` | The active tool. Default `"adjust"`. |
| `onImageLoad` / `onImageError` | `(result) / (error) => void` | Image loading events. |
| `onExport` | `(result) => void` | Called after each export. |
| `disabled` | `boolean` | Turn off all editing. |
| `filterPresets` | `FilterPreset[]` | Replace the built-in filters. |
| `aspectRatioOptions` | `{ value, label }[]` | Crop ratio choices, e.g. `{ value: "3:2", label: "3:2" }`. |
| `renderMode` | `"worker" \| "main"` | Where rendering happens. Defaults to a worker when the browser supports it. |
| `spawnWorker` | `() => Worker` | Start the render worker yourself, for strict CSPs. |
| `keyboardShortcuts` | `boolean` | Undo/redo and crop shortcuts. Default `true`. |
| `historyLimit` / `commitDelay` | `number` | Undo depth (100), and how long (ms) a slider drag counts as one step (400). |

## What it returns

| Field | Description |
| --- | --- |
| `status`, `isReady`, `isLoading`, `hasImage`, `error` | Loading state. |
| `params`, `setParams`, `patch` | The current edits. `patch(section, values, { transient })` groups slider drags into one undo step. |
| `history` | `undo`, `redo`, `canUndo`, `canRedo`. |
| `tool`, `setTool` | The active tool. |
| `openFile()`, `load(src)` | Open a file picker, or load an image from code. |
| `adjust` | Light, color and effects. |
| `filters` | `presets`, `select(preset \| label \| null)`, `setStrength(0..1)`. |
| `curves`, `blur`, `blend` | Each tool's values and setters. |
| `geometry` | Crop, straighten, perspective, rotate and flip. See [Crop & transform](https://photocn.dev/docs/crop-and-transform). |
| `compare` | `active`, `setActive`: show the original. |
| `recipes` | `current` (the look as JSON) and `apply(recipe)`. |
| `histogram` | `data` and a `canvasRef` it draws into. |
| `exportImage(options)`, `download(options)` | Encode at full size, or at `width`/`height`. Options: `format` (`png`, `jpeg`, `webp`) and `quality`. |
| `canvasRef`, `canvasKey`, `stageRef`, `rootRef` | Attach these to your canvas, its container and the editor's outer element. Use `canvasKey` as the canvas's `key`. |
| `engine`, `worker` | Lower-level access to the renderer and worker. |

## Custom filters

```tsx
import { createLutPreset, filterPresets } from "photocn/filters";

// A 33×1089 3D LUT image, the same format as the built-in filters.
const presets = [...filterPresets, createLutPreset("teal-orange", "/luts/teal-orange.png")];

<ImageEditor src={src} filterPresets={presets} />;
```


---

# The full editor

> One component with every tool. Use it as is, or trim it down.

```tsx
"use client";

import { ImageEditor } from "@/components/image-editor/image-editor";

export default function FullEditorExample() {
  return <ImageEditor className="h-[680px] overflow-hidden rounded-xl border" src="/samples/mountain-lake.jpg" />;
}
```

## Installation

```bash
npx shadcn@latest add @photocn/image-editor
```

## Choose the tools

`tools` sets which tools appear. `panels` replaces a built-in panel or adds a panel for your own tool. Custom panels use `useImageEditor()`, just like the built-in ones.

```tsx
"use client";

import { CropIcon, SlidersHorizontalIcon, StickerIcon, WandSparklesIcon } from "lucide-react";
import { useImageEditor } from "photocn/react";

import { Button } from "@/components/ui/button";
import { ImageEditor } from "@/components/image-editor/image-editor";

// The full editor, trimmed to three built-in tools plus one of your own.
export default function CustomToolsExample() {
  return (
    <ImageEditor
      className="h-[640px] overflow-hidden rounded-xl border"
      defaultTool="filters"
      panels={{ stamp: <StampPanel /> }}
      showRecipes={false}
      src="/samples/dog.jpg"
      tools={[
        { value: "filters", label: "Filters", icon: WandSparklesIcon },
        { value: "adjust", label: "Adjust", icon: SlidersHorizontalIcon },
        { value: "compose", label: "Crop", icon: CropIcon },
        { value: "stamp", label: "Looks", icon: StickerIcon },
      ]}
    />
  );
}

// Custom panels get the same API as the built-in ones.
function StampPanel() {
  const editor = useImageEditor();
  const looks = {
    Moody: { exposure: -0.15, contrast: 0.35, shadows: -0.2 },
    Airy: { exposure: 0.2, contrast: -0.15, highlights: -0.3 },
    Punchy: { contrast: 0.45, bloom: 0.15 },
  } as const;
  return (
    <div className="flex flex-col gap-2 p-4">
      <h2 className="text-sm font-semibold">One-tap looks</h2>
      {Object.entries(looks).map(([name, look]) => (
        <Button
          key={name}
          onClick={() => editor.adjust.setLights({ ...editor.adjust.value.lights, ...look })}
          variant="outline"
        >
          {name}
        </Button>
      ))}
    </div>
  );
}
```

## On phones

When the editor is narrower than 640px, it switches to a phone layout: the tools move to a bar at the bottom, and each tool's controls open in a sheet. Set `layout="compact"` or `layout="desktop"` to choose a layout yourself. See [Compact layout](https://photocn.dev/docs/compact-layout).

## Props

`ImageEditor` takes every [option](https://photocn.dev/docs/api) of `useImageEditorState`, plus:

| Prop | Type | Description |
| --- | --- | --- |
| `editor` | `ImageEditorApi` | An editor you created with `useImageEditorState()`. |
| `tools` | `EditorToolbarTool[]` | The tools to show. Default: all of them. |
| `panels` | `Record<toolId, ReactNode>` | Replace a panel or add your own. |
| `layout` | `"auto" \| "desktop" \| "compact"` | Default `"auto"`: compact below 640px. |
| `showOpenButton` | `boolean` | Show the Open button. Default `true`. |
| `showRecipes` | `boolean` | Show the save/load look buttons. Default `true`. |
| `showHistogram` | `boolean` | Show the histogram. Default `true`. |
| `toolbarExtra` | `ReactNode` | Extra buttons, placed before Export. |
| `onSave` | `(result) => void` | Adds a Save button to the export panel, e.g. to upload the result. |


---

# Compose your own

> Put the pieces you want into your own layout.

## How it works

`<ImageEditorProvider>` holds the image, the edits and the undo history. Every `ImageEditor*` component inside it reads from it with `useImageEditor()`, so they stay in sync automatically.

```tsx

  <ImageEditorCanvas />
  <ImageEditorAdjustments />
  <ImageEditorUndoButton />
  <MyOwnControl /> {/* calls useImageEditor() */}

```

## A smaller editor

The canvas, adjustments, undo, compare and export.

```tsx
"use client";

import { ImageEditorProvider } from "photocn/react";

import { ImageEditorAdjustments } from "@/components/image-editor/adjustments-panel";
import { ImageEditorCanvas } from "@/components/image-editor/canvas";
import { ImageEditorExportDialog } from "@/components/image-editor/export-dialog";
import {
  ImageEditorCompareButton,
  ImageEditorRedoButton,
  ImageEditorUndoButton,
} from "@/components/image-editor/toolbar";

export default function ComposeYourOwnExample() {
  return (
    <ImageEditorProvider src="/samples/street.jpg">
      <div className="grid h-[560px] grid-rows-[auto_1fr] overflow-hidden rounded-xl border bg-background md:grid-cols-[1fr_300px]">
        <header className="flex items-center gap-1 border-b p-2 md:col-span-2">
          <ImageEditorUndoButton />
          <ImageEditorRedoButton />
          <div className="ml-auto flex gap-2">
            <ImageEditorCompareButton />
            <ImageEditorExportDialog />
          </div>
        </header>
        <ImageEditorCanvas className="min-h-64 p-3" />
        <aside className="overflow-y-auto border-t md:border-t-0 md:border-l">
          <ImageEditorAdjustments />
        </aside>
      </div>
    </ImageEditorProvider>
  );
}
```

## Your own controls

This filter picker is plain JSX on top of `useImageEditor()`. It lists `editor.filters.presets`, calls `editor.filters.select()`, and shares the canvas.

```tsx
"use client";

import { ImageEditorProvider, useImageEditor } from "photocn/react";

import { Button } from "@/components/ui/button";
import { Slider } from "@/components/ui/slider";
import { cn } from "@/lib/utils";
import { ImageEditorCanvas } from "@/components/image-editor/canvas";

export default function FilterStripExample() {
  return (
    <ImageEditorProvider src="/samples/strawberries.jpg">
      <div className="mx-auto flex h-[640px] max-w-sm flex-col overflow-hidden rounded-3xl border bg-background shadow-sm">
        <ImageEditorCanvas className="min-h-0 flex-1 p-2" showOpenButton={false} />
        <FilterStrip />
      </div>
    </ImageEditorProvider>
  );
}

// A custom control built only from `useImageEditor()` — no photocn panel.
function FilterStrip() {
  const { filters, isReady } = useImageEditor();
  return (
    <div className="flex flex-col gap-3 border-t p-3">
      <div className="flex gap-2 overflow-x-auto pb-1">
        <FilterChip
          active={filters.value.label === null}
          disabled={!isReady}
          label="Original"
          onClick={() => void filters.select(null)}
        />
        {filters.presets.map((preset) => (
          <FilterChip
            active={filters.value.label === preset.label}
            disabled={!isReady || filters.loading !== null}
            key={preset.label}
            label={preset.label}
            onClick={() => void filters.select(preset)}
          />
        ))}
      </div>
      {filters.value.label ? (
        <Slider
          aria-label="Filter strength"
          max={100}
          onValueChange={([value]) => filters.setStrength((value ?? 0) / 100)}
          value={[Math.round(filters.value.strength * 100)]}
        />
      ) : null}
    </div>
  );
}

function FilterChip({
  active,
  label,
  ...props
}: { active: boolean; label: string } & React.ComponentProps<typeof Button>) {
  return (
    <Button
      aria-pressed={active}
      className={cn("shrink-0 rounded-full capitalize", active && "shadow-sm")}
      size="sm"
      variant={active ? "default" : "outline"}
      {...props}
    >
      {label}
    </Button>
  );
}
```

## Controlled versions

Each panel file also exports a plain version that takes `value` and `onChange`: `AdjustmentPanel`, `CurvesPanel`, `FiltersPanel` and so on. Use these when you want to drive a panel from your own state, or use it outside a provider.


---

# Headless

> Use the editor with your own UI and no photocn components.

`useImageEditorState()` from `photocn/react` returns the same API the components use. Put `canvasRef` (with `key={canvasKey}`) on a `<canvas>` and `stageRef` on its container, and build any UI you like. To share it with child components, pass it to `<ImageEditorProvider editor={editor}>`.

```tsx
"use client";

import { useImageEditorState, type AdjustLightValue } from "photocn/react";

// No provider, no registry components: the headless hook plus a <canvas>.
export default function HeadlessExample() {
  const editor = useImageEditorState({ src: "/samples/portrait.jpg" });
  const { lights } = editor.adjust.value;

  const slider = (key: keyof AdjustLightValue, label: string) => (
    <label className="flex flex-col gap-1 text-sm">
      <span className="flex justify-between">
        {label}
        <span className="tabular-nums text-muted-foreground">{lights[key].toFixed(2)}</span>
      </span>
      <input
        max={1}
        min={-1}
        onChange={(event) =>
          editor.adjust.setLights({ ...lights, [key]: Number(event.target.value) })
        }
        step={0.01}
        type="range"
        value={lights[key]}
      />
    </label>
  );

  return (
    <div className="grid gap-4 rounded-xl border p-4 sm:grid-cols-[1fr_220px]" ref={editor.rootRef}>
      <div className="relative h-[480px] overflow-hidden rounded-lg bg-muted" ref={editor.stageRef}>
        {editor.imageSrc ? (
          <canvas className="absolute inset-0 size-full object-contain" key={editor.canvasKey} ref={editor.canvasRef} />
        ) : null}
      </div>
      <div className="flex flex-col gap-4">
        {slider("exposure", "Exposure")}
        {slider("contrast", "Contrast")}
        {slider("highlights", "Highlights")}
        {slider("shadows", "Shadows")}
        <div className="mt-auto flex gap-2">
          <button className="rounded-md border px-3 py-1.5 text-sm" disabled={!editor.history.canUndo} onClick={editor.history.undo} type="button">
            Undo
          </button>
          <button
            className="rounded-md bg-foreground px-3 py-1.5 text-sm text-background"
            disabled={!editor.isReady}
            onClick={() => void editor.download({ format: "jpeg", quality: 0.9 })}
            type="button"
          >
            Download JPEG
          </button>
        </div>
      </div>
    </div>
  );
}
```

## Lower level

`photocn/hooks` has the building blocks: `useMiniPhotoEditor`, `useEditorHistory`, `useHistogram` and `useExifMetadata`. Outside React, use [`createPhoto()`](https://photocn.dev/docs/without-react).


---

# Without React

> Edit and export photos from plain JavaScript with createPhoto().

`createPhoto()` from `photocn/photo` loads an image and returns a `Photo`: a stateful object you edit with method calls. It doesn't depend on React, so you can use it in Vue, Svelte, web components, plain scripts, or a React app that only needs to process images.

```tsx
"use client";

import { useEffect, useRef } from "react";
import { createPhoto } from "photocn/photo";

// Plain DOM: no React state, no components. Works the same in Vue, Svelte,
// a web component or a <script type="module">.
async function mountPhotoEditor(root: HTMLElement, signal: AbortSignal) {
  root.innerHTML = `
    <canvas class="aspect-[3/2] w-full rounded-lg bg-muted object-contain"></canvas>
    <div class="flex flex-wrap items-center gap-3 text-sm">
      <label class="flex items-center gap-2">Exposure <input name="exposure" type="range" min="-1" max="1" step="0.01" value="0" /></label>
      <label class="flex items-center gap-2">Saturation <input name="saturation" type="range" min="-1" max="1" step="0.01" value="0" /></label>
      <button data-filter="juno" class="rounded-md border px-2.5 py-1">Juno</button>
      <button data-filter="moon" class="rounded-md border px-2.5 py-1">Moon</button>
      <button data-action="rotate" class="rounded-md border px-2.5 py-1">Rotate</button>
      <button data-action="undo" class="rounded-md border px-2.5 py-1">Undo</button>
      <button data-action="export" class="rounded-md border bg-primary px-2.5 py-1 text-primary-foreground">Export</button>
      <output class="text-muted-foreground"></output>
    </div>`;

  const canvas = root.querySelector("canvas")!;
  const inputs = root.querySelectorAll<HTMLInputElement>("input[type=range]");
  const output = root.querySelector("output")!;

  const photo = await createPhoto("/samples/street.jpg");
  if (signal.aborted) return photo.dispose();
  signal.addEventListener("abort", () => photo.dispose());
  photo.attach(canvas);

  inputs.forEach((input) =>
    input.addEventListener("input", () => photo.adjust({ [input.name]: Number(input.value) }), { signal }),
  );
  root.addEventListener("click", async (event) => {
    const button = (event.target as HTMLElement).closest("button");
    if (!button) return;
    if (button.dataset.filter) photo.filter(button.dataset.filter);
    if (button.dataset.action === "rotate") photo.rotate(1);
    if (button.dataset.action === "undo") photo.undo();
    if (button.dataset.action === "export") {
      const { blob, width, height } = await photo.export({ format: "jpeg", width: 1200 });
      output.textContent = `${width}×${height}, ${Math.round(blob.size / 1024)} KB`;
    }
  }, { signal });
  // Keep the controls in sync after undo.
  photo.subscribe(() => {
    inputs[0]!.value = String(photo.params.lights.exposure);
    inputs[1]!.value = String(photo.params.colors.saturation);
  });
}

// Only this wrapper is React, to show the demo on this page.
export default function VanillaExample() {
  const ref = useRef<HTMLDivElement>(null);
  useEffect(() => {
    const controller = new AbortController();
    void mountPhotoEditor(ref.current!, controller.signal);
    return () => controller.abort();
  }, []);
  return <div className="flex flex-col gap-3" ref={ref} />;
}
```

## Edit and export

Every edit returns the photo, so calls chain. Nothing is baked in until you export.

```ts
import { createPhoto } from "photocn/photo";

const photo = await createPhoto(file); // File, Blob, URL, ArrayBuffer or <img>

photo
  .adjust({ exposure: 0.3, contrast: 0.2, saturation: -1 })
  .filter("juno", 0.8)
  .straighten(4)
  .aspectRatio("1:1");

const { blob, width, height, filename } = await photo.export({ format: "jpeg", width: 1080 });
photo.dispose();
```

To process many photos the same way, export one edit as a recipe and apply it to the others:

```ts
const look = photo.recipe;
for (const file of files) {
  const next = await createPhoto(file);
  await next.applyRecipe(look!);
  upload(await next.export({ format: "webp" }));
  next.dispose();
}
```

## Live preview

`attach(canvas)` renders every change into a `<canvas>`, in a Web Worker when the browser supports it. `subscribe()` tells you when to update your own controls.

```ts
const detach = photo.attach(document.querySelector("canvas")!);
const unsubscribe = photo.subscribe((photo) => {
  exposureInput.value = String(photo.params.lights.exposure);
});
```

A canvas handed to the worker can't be reused, so give each `attach()` a fresh `<canvas>`.

## API

| Member | Description |
|---|---|
| `adjust(values)` | Light, color and effect sliders, each `-1..1`: `brightness`, `exposure`, `gamma`, `contrast`, `shadows`, `highlights`, `bloom`, `temperature`, `tint`, `vibrance`, `saturation`, `sepia`, `clarity`, `noise`, `vignette`. |
| `filter(name, strength?)` | Apply a look (`"juno"`, `"moon"`…) at `0..1` strength; `null` removes it. |
| `filterStrength(strength)` | Change the strength of the current look. |
| `curves(channels)` | Tone curves `[rgb, r, g, b]`, each a list of `[input, output]` points or `null`. |
| `blur({ bokeh, gaussian, focus, centerX, centerY })` | Lens or gaussian blur around a sharp center. |
| `rotate(1 \| -1)`, `flip(axis)` | Quarter turns and mirroring. |
| `straighten(degrees)` | `±45°`. The crop shrinks to stay inside the photo. |
| `perspective({ x, y })` | Keystone correction, `-1..1`. |
| `crop(rect)`, `aspectRatio(ratio)` | Normalized crop rect, or a ratio (`16 / 9`, `"4:3"`, `"original"`, `null`). |
| `undo()`, `redo()`, `reset()` | One step per call. |
| `params`, `setParams(params)` | The full edit as plain data (save it, restore it later). |
| `recipe`, `applyRecipe(recipe)` | A portable copy of the edit. |
| `export(options)` | `{ format, quality, width, height, preserveExif }` → `{ blob, type, width, height, filename }`. |
| `histogram()` | RGB histogram of the result. |
| `attach(canvas)`, `detach()` | Live preview. |
| `subscribe(listener)` | Called after every change; returns an unsubscribe function. |
| `ready()` | Resolves once pending filters have loaded. `export()` waits for it. |
| `size`, `outputSize`, `metadata`, `name` | Source size, size after crop, EXIF, filename. |
| `dispose()` | Free the renderer and worker. |

In React, use [`useImageEditorState`](https://photocn.dev/docs/headless) instead: it adds tools, transient slider edits, crop interactions and everything the components need.


---

# Saving edits

> Save the finished image, the edits, or both.

## Upload the result

`onSave` adds a Save button to the export panel. It receives the image as a `Blob`. JPEG exports keep the original EXIF data.

```tsx
<ImageEditor
  src={photo.url}
  onSave={async ({ blob, filename }) => {
    const body = new FormData();
    body.append("file", blob, filename);
    await fetch("/api/photos", { method: "POST", body });
  }}
/>
```

Or export from code: `await editor.exportImage({ format: "webp", quality: 0.9, width: 1600 })`.

## Save the edits instead

Edits are plain data. `onParamsChange` fires on every change, and `buildRecipe(params)` turns the edits into a small JSON object you can store and apply later with `editor.recipes.apply(recipe)`. To reopen a whole session, pass `defaultParams`.

```tsx
"use client";

import { useState } from "react";
import { buildRecipe, type EditorParams, type RecipeV1 } from "photocn";

import { ImageEditor } from "@/components/image-editor/image-editor";

// Persist edits anywhere: `onParamsChange` fires with every change and
// `buildRecipe` turns params into a small, serializable diff.
export default function ControlledExample() {
  const [recipe, setRecipe] = useState<RecipeV1 | null>(null);
  return (
    <div className="flex flex-col gap-4">
      <ImageEditor
        className="h-[600px] overflow-hidden rounded-xl border"
        onParamsChange={(params: EditorParams) => setRecipe(buildRecipe(params))}
        src="/samples/mountain-lake.jpg"
      />
      <pre className="max-h-72 overflow-auto rounded-xl border bg-muted/40 p-3 font-mono text-xs">
        {recipe ? JSON.stringify(recipe, null, 2) : "// Move a slider…"}
      </pre>
    </div>
  );
}
```

## Older saved sessions

Edits saved by earlier versions are converted automatically when you pass them to `defaultParams` or `setParams`. To convert them yourself, call `normalizeEditorParams(saved)` from `photocn`.


---

# Crop & transform

> How crop, straighten, perspective and rotate work together.

Crop works like the Photos app: every change can be undone or adjusted later. Edits never overwrite the original. The editor always starts from the original photo and applies the changes in the same order:

```
flip and rotate → perspective → straighten → crop → resize (only when exporting)
```

- **The crop is a window.** Open the crop tool again and you can make it bigger.
- **The frame stays put.** When you straighten or change perspective, the photo turns and zooms behind the frame so no empty corners show. Set straighten back to 0 and your original crop returns.
- **Rotating turns the whole picture**, crop included. A 4:5 crop becomes 5:4.
- **There's no zoom slider.** Make the crop smaller, or scroll or pinch inside the frame.
- **Resizing happens when you export**, in the export panel.

```tsx
"use client";

import { ImageEditorProvider } from "photocn/react";

import { ImageEditorCanvas } from "@/components/image-editor/canvas";
import { ImageEditorCrop } from "@/components/image-editor/crop-panel";

export default function CropDemo() {
  return (
    <ImageEditorProvider src="/samples/street.jpg" defaultTool="compose">
      <div className="grid h-[520px] grid-rows-[1fr_auto] overflow-hidden rounded-xl border bg-background sm:grid-cols-[1fr_300px] sm:grid-rows-1">
        <ImageEditorCanvas className="min-h-0 p-2" />
        <aside className="max-h-60 overflow-y-auto border-t sm:max-h-none sm:border-t-0 sm:border-l">
          <ImageEditorCrop />
        </aside>
      </div>
    </ImageEditorProvider>
  );
}
```

## Keyboard shortcuts

| Keys | Action |
| --- | --- |
| R / ⇧R | Rotate right / left |
| H · V | Flip horizontal · vertical |
| X | Switch portrait and landscape |
| [ · ] | Straighten by 0.5° (⇧ for 5°) |
| Arrows | Move the photo under the frame |
| Esc | Cancel the current drag |
| ⌫ | Reset the crop |
| ↵ | Done |

## From code

```tsx
const { geometry } = useImageEditor();

geometry.rotate(1);                    // quarter turn clockwise
geometry.setAspectRatio("16:9");
geometry.toggleOrientation();          // 16:9 → 9:16
geometry.setStraighten(3.5);           // -45 to 45 degrees
geometry.setPerspective({ y: 0.25 });  // -1 to 1
geometry.setCrop({ x: 0.1, y: 0.1, width: 0.8, height: 0.8 });
```


---

# Using with AI agents

> Docs and install commands that coding agents can read.

## llms.txt

[/llms.txt](https://photocn.dev/llms.txt) lists every component and block with an install command and a short example. [/llms-full.txt](https://photocn.dev/llms-full.txt) has every docs page in one file.

## Markdown pages

Every docs page is also available as markdown. Add `.md` to the URL (for example [/docs/editor.md](https://photocn.dev/docs/editor.md)), or request the page with `Accept: text/markdown`. Each page also has a **Copy page** button.

## Per-component pages

`/llm/<name>` describes one registry item: what it does, how to install it, and its files. For example [/llm/image-editor](https://photocn.dev/llm/image-editor).

## shadcn MCP

With the shadcn MCP server, agents can browse and install from photocn directly:

```bash
npx shadcn@latest mcp init --client claude
```

Then add the registry to `components.json`, as described in [Installation](https://photocn.dev/docs/installation).


---

# Canvas

> Shows the photo. In the crop tool it shows the crop frame, and everywhere else the edited result.

```tsx
"use client";

import { ImageEditorProvider } from "photocn/react";

import { ImageEditorCanvas } from "@/components/image-editor/canvas";

// Scroll with ⌘/Ctrl or pinch to zoom · press and hold to see the original ·
// drop another photo onto it.
export default function CanvasDemo() {
  return (
    <ImageEditorProvider src="/samples/mountain-lake.jpg">
      <ImageEditorCanvas className="h-[420px] rounded-xl border p-2" />
    </ImageEditorProvider>
  );
}
```

## Installation

```bash
npx shadcn@latest add @photocn/image-editor-canvas
```

## Usage

```tsx
import { ImageEditorCanvas } from "@/components/image-editor/canvas";


  <ImageEditorCanvas className="h-96" />

```

## Props

| Prop | Type | Description |
| --- | --- | --- |
| `className` | `string` | Classes for the outer element. |
| `alt` | `string` | Accessible name for the image. |
| `allowDrop` | `boolean` | Open images dropped onto it. Default `true`. |
| `showOpenButton` | `boolean` | Show the Open button when there's no image. Default `true`. |
| `emptyState` | `ReactNode` | What to show before an image is loaded. |
| `padding` | `number` | Space around the photo in px. Default 24. |
| `children` | `ReactNode` | Your own layers, drawn over the photo. |


---

# Toolbar

> The tool switcher, plus undo, redo, open, reset and compare buttons.

```tsx
"use client";

import { ImageEditorProvider } from "photocn/react";

import { ImageEditorCanvas } from "@/components/image-editor/canvas";
import { ImageEditorToolPanel } from "@/components/image-editor/tool-panel";
import {
  ImageEditorCompareButton,
  ImageEditorOpenButton,
  ImageEditorRedoButton,
  ImageEditorResetButton,
  ImageEditorToolbar,
  ImageEditorUndoButton,
} from "@/components/image-editor/toolbar";

export default function ToolbarDemo() {
  return (
    <ImageEditorProvider src="/samples/dog.jpg">
      <div className="flex h-[560px] flex-col overflow-hidden rounded-xl border bg-background">
        <div className="flex flex-wrap items-center gap-1 border-b p-2">
          <ImageEditorUndoButton />
          <ImageEditorRedoButton />
          <ImageEditorOpenButton />
          <div className="ml-auto flex gap-2">
            <ImageEditorResetButton />
            <ImageEditorCompareButton />
          </div>
        </div>
        <div className="border-b p-2">
          <ImageEditorToolbar />
        </div>
        <div className="grid min-h-0 flex-1 sm:grid-cols-[1fr_300px]">
          <ImageEditorCanvas className="min-h-48 p-2" />
          <aside className="hidden overflow-y-auto border-l sm:block">
            <ImageEditorToolPanel />
          </aside>
        </div>
      </div>
    </ImageEditorProvider>
  );
}
```

## Installation

```bash
npx shadcn@latest add @photocn/image-editor-toolbar
```

## Usage

```tsx
import {
  ImageEditorToolbar,
  ImageEditorUndoButton,
  ImageEditorRedoButton,
} from "@/components/image-editor/toolbar";




```

## Props

| Prop | Type | Description |
| --- | --- | --- |
| `ImageEditorToolbar · tools` | `EditorToolbarTool[]` | The tools to show. Default: all of them. |
| `ImageEditorToolbar · actions` | `EditorToolbarAction[]` | Extra icon buttons. |
| `Undo / Redo / Open / Reset / Compare · showLabel` | `boolean` | Show the text label next to the icon. |


---

# Adjustments

> Sliders for light, color and effects.

```tsx
"use client";

import { ImageEditorProvider } from "photocn/react";

import { ImageEditorCanvas } from "@/components/image-editor/canvas";
import { ImageEditorAdjustments } from "@/components/image-editor/adjustments-panel";

export default function AdjustmentsDemo() {
  return (
    <ImageEditorProvider src="/samples/mountain-lake.jpg">
      <div className="grid h-[520px] grid-rows-[1fr_auto] overflow-hidden rounded-xl border bg-background sm:grid-cols-[1fr_300px] sm:grid-rows-1">
        <ImageEditorCanvas className="min-h-0 p-2" />
        <aside className="max-h-60 overflow-y-auto border-t sm:max-h-none sm:border-t-0 sm:border-l">
          <ImageEditorAdjustments />
        </aside>
      </div>
    </ImageEditorProvider>
  );
}
```

## Installation

```bash
npx shadcn@latest add @photocn/image-editor-adjustments
```

## Usage

```tsx
import { ImageEditorAdjustments } from "@/components/image-editor/adjustments-panel";


```

## Props

| Prop | Type | Description |
| --- | --- | --- |
| `defaultSection` | `"lights" \| "colors" \| "effects"` | The tab shown first. |
| `disabled` | `boolean` | Turn off the sliders. |
| `className` | `string` | Classes for the panel. |


---

# Filters

> 13 filter presets with a strength slider. 0% is the original photo.

```tsx
"use client";

import { ImageEditorProvider } from "photocn/react";

import { ImageEditorCanvas } from "@/components/image-editor/canvas";
import { ImageEditorFilters } from "@/components/image-editor/filters-panel";

export default function FiltersDemo() {
  return (
    <ImageEditorProvider src="/samples/strawberries.jpg">
      <div className="grid h-[520px] grid-rows-[1fr_auto] overflow-hidden rounded-xl border bg-background sm:grid-cols-[1fr_300px] sm:grid-rows-1">
        <ImageEditorCanvas className="min-h-0 p-2" />
        <aside className="max-h-60 overflow-y-auto border-t sm:max-h-none sm:border-t-0 sm:border-l">
          <ImageEditorFilters />
        </aside>
      </div>
    </ImageEditorProvider>
  );
}
```

## Installation

```bash
npx shadcn@latest add @photocn/image-editor-filters
```

## Usage

```tsx
import { ImageEditorFilters } from "@/components/image-editor/filters-panel";


```

## Props

| Prop | Type | Description |
| --- | --- | --- |
| `options` | `FilterPreset[]` | The filters to show. Default: the editor's presets. |
| `disabled` | `boolean` | Turn off the panel. |


---

# Curves

> Tone curves for RGB and each color channel, drawn over a live histogram.

```tsx
"use client";

import { ImageEditorProvider } from "photocn/react";

import { ImageEditorCanvas } from "@/components/image-editor/canvas";
import { ImageEditorCurves } from "@/components/image-editor/curves-panel";

export default function CurvesDemo() {
  return (
    <ImageEditorProvider src="/samples/street.jpg">
      <div className="grid h-[520px] grid-rows-[1fr_auto] overflow-hidden rounded-xl border bg-background sm:grid-cols-[1fr_300px] sm:grid-rows-1">
        <ImageEditorCanvas className="min-h-0 p-2" />
        <aside className="max-h-60 overflow-y-auto border-t sm:max-h-none sm:border-t-0 sm:border-l">
          <ImageEditorCurves />
        </aside>
      </div>
    </ImageEditorProvider>
  );
}
```

## Installation

```bash
npx shadcn@latest add @photocn/image-editor-curves
```

## Usage

```tsx
import { ImageEditorCurves } from "@/components/image-editor/curves-panel";


```

## Props

| Prop | Type | Description |
| --- | --- | --- |
| `activeChannel` | `"rgb" \| "r" \| "g" \| "b"` | The channel being edited. |
| `size` | `number` | Size of the curve area in px. |
| `histogram` | `RgbHistogram \| null` | Override the histogram behind the curve. |


---

# Crop

> Rotate, flip, aspect ratio, straighten and perspective. You crop by dragging the frame on the photo.

```tsx
"use client";

import { ImageEditorProvider } from "photocn/react";

import { ImageEditorCanvas } from "@/components/image-editor/canvas";
import { ImageEditorCrop } from "@/components/image-editor/crop-panel";

export default function CropDemo() {
  return (
    <ImageEditorProvider src="/samples/street.jpg" defaultTool="compose">
      <div className="grid h-[520px] grid-rows-[1fr_auto] overflow-hidden rounded-xl border bg-background sm:grid-cols-[1fr_300px] sm:grid-rows-1">
        <ImageEditorCanvas className="min-h-0 p-2" />
        <aside className="max-h-60 overflow-y-auto border-t sm:max-h-none sm:border-t-0 sm:border-l">
          <ImageEditorCrop />
        </aside>
      </div>
    </ImageEditorProvider>
  );
}
```

## Installation

```bash
npx shadcn@latest add @photocn/image-editor-crop
```

## Usage

```tsx
import { ImageEditorCrop } from "@/components/image-editor/crop-panel";

// The canvas shows the crop frame while the "compose" tool is active.

  <ImageEditorCanvas />
  <ImageEditorCrop />

```

## Props

| Prop | Type | Description |
| --- | --- | --- |
| `hidePerspective` | `boolean` | Hide the perspective controls. |
| `showShortcuts` | `boolean` | Show the list of keyboard shortcuts. Default `true`. |
| `onDone` | `() => void` | Called by the Done button. Default: go back to the previous tool. |


---

# Blur

> Lens blur (bokeh or gaussian). Drag the dot on the photo to choose what stays sharp.

```tsx
"use client";

import { ImageEditorProvider } from "photocn/react";

import { ImageEditorCanvas } from "@/components/image-editor/canvas";
import { ImageEditorBlur } from "@/components/image-editor/blur-panel";

export default function BlurDemo() {
  return (
    <ImageEditorProvider src="/samples/dog.jpg" defaultTool="blur">
      <div className="grid h-[520px] grid-rows-[1fr_auto] overflow-hidden rounded-xl border bg-background sm:grid-cols-[1fr_300px] sm:grid-rows-1">
        <ImageEditorCanvas className="min-h-0 p-2" />
        <aside className="max-h-60 overflow-y-auto border-t sm:max-h-none sm:border-t-0 sm:border-l">
          <ImageEditorBlur />
        </aside>
      </div>
    </ImageEditorProvider>
  );
}
```

## Installation

```bash
npx shadcn@latest add @photocn/image-editor-blur
```

## Usage

```tsx
import { ImageEditorBlur } from "@/components/image-editor/blur-panel";


```

## Props

| Prop | Type | Description |
| --- | --- | --- |
| `activeKind` | `"bokeh" \| "gaussian"` | The blur type shown. |
| `disabled` | `boolean` | Turn off the panel. |


---

# Blend

> Blend a second image into the photo.

```tsx
"use client";

import { ImageEditorProvider } from "photocn/react";

import { ImageEditorCanvas } from "@/components/image-editor/canvas";
import { ImageEditorBlend } from "@/components/image-editor/blend-panel";

export default function BlendDemo() {
  return (
    <ImageEditorProvider src="/samples/portrait.jpg">
      <div className="grid h-[520px] grid-rows-[1fr_auto] overflow-hidden rounded-xl border bg-background sm:grid-cols-[1fr_300px] sm:grid-rows-1">
        <ImageEditorCanvas className="min-h-0 p-2" />
        <aside className="max-h-60 overflow-y-auto border-t sm:max-h-none sm:border-t-0 sm:border-l">
          <ImageEditorBlend />
        </aside>
      </div>
    </ImageEditorProvider>
  );
}
```

## Installation

```bash
npx shadcn@latest add @photocn/image-editor-blend
```

## Usage

```tsx
import { ImageEditorBlend } from "@/components/image-editor/blend-panel";


```

## Props

| Prop | Type | Description |
| --- | --- | --- |
| `disabled` | `boolean` | Turn off the panel. |
| `onError` | `(error) => void` | Called when the second image can't be read. |


---

# Histogram

> A live RGB histogram of the edited photo.

```tsx
"use client";

import { ImageEditorProvider } from "photocn/react";

import { ImageEditorAdjustments } from "@/components/image-editor/adjustments-panel";
import { ImageEditorCanvas } from "@/components/image-editor/canvas";
import { ImageEditorHistogram } from "@/components/image-editor/histogram";

// Move a slider and watch the histogram follow (it's computed off-thread).
export default function HistogramDemo() {
  return (
    <ImageEditorProvider src="/samples/street.jpg">
      <div className="grid h-[520px] grid-rows-[1fr_auto] overflow-hidden rounded-xl border bg-background sm:grid-cols-[1fr_300px] sm:grid-rows-1">
        <ImageEditorCanvas className="min-h-0 p-2" />
        <aside className="max-h-64 overflow-y-auto border-t sm:max-h-none sm:border-t-0 sm:border-l">
          <ImageEditorHistogram className="border-b bg-muted/30 px-3 py-2" />
          <ImageEditorAdjustments />
        </aside>
      </div>
    </ImageEditorProvider>
  );
}
```

## Installation

```bash
npx shadcn@latest add @photocn/image-editor-histogram
```

## Usage

```tsx
import { ImageEditorHistogram } from "@/components/image-editor/histogram";


```

## Props

| Prop | Type | Description |
| --- | --- | --- |
| `showHeader` | `boolean` | Show the label and pixel count. Default `true`. |
| `className` | `string` | Classes for the outer element. |


---

# Metadata

> File details and EXIF data for the open image.

```tsx
"use client";

import { ImageEditorProvider } from "photocn/react";

import { ImageEditorCanvas } from "@/components/image-editor/canvas";
import { ImageEditorMetadata } from "@/components/image-editor/metadata-panel";

export default function MetadataDemo() {
  return (
    <ImageEditorProvider src="/samples/mountain-lake.jpg">
      <div className="grid h-[520px] grid-rows-[1fr_auto] overflow-hidden rounded-xl border bg-background sm:grid-cols-[1fr_300px] sm:grid-rows-1">
        <ImageEditorCanvas className="min-h-0 p-2" />
        <aside className="max-h-60 overflow-y-auto border-t sm:max-h-none sm:border-t-0 sm:border-l">
          <ImageEditorMetadata />
        </aside>
      </div>
    </ImageEditorProvider>
  );
}
```

## Installation

```bash
npx shadcn@latest add @photocn/image-editor-metadata
```

## Usage

```tsx
import { ImageEditorMetadata } from "@/components/image-editor/metadata-panel";


```

## Props

| Prop | Type | Description |
| --- | --- | --- |
| `title` | `ReactNode` | Heading. Default "Metadata". |
| `className` | `string` | Classes for the panel. |


---

# Export

> Choose the format, quality and size, then download or save.

```tsx
"use client";

import { ImageEditorProvider } from "photocn/react";

import { ImageEditorCanvas } from "@/components/image-editor/canvas";
import { ImageEditorExportDialog } from "@/components/image-editor/export-dialog";

export default function ExportDemo() {
  return (
    <ImageEditorProvider src="/samples/portrait.jpg">
      <div className="relative h-[420px] overflow-hidden rounded-xl border bg-background">
        <ImageEditorCanvas className="p-2" />
        <div className="absolute top-4 right-4">
          <ImageEditorExportDialog
            onSave={async ({ blob, filename }) => {
              // Upload it anywhere, e.g. await fetch("/api/photos", { method: "POST", body: blob })
              console.log("save", filename, blob.size);
            }}
          />
        </div>
      </div>
    </ImageEditorProvider>
  );
}
```

## Installation

```bash
npx shadcn@latest add @photocn/image-editor-export
```

## Usage

```tsx
import { ImageEditorExportDialog } from "@/components/image-editor/export-dialog";

<ImageEditorExportDialog onSave={async ({ blob }) => upload(blob)} />
```

## Props

| Prop | Type | Description |
| --- | --- | --- |
| `trigger` | `ReactNode` | The button that opens it. Default: an Export button. |
| `defaultFormat` | `"png" \| "jpeg" \| "webp"` | Default `"png"`. |
| `onSave` | `(result) => Promise<void>` | Adds a Save button, e.g. to upload the image. |
| `saveLabel` | `string` | Label for the Save button. |
| `hideDownload` | `boolean` | Hide the Download button. |
| `open / onOpenChange` | `boolean` | Control whether it's open. |


---

# Recipes

> Save a look as JSON and apply it to any photo.

```tsx
"use client";

import { ImageEditorProvider } from "photocn/react";

import { ImageEditorCanvas } from "@/components/image-editor/canvas";
import { ImageEditorFilters } from "@/components/image-editor/filters-panel";
import { ImageEditorRecipes } from "@/components/image-editor/recipes";

// Pick a filter, save the look as JSON, open another photo and load it back.
export default function RecipesDemo() {
  return (
    <ImageEditorProvider src="/samples/dog.jpg">
      <div className="grid h-[520px] grid-rows-[1fr_auto] overflow-hidden rounded-xl border bg-background sm:grid-cols-[1fr_300px] sm:grid-rows-1">
        <ImageEditorCanvas className="min-h-0 p-2" />
        <aside className="flex max-h-64 flex-col overflow-y-auto border-t sm:max-h-none sm:border-t-0 sm:border-l">
          <div className="flex items-center justify-between border-b px-4 py-2 text-sm font-medium">
            Recipe
            <ImageEditorRecipes />
          </div>
          <ImageEditorFilters />
        </aside>
      </div>
    </ImageEditorProvider>
  );
}
```

## Installation

```bash
npx shadcn@latest add @photocn/image-editor-recipes
```

## Usage

```tsx
import { ImageEditorRecipes } from "@/components/image-editor/recipes";


```

## Props

| Prop | Type | Description |
| --- | --- | --- |
| `onError` | `(error) => void` | Called when a recipe file can't be read. |
| `disabled` | `boolean` | Turn off the buttons. |


---

# Command Menu

> Press ⌘K to search tools, undo, redo and filters.

```tsx
"use client";

import { ImageEditorProvider } from "photocn/react";

import { Kbd } from "@/components/ui/kbd";
import { ImageEditorCanvas } from "@/components/image-editor/canvas";
import { ImageEditorCommandMenu } from "@/components/image-editor/command-menu";

export default function CommandMenuDemo() {
  return (
    <ImageEditorProvider src="/samples/strawberries.jpg">
      <div className="relative h-[420px] overflow-hidden rounded-xl border bg-background">
        <ImageEditorCanvas className="p-2" />
        <p className="absolute bottom-3 left-1/2 -translate-x-1/2 rounded-md bg-background/90 px-2 py-1 text-xs text-muted-foreground shadow-sm">
          Click the photo, then press <Kbd>⌘</Kbd> <Kbd>K</Kbd>
        </p>
        <ImageEditorCommandMenu />
      </div>
    </ImageEditorProvider>
  );
}
```

## Installation

```bash
npx shadcn@latest add @photocn/image-editor-command-menu
```

## Usage

```tsx
import { ImageEditorCommandMenu } from "@/components/image-editor/command-menu";


```

## Props

| Prop | Type | Description |
| --- | --- | --- |
| `shortcut` | `boolean` | Open it with ⌘K / Ctrl+K. Default `true`. |
| `groups` | `EditorCommandGroup[]` | Your own commands, added after the built-in ones. |
| `open / onOpenChange` | `boolean` | Control whether it's open. |


---

# Compact Layout

> The phone layout: the photo first, tools at the bottom, and controls in a sheet.

```tsx
"use client";

import { ImageEditor } from "@/components/image-editor/image-editor";

// The full editor forced into its phone layout. It switches automatically
// whenever the editor is narrower than 640px.
export default function CompactLayoutDemo() {
  return (
    <div className="mx-auto h-[640px] max-w-[390px] overflow-hidden rounded-[2rem] border-4 border-foreground/10 shadow-sm">
      <ImageEditor className="min-h-0" layout="compact" src="/samples/strawberries.jpg" />
    </div>
  );
}
```

## Installation

```bash
npx shadcn@latest add @photocn/image-editor-compact-layout
```

## Usage

```tsx
// The full editor switches to this layout below 640px wide.

```

## Props

| Prop | Type | Description |
| --- | --- | --- |
| `tools` | `EditorToolbarTool[]` | The tools in the bottom bar. |
| `panels` | `Record<toolId, ReactNode>` | Replace or add panels. |
| `panelMode` | `"drawer" \| "inline"` | Where panels open. Default: a sheet on phones, below the photo on wider screens. |
| `onSave` | `(result) => Promise<void>` | Adds a Save button to the export panel. |


---

# Layout

> The desktop layout: a toolbar on top and a resizable sidebar.

```tsx
"use client";

import { ImageEditorProvider } from "photocn/react";

import { ImageEditorCanvas } from "@/components/image-editor/canvas";
import { ImageEditorCurves } from "@/components/image-editor/curves-panel";
import { ImageEditorLayout } from "@/components/image-editor/layout";
import { ImageEditorRedoButton, ImageEditorUndoButton } from "@/components/image-editor/toolbar";

export default function LayoutDemo() {
  return (
    <ImageEditorProvider src="/samples/mountain-lake.jpg">
      <ImageEditorLayout
        className="h-[520px] overflow-hidden rounded-xl border"
        sidebar={<ImageEditorCurves />}
        sidebarDefaultSize="300px"
        toolbar={
          <>
            <ImageEditorUndoButton />
            <ImageEditorRedoButton />
          </>
        }
      >
        <ImageEditorCanvas />
      </ImageEditorLayout>
    </ImageEditorProvider>
  );
}
```

## Installation

```bash
npx shadcn@latest add @photocn/image-editor-layout
```

## Usage

```tsx
import { ImageEditorLayout } from "@/components/image-editor/layout";

<ImageEditorLayout toolbar={...} sidebar={<ImageEditorCurves />}>
  <ImageEditorCanvas />

```

## Props

| Prop | Type | Description |
| --- | --- | --- |
| `toolbar` | `ReactNode` | Content of the top bar. |
| `sidebar` | `ReactNode` | Content of the sidebar. |
| `resizableSidebar` | `boolean` | Let users drag to resize the sidebar. Default `true`. |
| `sidebarDefaultSize / sidebarMinSize / sidebarMaxSize` | `string \| number` | Sidebar sizes, e.g. `"338px"`. |
