> ## Documentation Index
> Fetch the complete documentation index at: https://layermetry.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Nextjs image editor

# Next.js ImageEditor Integration Guide

Complete guide for integrating the `@layermetry/media-editor` ImageEditor component into your Next.js application.

<Warning>
  **This page still describes the version 1 setup.** In version 2
  (`@layermetry/media-editor` 2.0.0) React is no longer pinned to 18.2.0 — React 18
  and React 19 both work, and the editor also runs with no React at all, so
  Next.js 15 is fine.

  Follow [Installation](/docs/installation) for the version 2 setup. This page is being
  rewritten.
</Warning>

## Table of Contents

* [Prerequisites](#prerequisites)
* [Installation](#installation)
* [Basic Setup](#basic-setup)
* [ImageEditor Interface](#imageeditor-interface)
* [Step-by-Step Integration](#step-by-step-integration)
* [Theme Customization](#theme-customization)
* [Callbacks and Event Handling](#callbacks-and-event-handling)
* [Complete Example](#complete-example)

***

## Prerequisites

* Node.js 18 or newer
* Next.js 14.x or 15.x
* React 18 or React 19 — both work. Version 2 does not pin React, and also runs with no React at all.

## Installation

### 1. Install the SDK

```bash theme={null}
npm install @layermetry/media-editor
```

### 2. Install Required Dependencies

```bash theme={null}
npm install framer-motion lucide-react clsx tailwind-merge
```

### 3. Configure Next.js for React 18

If you're using Next.js 15+ (which requires React 19), you need to downgrade to Next.js 14:

**package.json:**

```json theme={null}
{
  "dependencies": {
    "@layermetry/media-editor": "^2.0.0",
    "next": "14.2.15",
    "react": "18.2.0",
    "react-dom": "18.2.0"
  },
  "devDependencies": {
    "@types/react": "^18",
    "@types/react-dom": "^18",
    "eslint": "^8",
    "eslint-config-next": "14.2.15"
  },
  "overrides": {
    "react": "18.2.0",
    "react-dom": "18.2.0"
  }
}
```

***

## Basic Setup

### 1. Import SDK Styles

Add the SDK CSS to your global styles file:

**app/globals.css:**

```css theme={null}
@import "tailwindcss";

/* Import media-editor SDK styles */
@import "@layermetry/media-editor/dist/index.css";

/* Your other styles */
```

### 2. Configure Next.js

**next.config.js:**

```javascript theme={null}
/** @type {import('next').NextConfig} */
const nextConfig = {
  webpack: (config) => {
    // Disable node modules that don't work in browser
    config.resolve.alias = {
      ...config.resolve.alias,
      canvas: false,
      fs: false,
    };
    return config;
  },
};

module.exports = nextConfig;
```

### 3. Use Dynamic Import (Avoid SSR)

The SDK uses browser-only APIs, so you must use dynamic imports:

```typescript theme={null}
'use client';

import dynamic from 'next/dynamic';

const ImageEditor = dynamic(
  () => import('@layermetry/media-editor').then(mod => ({ default: mod.ImageEditor })),
  { ssr: false }
);
```

***

## ImageEditor Interface

### Core Props

```typescript theme={null}
interface ImageEditorProps {
  // Required
  licenseKey: string;              // Your SDK license key (JWT)
  onClose: () => void;             // Called when user closes editor

  // Optional
  apiUrl?: string;                 // Override license validation API URL
  files?: File;                    // Initial image file to load
  callback?: (result: CallbackProps, extras?: EditorExtras) => void;
  theme?: Record<string, string>;  // Custom theme colors
  showThemeCreator?: boolean;      // Show theme customization UI
  headless?: boolean;              // Enable programmatic mode
  brands?: BrandDetails[];         // Brand presets
  defaultTemplate?: Template;      // Load template on start

  // Advanced
  onExport?: (templateId: string, brandResults: any, updatedImage: string, thumbUri: string) => void;
  onSaveTemplate?: (props: { brandId: string; template: any }) => Promise<void>;
  onGetTemplates?: (brandIdList?: string[]) => Promise<void | { success: { data: any } }>;
  createAutomatedContent?: CreateAutomatedContent;
  calls?: CallItem[];              // Headless mode: functions to execute
}
```

### Callback Types

```typescript theme={null}
interface CallbackProps {
  base64: string;        // Exported image as base64 data URL
  width: number;         // Canvas width
  height: number;        // Canvas height
  template?: any;        // Scene data if template was used
}

interface EditorExtras {
  thumbnail?: string;    // Thumbnail preview
  format?: string;       // Image format (png, jpg, etc)
}
```

***

## Step-by-Step Integration

### Step 1: Create Your Page Component

**app/studio/image/page.tsx:**

```typescript theme={null}
'use client';

import { useState, useCallback, useRef } from 'react';
import { useRouter } from 'next/navigation';
import dynamic from 'next/dynamic';

// Dynamic import to avoid SSR
const ImageEditor = dynamic(
  () => import('@layermetry/media-editor').then(mod => ({ default: mod.ImageEditor })),
  { ssr: false }
);

export default function ImageStudioPage() {
  const router = useRouter();
  const fileInputRef = useRef<HTMLInputElement>(null);

  // State management
  const [selectedFile, setSelectedFile] = useState<File | null>(null);
  const [showEditor, setShowEditor] = useState(false);
  const [exportedImage, setExportedImage] = useState<string | null>(null);

  // Continue to Step 2...
}
```

### Step 2: Implement File Selection

```typescript theme={null}
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
  const file = e.target.files?.[0];
  if (file && file.type.startsWith('image/')) {
    setSelectedFile(file);
    setShowEditor(true);
  }
};

const handleDrop = (e: React.DragEvent) => {
  e.preventDefault();
  const file = e.dataTransfer.files?.[0];
  if (file && file.type.startsWith('image/')) {
    setSelectedFile(file);
    setShowEditor(true);
  }
};
```

### Step 3: Implement Callbacks

```typescript theme={null}
const handleExport = useCallback((result: any, extras?: any) => {
  console.log('Export result:', result, extras);

  if (result.base64) {
    setExportedImage(result.base64);
    setShowEditor(false);
    // Show success modal or download
  }
}, []);

const handleClose = () => {
  setShowEditor(false);
  setSelectedFile(null);
};

const handleDownload = () => {
  if (exportedImage) {
    const link = document.createElement('a');
    link.href = exportedImage;
    link.download = `edited-image-${Date.now()}.png`;
    link.click();
  }
};
```

### Step 4: Render the Editor

```typescript theme={null}
return (
  <div className="min-h-screen">
    {/* Upload UI */}
    {!showEditor && (
      <div>
        <input
          ref={fileInputRef}
          type="file"
          accept="image/*"
          onChange={handleFileSelect}
          className="hidden"
        />
        <button onClick={() => fileInputRef.current?.click()}>
          Choose Image
        </button>
      </div>
    )}

    {/* Image Editor */}
    {showEditor && selectedFile && (
      <div className="fixed inset-0 z-40">
        <ImageEditor
          licenseKey="YOUR_LICENSE_KEY_HERE"
          apiUrl="https://your-api.com/social"
          files={selectedFile}
          onClose={handleClose}
          callback={handleExport}
          theme={customTheme}
          showThemeCreator={false}
        />
      </div>
    )}
  </div>
);
```

***

## Theme Customization

### Creating a Custom Theme

```typescript theme={null}
const customTheme = {
  // Backgrounds
  'background.primary': '#0f172a',      // Main background
  'background.secondary': '#1e293b',    // Secondary panels
  'background.tertiary': '#334155',     // Tertiary elements

  // Text
  'text.primary': '#ffffff',            // Primary text
  'text.secondary': '#cbd5e0',          // Secondary text

  // Brand colors
  'accent.primary': '#3b82f6',          // Primary actions
  'accent.secondary': '#06b6d4',        // Secondary actions
  'accent.hover': '#60a5fa',            // Hover states

  // Borders
  'border.default': '#334155',
  'border.subtle': '#1e293b',

  // Components
  'button.primary': '#3b82f6',
  'input.background': '#1e293b',
  'toolbar.background': '#0f172a',
  'canvas.background': '#1a202c',
};
```

### Applying the Theme

```typescript theme={null}
<ImageEditor
  licenseKey={licenseKey}
  files={selectedFile}
  onClose={handleClose}
  callback={handleExport}
  theme={customTheme}
  showThemeCreator={false}  // Hide theme customization UI
/>
```

***

## Callbacks and Event Handling

### Export Callback

Called when user exports the edited image:

```typescript theme={null}
const handleExport = useCallback((result: CallbackProps, extras?: EditorExtras) => {
  console.log('Exported image:', {
    base64: result.base64,        // Data URL of image
    width: result.width,          // Canvas width
    height: result.height,        // Canvas height
    thumbnail: extras?.thumbnail  // Optional thumbnail
  });

  // Download the image
  const link = document.createElement('a');
  link.href = result.base64;
  link.download = 'edited-image.png';
  link.click();

  // Or upload to server
  fetch('/api/upload', {
    method: 'POST',
    body: JSON.stringify({ image: result.base64 }),
    headers: { 'Content-Type': 'application/json' }
  });
}, []);
```

### Close Callback

Called when user clicks close/cancel:

```typescript theme={null}
const handleClose = () => {
  // Confirm before closing if changes were made
  const confirmed = window.confirm('Discard changes?');
  if (confirmed) {
    setShowEditor(false);
    setSelectedFile(null);
  }
};
```

### Template Callbacks

Save and load custom templates:

```typescript theme={null}
const handleSaveTemplate = async ({ brandId, template }: any) => {
  const response = await fetch('/api/templates', {
    method: 'POST',
    body: JSON.stringify({ brandId, template }),
    headers: { 'Content-Type': 'application/json' }
  });
  return response.json();
};

const handleGetTemplates = async (brandIds?: string[]) => {
  const response = await fetch('/api/templates?' + new URLSearchParams({
    brandIds: brandIds?.join(',') || ''
  }));
  return response.json();
};

<ImageEditor
  onSaveTemplate={handleSaveTemplate}
  onGetTemplates={handleGetTemplates}
  // ... other props
/>
```

***

## Complete Example

**app/studio/image/page.tsx:**

```typescript theme={null}
'use client';

import { useState, useCallback, useRef } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Upload, Download, X } from 'lucide-react';
import { useRouter } from 'next/navigation';
import dynamic from 'next/dynamic';

const ImageEditor = dynamic(
  () => import('@layermetry/media-editor').then(mod => ({ default: mod.ImageEditor })),
  { ssr: false }
);

const customTheme = {
  'background.primary': '#0f172a',
  'background.secondary': '#1e293b',
  'background.tertiary': '#334155',
  'text.primary': '#ffffff',
  'text.secondary': '#cbd5e0',
  'accent.primary': '#3b82f6',
  'accent.secondary': '#06b6d4',
  'accent.hover': '#60a5fa',
  'border.default': '#334155',
  'border.subtle': '#1e293b',
};

export default function ImageStudioPage() {
  const router = useRouter();
  const fileInputRef = useRef<HTMLInputElement>(null);

  const [selectedFile, setSelectedFile] = useState<File | null>(null);
  const [showEditor, setShowEditor] = useState(false);
  const [exportedImage, setExportedImage] = useState<string | null>(null);
  const [showExportModal, setShowExportModal] = useState(false);

  const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (file && file.type.startsWith('image/')) {
      setSelectedFile(file);
      setShowEditor(true);
    }
  };

  const handleDrop = (e: React.DragEvent) => {
    e.preventDefault();
    const file = e.dataTransfer.files?.[0];
    if (file && file.type.startsWith('image/')) {
      setSelectedFile(file);
      setShowEditor(true);
    }
  };

  const handleExport = useCallback((result: any, extras?: any) => {
    console.log('Export result:', result, extras);
    if (result.base64) {
      setExportedImage(result.base64);
      setShowExportModal(true);
      setShowEditor(false);
    }
  }, []);

  const handleClose = () => {
    setShowEditor(false);
    setSelectedFile(null);
  };

  const handleDownload = () => {
    if (exportedImage) {
      const link = document.createElement('a');
      link.href = exportedImage;
      link.download = `edited-image-${Date.now()}.png`;
      link.click();
    }
  };

  return (
    <div className="min-h-screen bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900">
      {/* Upload Interface */}
      <AnimatePresence>
        {!showEditor && !showExportModal && (
          <motion.div
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
            className="flex flex-col items-center justify-center min-h-screen p-6"
          >
            <div
              onDrop={handleDrop}
              onDragOver={(e) => e.preventDefault()}
              onClick={() => fileInputRef.current?.click()}
              className="relative w-full max-w-2xl cursor-pointer"
            >
              <div className="bg-white/10 backdrop-blur-md border-2 border-dashed border-white/30 rounded-3xl p-16 text-center hover:border-purple-400 transition-all">
                <Upload className="w-20 h-20 mx-auto mb-6 text-purple-400" />
                <h3 className="text-2xl font-bold text-white mb-3">
                  Drop your image here
                </h3>
                <p className="text-slate-300 mb-6">
                  or click to browse your files
                </p>
              </div>
              <input
                ref={fileInputRef}
                type="file"
                accept="image/*"
                onChange={handleFileSelect}
                className="hidden"
              />
            </div>
          </motion.div>
        )}
      </AnimatePresence>

      {/* Image Editor */}
      {showEditor && selectedFile && (
        <div className="fixed inset-0 z-40">
          <ImageEditor
            licenseKey="YOUR_LICENSE_KEY_HERE"
            apiUrl="https://localhost:3030/social"
            files={selectedFile}
            onClose={handleClose}
            callback={handleExport}
            theme={customTheme}
            showThemeCreator={false}
          />
        </div>
      )}

      {/* Export Modal */}
      <AnimatePresence>
        {showExportModal && exportedImage && (
          <motion.div
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
            className="fixed inset-0 z-50 flex items-center justify-center p-6 bg-black/80"
            onClick={() => setShowExportModal(false)}
          >
            <motion.div
              initial={{ scale: 0.8, y: 20 }}
              animate={{ scale: 1, y: 0 }}
              exit={{ scale: 0.8, y: 20 }}
              onClick={(e) => e.stopPropagation()}
              className="bg-slate-800 rounded-2xl p-8 max-w-4xl w-full"
            >
              <div className="flex justify-between items-center mb-6">
                <h2 className="text-3xl font-bold text-white">
                  Your Creation is Ready!
                </h2>
                <button
                  onClick={() => setShowExportModal(false)}
                  className="p-2 hover:bg-white/10 rounded-lg"
                >
                  <X className="w-6 h-6 text-white" />
                </button>
              </div>

              <div className="mb-6 rounded-xl overflow-hidden bg-slate-900">
                <img src={exportedImage} alt="Edited" className="w-full h-auto" />
              </div>

              <div className="flex gap-4">
                <button
                  onClick={handleDownload}
                  className="flex-1 flex items-center justify-center gap-2 px-6 py-4 bg-gradient-to-r from-purple-500 to-pink-500 rounded-xl text-white font-bold"
                >
                  <Download className="w-5 h-5" />
                  Download Image
                </button>
                <button
                  onClick={() => {
                    setShowExportModal(false);
                    setSelectedFile(null);
                    setExportedImage(null);
                  }}
                  className="flex-1 px-6 py-4 bg-white/10 rounded-xl text-white font-bold"
                >
                  Create Another
                </button>
              </div>
            </motion.div>
          </motion.div>
        )}
      </AnimatePresence>
    </div>
  );
}
```

***

## Next Steps

1. See [FAQ](/docs/troubleshooting/faq) for common issues and troubleshooting
2. See [Next.js VideoEditor Integration](/docs/integration-guides/nextjs-video-editor) for video editing
3. Check the included example project for a complete working implementation

## Support

For issues or questions, please visit:

* Support Portal: Contact your account manager or [support@layermetry.com](mailto:support@layermetry.com)
* Documentation: [https://docs.layermetry.com](https://docs.layermetry.com)
