npm package: @origin-space/image-cropper
Description: A headless React component for image cropping
Author: origin-space
Latest version: 0.1.9

# React Image Cropper

[![npm version](https://badge.fury.io/js/%40origin%2Fimage-cropper.svg)](https://badge.fury.io/js/%40origin%2Fimage-cropper)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

A simple, composable, headless React component for interactive image cropping — inspired by the experience on <a href="https://x.com" target="_blank">X</a>.

![React Image Cropper](https://raw.githubusercontent.com/origin-space/image-cropper/HEAD/media/cropper.png)

## Features

*   **Headless & Composable:** Provides building blocks (`Cropper.Root`, `Cropper.Image`, `Cropper.CropArea`, `Cropper.Description`) for full control over structure and styling.
*   **Interactive:** Supports zooming (mouse wheel, pinch gesture) and panning (mouse drag, touch drag, arrow keys).
*   **Aspect Ratio:** Enforces a specified aspect ratio for the crop area.
*   **Controlled/Uncontrolled:** Manage zoom state internally or control it via props.
*   **Crop Calculation:** Outputs precise pixel coordinates of the cropped area relative to the original image.
*   **Accessible:** Designed with ARIA attributes and requires a description element for screen reader users.
*   **Customizable:** Control zoom limits, sensitivity, padding, keyboard steps, and apply custom styles.

## Installation

```bash
npm install @origin/image-cropper
# or
yarn add @origin/image-cropper
# or
pnpm add @origin/image-cropper
```

## Usage

Here's a basic example of how to use the `Cropper` primitive components:

```tsx
import { Cropper } from "@origin-space/image-cropper"
import React from "react"

function MyImageCropper() {
  const [cropData, setCropData] = React.useState<Area | null>(null)

  return (
    <div>
      <Cropper.Root
        image="https://images.unsplash.com/photo-1494790108377-be9c29b29330"
        aspectRatio={1}
        onCropChange={setCropData}
        className="relative flex h-80 w-full cursor-move touch-none items-center justify-center overflow-hidden rounded-md border focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
      >
        {/* Required for accessibility */}
        <Cropper.Description className="sr-only" />
        <Cropper.Image className="pointer-events-none h-full w-full select-none object-cover" />
        <Cropper.CropArea className="pointer-events-none absolute border-2 border-dashed border-background shadow-[0_0_0_9999px_rgba(0,0,0,0.6)]" />
      </Cropper.Root>

      {cropData && (
        <pre className="mt-4 overflow-auto rounded bg-muted p-2 text-sm">
          {JSON.stringify(cropData, null, 2)}
        </pre>
      )}
    </div>
  )
}
```

For more examples and integration with UI frameworks, check out the implementation on [Origin UI](http://originui.com/image-cropper).

## Origin UI Abstraction

Origin UI provides a [pre-styled component](https://github.com/origin-space/originui/blob/main/registry/default/ui/cropper.tsx) built on top of `@origin-space/image-cropper`. If you're using Origin UI or want a quicker setup, you can use this abstraction.

### Usage with Abstraction

Using the Origin UI abstraction simplifies the markup:

```tsx
"use client"

import React from "react"
import {
  Cropper,
  CropperCropArea,
  CropperDescription,
  CropperImage,
} from "@/registry/default/ui/cropper"

type Area = { x: number; y: number; width: number; height: number }

export default function Component() {
  const [cropData, setCropData] = React.useState<Area | null>(null)

  return (
    <div className="flex flex-col gap-4">
      <Cropper
        className="h-80"
        image="https://images.unsplash.com/photo-1494790108377-be9c29b29330"
        aspectRatio={1}
        onCropChange={setCropData}
      >
        <CropperDescription />
        <CropperImage />
        <CropperCropArea />
      </Cropper>

      {cropData && (
        <pre className="overflow-auto rounded bg-muted p-2 text-sm">
          {JSON.stringify(cropData, null, 2)}
        </pre>
      )}
    </div>
  )
}
```

## Components

*   **`Cropper.Root`**: The main container and controller. It handles logic, state, and interactions.
*   **`Cropper.Image`**: Renders the actual `<img>` tag. It's positioned and scaled by `Cropper.Root`.
*   **`Cropper.CropArea`**: A simple `<div>` representing the visual crop area. You style this component to show the bounds.
*   **`Cropper.Description`**: Renders a `<div>` intended for accessibility instructions. Its `id` is automatically linked via `aria-describedby` on the `Root` element. **This component is required for accessibility.**

## `Cropper.Root` Props

| Prop             | Type                           | Default     | Description                                                                                                  |
|------------------|--------------------------------|-------------|--------------------------------------------------------------------------------------------------------------|
| `image`          | `string`                       | **Required**| URL of the image to crop.                                                                                    |
| `children`       | `React.ReactNode`              | **Required**| Should include `Cropper.Image`, `Cropper.CropArea`, and `Cropper.Description`.                               |
| `aspectRatio`    | `number`                       | `1`         | The desired width/height aspect ratio (e.g., `1`, `1.5`, `4 / 3`, `16 / 9`).                                 |
| `cropPadding`    | `number`                       | `25`        | Minimum padding (in pixels) between the crop area edges and the container edges.                             |
| `minZoom`        | `number`                       | `1`         | Minimum zoom level (1 = 100% original size relative to crop area).                                           |
| `maxZoom`        | `number`                       | `3`         | Maximum zoom level.                                                                                          |
| `zoomSensitivity`| `number`                       | `0.005`     | Multiplier for mouse wheel delta to control zoom speed.                                                      |
| `keyboardStep`   | `number`                       | `10`        | Number of pixels to pan the image when using arrow keys.                                                     |
| `zoom`           | `number`                       | `undefined` | Controlled zoom level. If provided, component zoom state is controlled externally.                           |
| `onCropChange`   | `(pixels: Area \| null) => void` | `undefined` | Callback function triggered whenever the crop area changes. Receives pixel data or `null` if invalid.      |
| `onZoomChange`   | `(zoom: number) => void`       | `undefined` | Callback function triggered when the zoom level changes interactively. Essential for controlled `zoom` prop. |
| `className`      | `string`                       | `undefined` | CSS class for the root container element.                                                                    |
| `style`          | `React.CSSProperties`          | `undefined` | Inline styles for the root container element.                                                                |
| *...restProps*   |                                |             | Any other standard `HTMLDivElement` props are passed to the root container.                                  |

### `onCropChange` Data (`Area`)

The `Area` object received by `onCropChange` contains the following properties relative to the *original* image dimensions:

*   `x`: The x-coordinate of the top-left corner of the cropped area.
*   `y`: The y-coordinate of the top-left corner of the cropped area.
*   `width`: The width of the cropped area in pixels.
*   `height`: The height of the cropped area in pixels.

## Accessibility

It is crucial to include a `Cropper.Description` component within `Cropper.Root`. This provides necessary context for screen reader users about how to interact with the cropper. If you don't provide one, a warning will appear in the console. You can visually hide the description using standard CSS techniques (e.g., an `sr-only` class).

## Styling

The component is headless, meaning it doesn't come with built-in styles beyond basic positioning for the image. You are expected to style the `Cropper.Root`, `Cropper.Image`, and especially the `Cropper.CropArea` using CSS classes (`className`) or inline styles (`style`) to match your application's design. The `Cropper.CropArea` typically needs styling to appear as an overlay (e.g., border, semi-transparent background outside the area).

## License

MIT