npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@files-ui/crop

v1.1.0

Published

Framework-agnostic image cropping for Files UI

Readme

@files-ui/crop

Framework-agnostic image cropping for Files UI. Pure JavaScript core with optional React wrapper.

Installation

npm install @files-ui/crop
# or
yarn add @files-ui/crop

Features

  • Framework Agnostic — Pure JavaScript core works everywhere (Vanilla JS, React, Vue, Angular, Svelte)
  • Zero Dependencies — No external libraries, just Canvas API
  • Touch-Friendly — Mouse drag, touch drag, pinch-zoom, wheel zoom
  • Aspect Ratios — Fixed ratios (1:1, 4:3, 16:9) or free-form
  • Dark Mode — Built-in theming support
  • TypeScript — Full type safety
  • Tiny Bundle — Core: ~5KB, React wrapper: ~2KB (total ~7KB gzipped)

Quick Start

React (Recommended)

import { FileMosaic } from "@files-ui/react";
import { CropDialog, useCropDialog } from "@files-ui/crop/react";

function MyComponent() {
  const { cropFile, openCrop, closeCrop, handleCropComplete } = useCropDialog();
  
  return (
    <>
      <FileMosaic 
        {...file} 
        onEdit={openCrop}
      />
      
      {cropFile && (
        <CropDialog
          file={cropFile}
          onComplete={handleCropComplete}
          onCancel={closeCrop}
          aspectRatio={1} // 1:1 for square crop
        />
      )}
    </>
  );
}
```Package Exports

### `@files-ui/crop` (Main)

Exports both core and React. Use this if you're using React:

```tsx
import { CropDialog, cropImage } from "@files-ui/crop";

@files-ui/crop/core (Framework-Agnostic)

Pure JavaScript utilities - no React dependency:

import { CropEngine, cropImage, cropExtFile } from "@files-ui/crop/core";

@files-ui/crop/react (React Only)

React components and hooks:

import { CropDialog, useCropDialog } from "@files-ui/crop/react";

Core API (Framework-Agnostic)

CropEngine

Interactive vanilla JS crop engine with visual interface.

const engine = new CropEngine(container, options, callbacks);
await engine.loadImage(imageUrl);
engine.setZoom(1.5);
const cropArea = engine.getCropArea();
engine.destroy();

Constructor:

  • container: HTMLElement — DOM element to render into
  • options?: CropEngineOptions — Configuration
  • callbacks?: CropEngineCallbacks — Event callbacks

Options: | Option | Type | Default | Description | |--------|------|---------|-------------| | aspectRatio | number \| null | null | Fixed aspect ratio (e.g., 1 for square) | | initialZoom | number | 1 | Starting zoom level | | minZoom | number | 1 | Minimum zoom | | maxZoom | number | 3 | Maximum zoom |

Methods:

  • loadImage(url: string): Promise<void> — Load image
  • setZoom(level: number): void — Set zoom level
  • getZoom(): number — Get current zoom

Usage Examples

Vanilla JavaScript (No Framework)

<!DOCTYPE html>
<html>
<body>
  <div id="crop-container" style="width: 600px; height: 400px;"></div>
  <input type="range" id="zoom" min="1" max="3" step="0.1" value="1" />
  <button id="save">Save Crop</button>

  <script type="module">
    import { CropEngine, cropExtFile } from "@files-ui/crop/core";

    const engine = new CropEngine(
      document.getElementById("crop-container"),
      { aspectRatio: 16/9 },
      { onZoomChange: (z) => console.log("Zoom:", z) }
    );

    // Load image
    await engine.loadImage("photo.jpg");

    // Zoom control
    document.getElementById("zoom").oninput = (e) => {
      engine.setZoom(Number(e.target.value));
    };

    // Save
    document.getElementById("save").onclick = async () => {
      const cropArea = engine.getCropArea();
      const file = { imageUrl: "photo.jpg", name: "photo.jpg" };
      const cropped = await cropExtFile(file, cropArea);
      console.log("Cropped file:", cropped);
    };
  </script>
</body>
</html>

React with Files UI

import { Dropzone, FileMosaic } from "@files-ui/react";
import { CropDialog, useCropDialog } from "@files-ui/crop/react";

function ImageUploader() {
  const [files, setFiles] = useState([]);
  const { cropFile, openCrop, closeCrop, handleCropComplete } = useCropDialog({
    onComplete: (cropped) => {
      setFiles(prev => prev.map(f => 
        f.id === cropped.id ? cropped : f
      ));
    }
  });

  return (
    <>
      <Dropzone value={files} onChange={setFiles}>
        {files.map(file => (
          <FileMosaic key={file.id} {...file} onEdit={openCrop} />
        ))}
      </Dropzone>
      
      {cropFile && (
        <CropDialog
          file={cropFile}
          onComplete={handleCropComplete}
          onCancel={closeCrop}
          aspectRatio={1}
          darkMode
        />
      )}
    </>
  );
}

Custom Aspect Ratios

<CropDialog
  file={file}
  aspectRatio={16 / 9}  // Widescreen
  onComplete={handleCrop}
  onCancel={closeCrop}
/>

Low-Level Cropping (No UI)

import { cropImage } from "@files-ui/crop/core";

// Crop specific area
const blob = await cropImage(
  imageFile,
  { x: 100, y: 100, width: 300, height: 300 },
  { quality: 0.95, format: "webp" }
);

// Use the blob
const url = URL.createObjectURL(blob);nst blob = await cropImage(imageSource, cropArea, options);

Parameters: | Param | Type | Description | |-------|------|-------------| | imageSource | string \| File \| Blob | Image to crop | | cropArea | CropArea | Crop coordinates | | options | CropOptions | Quality/format settings |

Returns: Promise<Blob>

cropExtFile()

Crop an ExtFile and return new ExtFile:

import { cropExtFile } from "@files-ui/crop/core";

const croppedFile = await cropExtFile(file, cropArea, options);

React API

Vanilla JavaScript (Framework-Agnostic)

import { CropEngine, cropExtFile } from "@files-ui/crop/core";

// Create crop engine
const container = document.getElementById("crop-container");
const engine = new CropEngine(container, { aspectRatio: 1 });
Architecture

@files-ui/crop ├── core/ # Framework-agnostic (5KB) │ ├── cropImage.ts # Pure Canvas cropping │ ├── CropEngine.ts # Interactive UI engine │ ├── extFileCrop.ts # ExtFile helpers │ └── types.ts # TypeScript definitions └── react/ # React wrapper (2KB) ├── CropDialog.tsx # React component ├── useCropDialog.ts # React hook └── CropDialog.css # Styles


**Benefits:**
- Use `core` in any framework (Vue, Angular, Svelte, vanilla JS)
- Use `react` for seamless React integration
- No external dependencies - just Canvas API
- Tiny bundle size (~7KB total)

## Browser Support

Works in all modern browsers with Canvas API:
- Chrome 60+
- Firefox 55+
- Safari 11+
- Edge 79+

## 
// Load image
await engine.loadImage(file.imageUrl);

// User interacts (drag, zoom, etc.)

// Get crop area and create cropped file
document.getElementById("save-btn").onclick = async () => {
  const cropArea = engine.getCropArea();
  const croppedFile = await cropExtFile(file, cropArea, {
    quality: 0.9,
    format: "jpeg"
  });
  console.log("Cropped:", croppedFile);
};

API Reference

<CropDialog>

| Prop | Type | Default | Description | |------|------|---------|-------------| | file | ExtFile | required | The file to crop | | onComplete | (croppedFile: ExtFile) => void | required | Called with cropped file | | onCancel | () => void | required | Called when user cancels | | aspectRatio | number \| null | null | Aspect ratio (e.g., 1 for square, 16/9 for widescreen) | | darkMode | boolean | false | Enable dark mode styling | | quality | number | 0.92 | Output quality (0-1) | | format | "jpeg" \| "png" \| "webp" | "jpeg" | Output format |

useCropDialog()

Hook for managing crop dialog state:

const {
  cropFile,      // Current file being cropped
  openCrop,      // (file: ExtFile) => void
  closeCrop,     // () => void
  handleCropComplete  // (croppedFile: ExtFile) => void
} = useCropDialog({
  onComplete: (file) => {
    // Replace original with cropped
    updateFile(file);
  }
});

Advanced Usage

Custom Aspect Ratios

<CropDialog
  file={file}
  aspectRatio={16 / 9}  // Widescreen
  onComplete={handleCrop}
  onCancel={closeCrop}
/>

With Multiple Files

function MultiFileCrop() {
  const [files, setFiles] = useState([]);
  const { cropFile, openCrop, closeCrop } = useCropDialog({
    onComplete: (croppedFile) => {
      setFiles(prev => prev.map(f => 
        f.id === croppedFile.id ? croppedFile : f
      ));
    }
  });
  
  return (
    <>
      <Dropzone value={files} onChange={setFiles}>
        {files.map(file => (
          <FileMosaic key={file.id} {...file} onEdit={openCrop} />
        ))}
      </Dropzone>
      
      {cropFile && (
        <CropDialog
          file={cropFile}
          onComplete={() => closeCrop()}
          onCancel={closeCrop}
        />
      )}
    </>
  );
}

License

MIT © JinSSJ3