# Image Cropper

URL: https://ark-ui.com/docs/components/image-cropper
LLM: https://ark-ui.com/llms.txt/components/image-cropper

Crop and transform images with zoom, rotation, and aspect ratio controls.

---



## Anatomy



```tsx
<ImageCropper.Root>
  <ImageCropper.Viewport>
    <ImageCropper.Image />
    <ImageCropper.Selection>
      <ImageCropper.Handle />
      <ImageCropper.Grid />
    </ImageCropper.Selection>
  </ImageCropper.Viewport>
</ImageCropper.Root>
```

## Examples

### Basic

Set up a basic image cropper. Drag the handles to resize the selection, or drag inside to pan the image.

```tsx
import { ImageCropper } from '@ark-ui/react/image-cropper'
import styles from 'styles/image-cropper.module.css'

export const Basic = () => {
  return (
    <div className={styles.Layout}>
      <ImageCropper.Root className={styles.Root}>
        <ImageCropper.Viewport className={styles.Viewport}>
          <ImageCropper.Image
            className={styles.Image}
            src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800"
            alt="Sample"
          />
          <ImageCropper.Selection className={styles.Selection}>
            {ImageCropper.handles.map((position) => (
              <ImageCropper.Handle className={styles.Handle} key={position} position={position}>
                <div />
              </ImageCropper.Handle>
            ))}
            <ImageCropper.Grid className={styles.Grid} axis="horizontal" />
            <ImageCropper.Grid className={styles.Grid} axis="vertical" />
          </ImageCropper.Selection>
        </ImageCropper.Viewport>
      </ImageCropper.Root>
    </div>
  )
}
```

### Aspect Ratio

Lock the crop area to a specific aspect ratio. Use the `aspectRatio` prop—pass a number like `16/9` for widescreen or
`1` for square.

```tsx
import { ImageCropper } from '@ark-ui/react/image-cropper'
import { RectangleHorizontalIcon, SquareIcon, RectangleVerticalIcon } from 'lucide-react'
import { useState } from 'react'
import button from 'styles/button.module.css'
import styles from 'styles/image-cropper.module.css'

const aspects = [
  { label: '16:9', value: 16 / 9, icon: RectangleHorizontalIcon },
  { label: '1:1', value: 1, icon: SquareIcon },
  { label: '9:16', value: 9 / 16, icon: RectangleVerticalIcon },
]

export const AspectRatio = () => {
  const [aspectRatio, setAspectRatio] = useState(16 / 9)

  return (
    <div className={styles.Layout}>
      <div className={button.Group}>
        {aspects.map((aspect) => (
          <button
            key={aspect.label}
            className={button.Root}
            data-variant={aspectRatio === aspect.value ? 'solid' : undefined}
            onClick={() => setAspectRatio(aspect.value)}
          >
            <aspect.icon />
            {aspect.label}
          </button>
        ))}
      </div>

      <ImageCropper.Root className={styles.Root} aspectRatio={aspectRatio}>
        <ImageCropper.Viewport className={styles.Viewport}>
          <ImageCropper.Image
            className={styles.Image}
            src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800"
            alt="Sample"
          />
          <ImageCropper.Selection className={styles.Selection}>
            {ImageCropper.handles.map((position) => (
              <ImageCropper.Handle className={styles.Handle} key={position} position={position}>
                <div />
              </ImageCropper.Handle>
            ))}
            <ImageCropper.Grid className={styles.Grid} axis="horizontal" />
            <ImageCropper.Grid className={styles.Grid} axis="vertical" />
          </ImageCropper.Selection>
        </ImageCropper.Viewport>
      </ImageCropper.Root>
    </div>
  )
}
```

### Circle Crop

Use `cropShape="circle"` for profile pictures or avatars. The selection becomes a circle instead of a rectangle.

```tsx
import { ImageCropper } from '@ark-ui/react/image-cropper'
import styles from 'styles/image-cropper.module.css'

export const Circle = () => {
  return (
    <div className={styles.Layout}>
      <ImageCropper.Root className={styles.Root} cropShape="circle">
        <ImageCropper.Viewport className={styles.Viewport}>
          <ImageCropper.Image
            className={styles.Image}
            src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800"
            alt="Sample"
          />
          <ImageCropper.Selection className={styles.Selection}>
            {ImageCropper.handles.map((position) => (
              <ImageCropper.Handle className={styles.Handle} key={position} position={position}>
                <div />
              </ImageCropper.Handle>
            ))}
            <ImageCropper.Grid className={styles.Grid} axis="horizontal" />
            <ImageCropper.Grid className={styles.Grid} axis="vertical" />
          </ImageCropper.Selection>
        </ImageCropper.Viewport>
      </ImageCropper.Root>
    </div>
  )
}
```

### Initial Crop

Start with a pre-defined crop area using the `initialCrop` prop. Pass an object with `x`, `y`, `width`, and `height` in
pixels.

```tsx
import { ImageCropper } from '@ark-ui/react/image-cropper'
import styles from 'styles/image-cropper.module.css'

export const InitialCrop = () => {
  return (
    <div className={styles.Layout}>
      <p className={styles.Description}>Starts with a pre-defined crop area</p>

      <ImageCropper.Root className={styles.Root} initialCrop={{ x: 50, y: 30, width: 200, height: 120 }}>
        <ImageCropper.Viewport className={styles.Viewport}>
          <ImageCropper.Image
            className={styles.Image}
            src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800"
            alt="Sample"
          />
          <ImageCropper.Selection className={styles.Selection}>
            {ImageCropper.handles.map((position) => (
              <ImageCropper.Handle className={styles.Handle} key={position} position={position}>
                <div />
              </ImageCropper.Handle>
            ))}
            <ImageCropper.Grid className={styles.Grid} axis="horizontal" />
            <ImageCropper.Grid className={styles.Grid} axis="vertical" />
          </ImageCropper.Selection>
        </ImageCropper.Viewport>
      </ImageCropper.Root>
    </div>
  )
}
```

### Controlled Zoom

Control zoom programmatically with the `zoom` and `onZoomChange` props. Useful when you want external buttons to zoom in
and out.

```tsx
import { ImageCropper } from '@ark-ui/react/image-cropper'
import { ZoomInIcon, ZoomOutIcon } from 'lucide-react'
import { useState } from 'react'
import button from 'styles/button.module.css'
import styles from 'styles/image-cropper.module.css'

export const ControlledZoom = () => {
  const [zoom, setZoom] = useState(1)

  return (
    <div className={styles.Layout}>
      <div className={button.Group}>
        <button className={button.Root} onClick={() => setZoom(zoom - 0.1)}>
          <ZoomOutIcon />
        </button>
        <span style={{ fontSize: '0.875rem', minWidth: '3rem', textAlign: 'center' }}>{zoom.toFixed(1)}x</span>
        <button className={button.Root} onClick={() => setZoom(zoom + 0.1)}>
          <ZoomInIcon />
        </button>
      </div>

      <ImageCropper.Root className={styles.Root} zoom={zoom} onZoomChange={(e) => setZoom(e.zoom)}>
        <ImageCropper.Viewport className={styles.Viewport}>
          <ImageCropper.Image
            className={styles.Image}
            src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800"
            alt="Sample"
          />
          <ImageCropper.Selection className={styles.Selection}>
            {ImageCropper.handles.map((position) => (
              <ImageCropper.Handle className={styles.Handle} key={position} position={position}>
                <div />
              </ImageCropper.Handle>
            ))}
            <ImageCropper.Grid className={styles.Grid} axis="horizontal" />
            <ImageCropper.Grid className={styles.Grid} axis="vertical" />
          </ImageCropper.Selection>
        </ImageCropper.Viewport>
      </ImageCropper.Root>
    </div>
  )
}
```

### Zoom Limits

Set `minZoom` and `maxZoom` to constrain how far users can zoom. Prevents over-zooming or zooming out past the image
bounds.

```tsx
import { ImageCropper } from '@ark-ui/react/image-cropper'
import { ZoomInIcon, ZoomOutIcon } from 'lucide-react'
import { useState } from 'react'
import button from 'styles/button.module.css'
import styles from 'styles/image-cropper.module.css'

export const ZoomLimits = () => {
  const [zoom, setZoom] = useState(1)
  const minZoom = 0.5
  const maxZoom = 2

  return (
    <div className={styles.Layout}>
      <div className={button.Group}>
        <button className={button.Root} onClick={() => setZoom(Math.max(minZoom, zoom - 0.1))}>
          <ZoomOutIcon />
        </button>
        <span style={{ fontSize: '0.875rem', padding: '0 0.5rem', minWidth: '3rem', textAlign: 'center' }}>
          {zoom.toFixed(1)}x
        </span>
        <button className={button.Root} onClick={() => setZoom(Math.min(maxZoom, zoom + 0.1))}>
          <ZoomInIcon />
        </button>
      </div>

      <p className={styles.Description}>
        Zoom constrained between {minZoom}x and {maxZoom}x
      </p>

      <ImageCropper.Root
        className={styles.Root}
        zoom={zoom}
        onZoomChange={(e) => setZoom(e.zoom)}
        minZoom={minZoom}
        maxZoom={maxZoom}
      >
        <ImageCropper.Viewport className={styles.Viewport}>
          <ImageCropper.Image
            className={styles.Image}
            src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800"
            alt="Sample"
          />
          <ImageCropper.Selection className={styles.Selection}>
            {ImageCropper.handles.map((position) => (
              <ImageCropper.Handle className={styles.Handle} key={position} position={position}>
                <div />
              </ImageCropper.Handle>
            ))}
            <ImageCropper.Grid className={styles.Grid} axis="horizontal" />
            <ImageCropper.Grid className={styles.Grid} axis="vertical" />
          </ImageCropper.Selection>
        </ImageCropper.Viewport>
      </ImageCropper.Root>
    </div>
  )
}
```

### Rotation

Rotate the image with the `rotation` and `onRotationChange` props. Values are in degrees—common increments are 90
or 180.

```tsx
import { ImageCropper } from '@ark-ui/react/image-cropper'
import { RotateCcwIcon, RotateCwIcon } from 'lucide-react'
import { useState } from 'react'
import button from 'styles/button.module.css'
import styles from 'styles/image-cropper.module.css'

export const Rotation = () => {
  const [rotation, setRotation] = useState(0)

  return (
    <div className={styles.Layout}>
      <div className={button.Group}>
        <button className={button.Root} onClick={() => setRotation(rotation - 90)}>
          <RotateCcwIcon />
        </button>
        <button className={button.Root} onClick={() => setRotation(rotation + 90)}>
          <RotateCwIcon />
        </button>
        <span style={{ fontSize: '0.875rem', padding: '0 0.5rem' }}>{rotation}°</span>
      </div>

      <ImageCropper.Root className={styles.Root} rotation={rotation} onRotationChange={(e) => setRotation(e.rotation)}>
        <ImageCropper.Viewport className={styles.Viewport}>
          <ImageCropper.Image
            className={styles.Image}
            src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800"
            alt="Sample"
          />
          <ImageCropper.Selection className={styles.Selection}>
            {ImageCropper.handles.map((position) => (
              <ImageCropper.Handle className={styles.Handle} key={position} position={position}>
                <div />
              </ImageCropper.Handle>
            ))}
            <ImageCropper.Grid className={styles.Grid} axis="horizontal" />
            <ImageCropper.Grid className={styles.Grid} axis="vertical" />
          </ImageCropper.Selection>
        </ImageCropper.Viewport>
      </ImageCropper.Root>
    </div>
  )
}
```

### Flip

Flip the image horizontally or vertically using the `flip` prop. Pass an object with `horizontal` and `vertical`
booleans.

```tsx
import { ImageCropper } from '@ark-ui/react/image-cropper'
import { FlipHorizontalIcon, FlipVerticalIcon } from 'lucide-react'
import { useState } from 'react'
import button from 'styles/button.module.css'
import styles from 'styles/image-cropper.module.css'

export const Flip = () => {
  const [flip, setFlip] = useState({ horizontal: false, vertical: false })

  return (
    <div className={styles.Layout}>
      <div className={button.Group}>
        <button
          className={button.Root}
          data-variant={flip.horizontal ? 'solid' : undefined}
          onClick={() => setFlip({ ...flip, horizontal: !flip.horizontal })}
        >
          <FlipHorizontalIcon />
          Horizontal
        </button>
        <button
          className={button.Root}
          data-variant={flip.vertical ? 'solid' : undefined}
          onClick={() => setFlip({ ...flip, vertical: !flip.vertical })}
        >
          <FlipVerticalIcon />
          Vertical
        </button>
      </div>

      <ImageCropper.Root className={styles.Root} flip={flip} onFlipChange={(e) => setFlip(e.flip)}>
        <ImageCropper.Viewport className={styles.Viewport}>
          <ImageCropper.Image
            className={styles.Image}
            src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800"
            alt="Sample"
          />
          <ImageCropper.Selection className={styles.Selection}>
            {ImageCropper.handles.map((position) => (
              <ImageCropper.Handle className={styles.Handle} key={position} position={position}>
                <div />
              </ImageCropper.Handle>
            ))}
            <ImageCropper.Grid className={styles.Grid} axis="horizontal" />
            <ImageCropper.Grid className={styles.Grid} axis="vertical" />
          </ImageCropper.Selection>
        </ImageCropper.Viewport>
      </ImageCropper.Root>
    </div>
  )
}
```

### Min and Max Size

Constrain the crop area size with `minWidth`, `minHeight`, `maxWidth`, and `maxHeight`. Keeps the selection within
sensible bounds.

```tsx
import { ImageCropper } from '@ark-ui/react/image-cropper'
import styles from 'styles/image-cropper.module.css'

export const MinMaxSize = () => {
  return (
    <div className={styles.Layout}>
      <p className={styles.Description}>Crop area constrained to min 80px and max 200px</p>

      <ImageCropper.Root className={styles.Root} minWidth={80} minHeight={80} maxWidth={200} maxHeight={200}>
        <ImageCropper.Viewport className={styles.Viewport}>
          <ImageCropper.Image
            className={styles.Image}
            src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800"
            alt="Sample"
          />
          <ImageCropper.Selection className={styles.Selection}>
            {ImageCropper.handles.map((position) => (
              <ImageCropper.Handle className={styles.Handle} key={position} position={position}>
                <div />
              </ImageCropper.Handle>
            ))}
            <ImageCropper.Grid className={styles.Grid} axis="horizontal" />
            <ImageCropper.Grid className={styles.Grid} axis="vertical" />
          </ImageCropper.Selection>
        </ImageCropper.Viewport>
      </ImageCropper.Root>
    </div>
  )
}
```

### Fixed Crop Area

Set `fixedCropArea` to `true` when the crop area should stay fixed while the image moves underneath. Useful for
overlay-style cropping.

```tsx
import { ImageCropper } from '@ark-ui/react/image-cropper'
import styles from 'styles/image-cropper.module.css'

export const Fixed = () => {
  return (
    <div className={styles.Layout}>
      <ImageCropper.Root className={styles.Root} fixedCropArea>
        <ImageCropper.Viewport className={styles.Viewport}>
          <ImageCropper.Image
            className={styles.Image}
            src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800"
            alt="Sample"
          />
          <ImageCropper.Selection className={styles.Selection}>
            <ImageCropper.Grid className={styles.Grid} axis="horizontal" />
            <ImageCropper.Grid className={styles.Grid} axis="vertical" />
          </ImageCropper.Selection>
        </ImageCropper.Viewport>
      </ImageCropper.Root>
    </div>
  )
}
```

### Crop Preview

Use `getCroppedImage()` from the context to get the cropped result. Call it with `{ output: 'dataUrl' }` for a base64
string you can use in an `img` src.

```tsx
import { ImageCropper, useImageCropper } from '@ark-ui/react/image-cropper'
import { CropIcon } from 'lucide-react'
import { useState } from 'react'
import button from 'styles/button.module.css'
import styles from 'styles/image-cropper.module.css'

export const CropPreview = () => {
  const imageCropper = useImageCropper()
  const [preview, setPreview] = useState<string | null>(null)

  const handleCrop = async () => {
    const result = await imageCropper.getCroppedImage({ output: 'dataUrl' })
    if (typeof result === 'string') {
      setPreview(result)
    }
  }

  return (
    <div className={styles.Layout}>
      <div className={button.Group}>
        <button className={button.Root} data-variant="solid" onClick={handleCrop}>
          <CropIcon />
          Crop Image
        </button>
      </div>

      <ImageCropper.RootProvider className={styles.Root} value={imageCropper}>
        <ImageCropper.Viewport className={styles.Viewport}>
          <ImageCropper.Image
            className={styles.Image}
            src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800"
            alt="Sample"
            crossOrigin="anonymous"
          />
          <ImageCropper.Selection className={styles.Selection}>
            {ImageCropper.handles.map((position) => (
              <ImageCropper.Handle className={styles.Handle} key={position} position={position}>
                <div />
              </ImageCropper.Handle>
            ))}
            <ImageCropper.Grid className={styles.Grid} axis="horizontal" />
            <ImageCropper.Grid className={styles.Grid} axis="vertical" />
          </ImageCropper.Selection>
        </ImageCropper.Viewport>
      </ImageCropper.RootProvider>

      <div className={styles.Preview}>
        <span className={styles.PreviewLabel}>Preview</span>
        {preview && <img src={preview} alt="Cropped preview" className={styles.PreviewImage} />}
      </div>
    </div>
  )
}
```

### Reset

The context exposes a `reset()` method that restores the image to its initial state. Handy for an "undo" or "start over"
button.

```tsx
import { ImageCropper, useImageCropper } from '@ark-ui/react/image-cropper'
import { FlipHorizontalIcon, RotateCwIcon, RotateCcwIcon, RefreshCwIcon, ZoomInIcon, ZoomOutIcon } from 'lucide-react'
import button from 'styles/button.module.css'
import styles from 'styles/image-cropper.module.css'

export const Reset = () => {
  const imageCropper = useImageCropper()

  return (
    <div className={styles.Layout}>
      <div className={button.Group}>
        <button className={button.Root} onClick={() => imageCropper.zoomBy(-0.1)}>
          <ZoomOutIcon />
        </button>
        <button className={button.Root} onClick={() => imageCropper.zoomBy(0.1)}>
          <ZoomInIcon />
        </button>
        <button className={button.Root} onClick={() => imageCropper.rotateBy(-90)}>
          <RotateCcwIcon />
        </button>
        <button className={button.Root} onClick={() => imageCropper.rotateBy(90)}>
          <RotateCwIcon />
        </button>
        <button className={button.Root} onClick={() => imageCropper.flipHorizontally()}>
          <FlipHorizontalIcon />
        </button>
        <button className={button.Root} data-variant="surface" onClick={() => imageCropper.reset()}>
          <RefreshCwIcon />
          Reset
        </button>
      </div>

      <ImageCropper.RootProvider className={styles.Root} value={imageCropper}>
        <ImageCropper.Viewport className={styles.Viewport}>
          <ImageCropper.Image
            className={styles.Image}
            src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800"
            alt="Sample"
          />
          <ImageCropper.Selection className={styles.Selection}>
            {ImageCropper.handles.map((position) => (
              <ImageCropper.Handle className={styles.Handle} key={position} position={position}>
                <div />
              </ImageCropper.Handle>
            ))}
            <ImageCropper.Grid className={styles.Grid} axis="horizontal" />
            <ImageCropper.Grid className={styles.Grid} axis="vertical" />
          </ImageCropper.Selection>
        </ImageCropper.Viewport>
      </ImageCropper.RootProvider>
    </div>
  )
}
```

### Events

Listen to `onCropChange` and `onZoomChange` to track crop position and zoom level. Use these to sync with external state
or show live previews.

```tsx
import { ImageCropper } from '@ark-ui/react/image-cropper'
import { useState } from 'react'
import styles from 'styles/image-cropper.module.css'

export const Events = () => {
  const [cropData, setCropData] = useState({ x: 0, y: 0, width: 0, height: 0 })
  const [zoom, setZoom] = useState(1)

  return (
    <div className={styles.Layout}>
      <ImageCropper.Root
        className={styles.Root}
        onCropChange={(e) => setCropData(e.crop)}
        onZoomChange={(e) => setZoom(e.zoom)}
      >
        <ImageCropper.Viewport className={styles.Viewport}>
          <ImageCropper.Image
            className={styles.Image}
            src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800"
            alt="Sample"
          />
          <ImageCropper.Selection className={styles.Selection}>
            {ImageCropper.handles.map((position) => (
              <ImageCropper.Handle className={styles.Handle} key={position} position={position}>
                <div />
              </ImageCropper.Handle>
            ))}
            <ImageCropper.Grid className={styles.Grid} axis="horizontal" />
            <ImageCropper.Grid className={styles.Grid} axis="vertical" />
          </ImageCropper.Selection>
        </ImageCropper.Viewport>
      </ImageCropper.Root>

      <div className={styles.DataDisplay}>
        <div className={styles.DataItem}>
          <span className={styles.DataLabel}>Zoom</span>
          <span className={styles.DataValue}>{zoom.toFixed(2)}x</span>
        </div>
        <div className={styles.DataItem}>
          <span className={styles.DataLabel}>Position</span>
          <span className={styles.DataValue}>
            {Math.round(cropData.x)}, {Math.round(cropData.y)}
          </span>
        </div>
        <div className={styles.DataItem}>
          <span className={styles.DataLabel}>Size</span>
          <span className={styles.DataValue}>
            {Math.round(cropData.width)} × {Math.round(cropData.height)}
          </span>
        </div>
      </div>
    </div>
  )
}
```

### Context

Use `ImageCropper.Context` to access the cropper API from anywhere inside the root. You get methods like `zoomBy`,
`rotateBy`, and `setZoom`.

```tsx
import { ImageCropper } from '@ark-ui/react/image-cropper'
import { ZoomInIcon, ZoomOutIcon } from 'lucide-react'
import button from 'styles/button.module.css'
import styles from 'styles/image-cropper.module.css'

export const Context = () => {
  return (
    <div className={styles.Layout}>
      <ImageCropper.Root className={styles.Root}>
        <ImageCropper.Context>
          {(context) => (
            <div className={button.Group}>
              <button className={button.Root} onClick={() => context.zoomBy(-0.1)}>
                <ZoomOutIcon />
              </button>
              <span style={{ fontSize: '0.875rem', padding: '0 0.5rem', minWidth: '3rem', textAlign: 'center' }}>
                {context.zoom.toFixed(1)}x
              </span>
              <button className={button.Root} onClick={() => context.zoomBy(0.1)}>
                <ZoomInIcon />
              </button>
            </div>
          )}
        </ImageCropper.Context>

        <ImageCropper.Viewport className={styles.Viewport}>
          <ImageCropper.Image
            className={styles.Image}
            src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800"
            alt="Sample"
          />
          <ImageCropper.Selection className={styles.Selection}>
            {ImageCropper.handles.map((position) => (
              <ImageCropper.Handle className={styles.Handle} key={position} position={position}>
                <div />
              </ImageCropper.Handle>
            ))}
            <ImageCropper.Grid className={styles.Grid} axis="horizontal" />
            <ImageCropper.Grid className={styles.Grid} axis="vertical" />
          </ImageCropper.Selection>
        </ImageCropper.Viewport>
      </ImageCropper.Root>
    </div>
  )
}
```

### Root Provider

Use `RootProvider` with `useImageCropper` when you need to control the cropper from outside the component tree. Build
custom toolbars or integrate with form state.

```tsx
import { ImageCropper, useImageCropper } from '@ark-ui/react/image-cropper'
import { ZoomInIcon, ZoomOutIcon } from 'lucide-react'
import button from 'styles/button.module.css'
import styles from 'styles/image-cropper.module.css'

export const RootProvider = () => {
  const imageCropper = useImageCropper()

  return (
    <div className={styles.Layout}>
      <div className={button.Group}>
        <button className={button.Root} onClick={() => imageCropper.setZoom(imageCropper.zoom - 0.1)}>
          <ZoomOutIcon />
        </button>
        <span style={{ fontSize: '0.875rem', minWidth: '3rem', textAlign: 'center' }}>
          {imageCropper.zoom.toFixed(1)}x
        </span>
        <button className={button.Root} onClick={() => imageCropper.setZoom(imageCropper.zoom + 0.1)}>
          <ZoomInIcon />
        </button>
      </div>

      <ImageCropper.RootProvider className={styles.Root} value={imageCropper}>
        <ImageCropper.Viewport className={styles.Viewport}>
          <ImageCropper.Image
            className={styles.Image}
            src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800"
            alt="Sample"
          />
          <ImageCropper.Selection className={styles.Selection}>
            {ImageCropper.handles.map((position) => (
              <ImageCropper.Handle className={styles.Handle} key={position} position={position}>
                <div />
              </ImageCropper.Handle>
            ))}
            <ImageCropper.Grid className={styles.Grid} axis="horizontal" />
            <ImageCropper.Grid className={styles.Grid} axis="vertical" />
          </ImageCropper.Selection>
        </ImageCropper.Viewport>
      </ImageCropper.RootProvider>
    </div>
  )
}
```

## API Reference

### Props

### Root

#### Props

**`asChild`**
Type: `boolean`
Required: false
Default Value: `undefined`
Description: Use the provided child element as the default rendered element, combining their props and behavior.

**`aspectRatio`**
Type: `number`
Required: false
Default Value: `undefined`
Description: The aspect ratio to maintain for the crop area (width / height).
For example, an aspect ratio of 16 / 9 will maintain a width to height ratio of 16:9.
If not provided, the crop area can be freely resized.

**`cropShape`**
Type: `'circle' | 'rectangle'`
Required: false
Default Value: `"rectangle"`
Description: The shape of the crop area.

**`defaultFlip`**
Type: `FlipState`
Required: false
Default Value: `{ horizontal: false, vertical: false }`
Description: The initial flip state to apply to the image.

**`defaultRotation`**
Type: `number`
Required: false
Default Value: `0`
Description: The initial rotation to apply to the image in degrees.

**`defaultZoom`**
Type: `number`
Required: false
Default Value: `1`
Description: The initial zoom factor to apply to the image.

**`fixedCropArea`**
Type: `boolean`
Required: false
Default Value: `false`
Description: Whether the crop area is fixed in size and position.

**`flip`**
Type: `FlipState`
Required: false
Default Value: `undefined`
Description: The controlled flip state of the image.

**`ids`**
Type: `Partial<{
  root: string
  viewport: string
  image: string
  selection: string
  handle: (position: string) => string
}>`
Required: false
Default Value: `undefined`
Description: The ids of the image cropper elements

**`initialCrop`**
Type: `Rect`
Required: false
Default Value: `undefined`
Description: The initial rectangle of the crop area.
If not provided, a smart default will be computed based on viewport size and aspect ratio.

**`maxHeight`**
Type: `number`
Required: false
Default Value: `Infinity`
Description: The maximum height of the crop area

**`maxWidth`**
Type: `number`
Required: false
Default Value: `Infinity`
Description: The maximum width of the crop area

**`maxZoom`**
Type: `number`
Required: false
Default Value: `5`
Description: The maximum zoom factor allowed.

**`minHeight`**
Type: `number`
Required: false
Default Value: `40`
Description: The minimum height of the crop area

**`minWidth`**
Type: `number`
Required: false
Default Value: `40`
Description: The minimum width of the crop area

**`minZoom`**
Type: `number`
Required: false
Default Value: `1`
Description: The minimum zoom factor allowed.

**`nudgeStep`**
Type: `number`
Required: false
Default Value: `1`
Description: The base nudge step for keyboard arrow keys (in pixels).

**`nudgeStepCtrl`**
Type: `number`
Required: false
Default Value: `50`
Description: The nudge step when Ctrl/Cmd key is held (in pixels).

**`nudgeStepShift`**
Type: `number`
Required: false
Default Value: `10`
Description: The nudge step when Shift key is held (in pixels).

**`onCropChange`**
Type: `(details: CropChangeDetails) => void`
Required: false
Default Value: `undefined`
Description: Callback fired when the crop area changes.

**`onFlipChange`**
Type: `(details: FlipChangeDetails) => void`
Required: false
Default Value: `undefined`
Description: Callback fired when the flip state changes.

**`onRotationChange`**
Type: `(details: RotationChangeDetails) => void`
Required: false
Default Value: `undefined`
Description: Callback fired when the rotation changes.

**`onZoomChange`**
Type: `(details: ZoomChangeDetails) => void`
Required: false
Default Value: `undefined`
Description: Callback fired when the zoom level changes.

**`rotation`**
Type: `number`
Required: false
Default Value: `undefined`
Description: The controlled rotation of the image in degrees (0 - 360).

**`translations`**
Type: `IntlTranslations`
Required: false
Default Value: `undefined`
Description: Specifies the localized strings that identify accessibility elements and their states.

**`zoom`**
Type: `number`
Required: false
Default Value: `undefined`
Description: The controlled zoom level of the image.

**`zoomSensitivity`**
Type: `number`
Required: false
Default Value: `2`
Description: Controls how responsive pinch-to-zoom is.

**`zoomStep`**
Type: `number`
Required: false
Default Value: `0.1`
Description: The amount of zoom applied per wheel step.

#### Data Attributes

**`data-scope`**: image-cropper
**`data-part`**: root
**`data-fixed`**: 
**`data-shape`**: 
**`data-pinch`**: 
**`data-dragging`**: Present when in the dragging state
**`data-panning`**: 

### Grid

#### Props

**`axis`**
Type: `'horizontal' | 'vertical'`
Required: true
Default Value: `undefined`
Description: The axis of the grid lines to display

**`asChild`**
Type: `boolean`
Required: false
Default Value: `undefined`
Description: Use the provided child element as the default rendered element, combining their props and behavior.

#### Data Attributes

**`data-scope`**: image-cropper
**`data-part`**: grid
**`data-axis`**: The axis to resize
**`data-dragging`**: Present when in the dragging state
**`data-panning`**: 

### Handle

#### Props

**`position`**
Type: `HandlePosition`
Required: true
Default Value: `undefined`
Description: The position of the handle

**`asChild`**
Type: `boolean`
Required: false
Default Value: `undefined`
Description: Use the provided child element as the default rendered element, combining their props and behavior.

#### Data Attributes

**`data-scope`**: image-cropper
**`data-part`**: handle
**`data-position`**: 
**`data-disabled`**: Present when disabled

### Image

#### Props

**`asChild`**
Type: `boolean`
Required: false
Default Value: `undefined`
Description: Use the provided child element as the default rendered element, combining their props and behavior.

#### Data Attributes

**`data-scope`**: image-cropper
**`data-part`**: image
**`data-ready`**: 
**`data-flip-horizontal`**: 
**`data-flip-vertical`**: 

### RootProvider

#### Props

**`value`**
Type: `UseImageCropperReturn`
Required: true
Default Value: `undefined`
Description: undefined

**`asChild`**
Type: `boolean`
Required: false
Default Value: `undefined`
Description: Use the provided child element as the default rendered element, combining their props and behavior.

### Selection

#### Props

**`asChild`**
Type: `boolean`
Required: false
Default Value: `undefined`
Description: Use the provided child element as the default rendered element, combining their props and behavior.

#### Data Attributes

**`data-scope`**: image-cropper
**`data-part`**: selection
**`data-disabled`**: Present when disabled
**`data-shape`**: 
**`data-measured`**: 
**`data-dragging`**: Present when in the dragging state
**`data-panning`**: 

### Viewport

#### Props

**`asChild`**
Type: `boolean`
Required: false
Default Value: `undefined`
Description: Use the provided child element as the default rendered element, combining their props and behavior.

#### Data Attributes

**`data-scope`**: image-cropper
**`data-part`**: viewport
**`data-disabled`**: Present when disabled

### Context

**API:**

| Property | Type | Description |
|----------|------|-------------|
| `zoom` | `number` | The current zoom level of the image. |
| `rotation` | `number` | The current rotation of the image in degrees. |
| `flip` | `FlipState` | The current flip state of the image. |
| `crop` | `Rect` | The current crop area rectangle in viewport coordinates. |
| `offset` | `Point` | The current offset (pan position) of the image. |
| `naturalSize` | `Size` | The natural (original) size of the image. |
| `viewportRect` | `BoundingRect` | The viewport rectangle dimensions and position. |
| `dragging` | `boolean` | Whether the crop area is currently being dragged. |
| `panning` | `boolean` | Whether the image is currently being panned. |
| `setZoom` | `(zoom: number) => void` | Function to set the zoom level of the image. |
| `zoomBy` | `(delta: number) => void` | Function to zoom the image by a relative amount. |
| `setRotation` | `(rotation: number) => void` | Function to set the rotation of the image. |
| `rotateBy` | `(degrees: number) => void` | Function to rotate the image by a relative amount in degrees. |
| `setFlip` | `(flip: Partial<FlipState>) => void` | Function to set the flip state of the image. |
| `flipHorizontally` | `(value?: boolean) => void` | Function to flip the image horizontally. Pass a boolean to set explicitly or omit to toggle. |
| `flipVertically` | `(value?: boolean) => void` | Function to flip the image vertically. Pass a boolean to set explicitly or omit to toggle. |
| `resize` | `(handlePosition: HandlePosition, delta: number) => void` | Function to resize the crop area from a handle programmatically. |
| `reset` | `() => void` | Function to reset the cropper to its initial state. |
| `getCroppedImage` | `(options?: GetCroppedImageOptions) => Promise<Blob | string | null>` | Function to get the cropped image with all transformations applied.
Returns a Promise that resolves to either a Blob or data URL. |
| `getCropData` | `() => CropData` | Function to get the crop geometry in natural image pixels.
The rect is axis-aligned; `corners` preserves the exact source quad. |
