# @edgepdf/viewer-react

React components and hooks for the EdgePDF viewer. Provides a declarative, React-friendly API for viewing large PDF documents with interactive markers, annotations, and zoom controls.

## Installation

```bash
npm install @edgepdf/viewer-react @edgepdf/viewer-js
# or
pnpm add @edgepdf/viewer-react @edgepdf/viewer-js
# or
yarn add @edgepdf/viewer-react @edgepdf/viewer-js
```

## Quick Start

### Next.js

In your Next.js application, you need to import the CSS file manually:

**App Router (app directory):**

```tsx
// app/layout.tsx
import '@edgepdf/viewer-react/styles.css';
import { ViewerProvider } from '@edgepdf/viewer-react';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <ViewerProvider>{children}</ViewerProvider>
      </body>
    </html>
  );
}
```

```tsx
// app/page.tsx
import { EdgePDFViewer } from '@edgepdf/viewer-react';
import type { ViewerConfig } from '@edgepdf/types';

const config: ViewerConfig = {
  tileUrl: 'https://example.com/tiles/{z}/{x}/{y}.png',
  imageInfo: {
    width: 2000,
    height: 3000,
    tileSize: 256,
    maxZoom: 5,
    minZoom: 0,
  },
};

export default function Page() {
  return <EdgePDFViewer config={config} />;
}
```

**Pages Router:**

```tsx
// pages/your-page.tsx
import '@edgepdf/viewer-react/styles.css';
import { ViewerProvider, EdgePDFViewer } from '@edgepdf/viewer-react';
import type { ViewerConfig } from '@edgepdf/types';

const config: ViewerConfig = {
  tileUrl: 'https://example.com/tiles/{z}/{x}/{y}.png',
  imageInfo: {
    width: 2000,
    height: 3000,
    tileSize: 256,
    maxZoom: 5,
    minZoom: 0,
  },
};

export default function Page() {
  return (
    <ViewerProvider>
      <EdgePDFViewer config={config} />
    </ViewerProvider>
  );
}
```

### Other React Applications

```tsx
import '@edgepdf/viewer-react/styles.css';
import {
  ViewerProvider,
  EdgePDFViewer,
  useMarkers,
  useViewer,
  useZoom,
} from '@edgepdf/viewer-react';
import type { ViewerConfig } from '@edgepdf/types';

const config: ViewerConfig = {
  tileUrl: 'https://example.com/tiles/{z}/{x}/{y}.png',
  imageInfo: {
    width: 2000,
    height: 3000,
    tileSize: 256,
    maxZoom: 5,
    minZoom: 0,
  },
};

function MarkerPanel() {
  const { markers, addMarker, removeMarker } = useMarkers();
  const { viewer, isInitialized } = useViewer();
  const { zoomIn, zoomOut, currentZoom } = useZoom();

  return (
    <div>
      <div>Initialized: {isInitialized ? 'yes' : 'no'}</div>
      <div>Total markers: {markers.length}</div>
      <div>Current zoom: {currentZoom}</div>
      <button onClick={zoomIn}>Zoom In</button>
      <button onClick={zoomOut}>Zoom Out</button>
    </div>
  );
}

function App() {
  return (
    <ViewerProvider>
      <MarkerPanel />
      <EdgePDFViewer config={config} />
    </ViewerProvider>
  );
}
```

## API Reference

### Components

#### `<ViewerProvider>`

Provides viewer context to child components. Must wrap all components that use viewer hooks.

**Props:**

```typescript
interface ViewerProviderProps {
  children: ReactNode;
}
```

**Example:**

```tsx
<ViewerProvider>
  <App />
</ViewerProvider>
```

---

#### `<EdgePDFViewer>`

Main React component for the EdgePDF viewer. Must be used within a `ViewerProvider`.

**Props:**

```typescript
interface EdgePDFViewerProps {
  /** Viewer configuration */
  config: ViewerConfig;
  /** Optional map options */
  mapOptions?: MapOptions;
  /** Optional className for the container */
  className?: string;
  /** Optional style for the container */
  style?: React.CSSProperties;
  /** Show zoom controls (default: true) */
  showZoomControls?: boolean;
  /** Zoom controls position (only used if showZoomControls is true) */
  zoomControlsPosition?:
    | 'top-left'
    | 'top-right'
    | 'bottom-left'
    | 'bottom-right';
  /** Show zoom level in zoom controls (only used if showZoomControls is true) */
  showZoomLevel?: boolean;
  /** Enable annotation functionality (default: true) */
  enableAnnotation?: boolean;
  /** Show edit button in marker action controls (default: true) */
  showEditButton?: boolean;
  /** Show delete button in marker action controls (default: true) */
  showDeleteButton?: boolean;
  /** Default zoom level for initial view */
  defaultZoomLevel?: number;
  /** Callback when markers/pins are updated */
  onPinsUpdate?: (pins: Marker[]) => void;
  /** Callback when a marker is clicked */
  onMarkerClick?: (marker: Marker) => void;
  /** Callback when a marker is updated */
  onMarkerUpdate?: (marker: Marker) => void;
  /** Callback when a marker is deleted */
  onMarkerDelete?: (marker: Marker) => void;
  /** Callback when a marker is added (created by user tap/click) */
  onMarkerAdd?: (marker: Marker) => void;
  /** Default pins to preload (uses internal import) */
  defaultPins?: Marker[] | MarkerData;
  /** Children components */
  children?: ReactNode;
}
```

**Example:**

```tsx
<EdgePDFViewer
  config={config}
  showZoomControls={true}
  zoomControlsPosition="top-right"
  enableAnnotation={true}
  onMarkerClick={(marker) => console.log('Marker clicked:', marker)}
  onPinsUpdate={(pins) => console.log('Pins updated:', pins)}
/>
```

---

#### `<ZoomControls>`

React component for zoom controls. Uses the JS library's zoom controls internally.

**Props:**

```typescript
interface ZoomControlsProps {
  /** Optional className for the container */
  className?: string;
  /** Optional style for the container */
  style?: React.CSSProperties;
  /** Position of the controls */
  position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
  /** Show zoom level display */
  showZoomLevel?: boolean;
}
```

**Example:**

```tsx
<EdgePDFViewer config={config}>
  <ZoomControls position="top-right" showZoomLevel={true} />
</EdgePDFViewer>
```

---

### Hooks

#### `useViewer()`

Hook to access and interact with the viewer instance.

**Returns:**

```typescript
interface UseViewerReturn {
  /** Viewer instance */
  viewer: EdgePdfViewer | null;
  /** Whether viewer is initialized */
  isInitialized: boolean;
  /** Get the map instance */
  getMap: () => L.Map | null;
  /** Check if viewer is initialized */
  checkInitialized: () => boolean;
}
```

**Example:**

```tsx
function MyComponent() {
  const { viewer, isInitialized, getMap } = useViewer();

  useEffect(() => {
    if (isInitialized && viewer) {
      const map = getMap();
      // Use map...
    }
  }, [isInitialized, viewer, getMap]);

  return <div>Viewer is {isInitialized ? 'ready' : 'loading'}</div>;
}
```

---

#### `useMarkers()`

Hook to manage markers in the viewer.

**Returns:**

```typescript
interface UseMarkersReturn {
  /** Current markers */
  markers: Marker[];
  /** Add a new marker */
  addMarker: (options: CreateMarkerOptions) => Marker | null;
  /** Remove a marker by ID */
  removeMarker: (id: string) => boolean;
  /** Update a marker */
  updateMarker: (id: string, updates: Partial<Marker>) => boolean;
  /** Get a marker by ID */
  getMarker: (id: string) => Marker | null;
  /** Get all markers */
  getAllMarkers: () => Marker[];
  /** Export markers as JSON */
  exportMarkers: () => MarkerData;
  /** Import markers from JSON */
  importMarkers: (data: MarkerData) => boolean;
  /** Clear all markers */
  clearMarkers: () => void;
  /** Focus on a marker by panning and zooming to its position */
  focusMarker: (
    markerOrId: string | Marker,
    options?: {
      zoom?: number;
      animate?: boolean;
      duration?: number;
      offsetLeft?: number;
      offsetRight?: number;
      offsetTop?: number;
      offsetBottom?: number;
    }
  ) => boolean;
}
```

**Example:**

```tsx
function MarkerControls() {
  const {
    markers,
    addMarker,
    removeMarker,
    updateMarker,
    exportMarkers,
    importMarkers,
    clearMarkers,
    focusMarker,
  } = useMarkers();

  const handleAddMarker = () => {
    addMarker({
      position: [100, 200],
      x: 100,
      y: 200,
      zoom: 1,
      label: 'New Marker',
    });
  };

  const handleExport = () => {
    const data = exportMarkers();
    console.log(JSON.stringify(data, null, 2));
  };

  const handleFocus = (markerId: string) => {
    focusMarker(markerId, { zoom: 3, animate: true });
  };

  return (
    <div>
      <button onClick={handleAddMarker}>Add Marker</button>
      <button onClick={handleExport}>Export Markers</button>
      <button onClick={clearMarkers}>Clear All</button>
      {markers.map((marker) => (
        <div key={marker.id}>
          {marker.label}
          <button onClick={() => removeMarker(marker.id)}>Remove</button>
          <button onClick={() => handleFocus(marker.id)}>Focus</button>
        </div>
      ))}
    </div>
  );
}
```

**Methods:**

- **`addMarker(options: CreateMarkerOptions): Marker | null`** - Creates a new marker. Returns the created marker or `null` if viewer is not initialized.

- **`removeMarker(id: string): boolean`** - Removes a marker by ID. Returns `true` if successful, `false` otherwise.

- **`updateMarker(id: string, updates: Partial<Marker>): boolean`** - Updates a marker's properties. Returns `true` if successful, `false` otherwise.

- **`getMarker(id: string): Marker | null`** - Gets a marker by ID. Returns the marker or `null` if not found.

- **`getAllMarkers(): Marker[]`** - Gets all markers. Returns an array of all markers.

- **`exportMarkers(): MarkerData`** - Exports all markers as JSON data. Returns a `MarkerData` object.

- **`importMarkers(data: MarkerData): boolean`** - Imports markers from JSON data. Returns `true` if successful, `false` otherwise.

- **`clearMarkers(): void`** - Removes all markers.

- **`focusMarker(markerOrId: string | Marker, options?: FocusOptions): boolean`** - Focuses on a marker by panning and zooming. Returns `true` if successful, `false` otherwise.

  **Options:**

  - `zoom?: number` - Target zoom level (uses marker's zoom or default if not provided)
  - `animate?: boolean` - Whether to animate the transition (default: true)
  - `duration?: number` - Animation duration in seconds (default: 0.5)
  - `offsetLeft?: number` - Pixel offset to move focus marker to the left (default: 0)
  - `offsetRight?: number` - Pixel offset to move focus marker to the right (default: 0)
  - `offsetTop?: number` - Pixel offset to move focus marker upward (default: 0)
  - `offsetBottom?: number` - Pixel offset to move focus marker downward (default: 0)

  **Example:**

  ```tsx
  // Focus with offset to account for overlay
  focusMarker(markerId, { offsetLeft: 100, offsetTop: 50 });
  ```

---

#### `useZoom()`

Hook to manage zoom in the viewer.

**Returns:**

```typescript
interface UseZoomReturn {
  /** Current zoom state */
  zoomState: ZoomState | null;
  /** Current zoom level */
  currentZoom: number;
  /** Minimum zoom level */
  minZoom: number;
  /** Maximum zoom level */
  maxZoom: number;
  /** Zoom in */
  zoomIn: () => void;
  /** Zoom out */
  zoomOut: () => void;
  /** Set zoom level */
  setZoom: (zoom: number) => void;
  /** Check if can zoom in */
  canZoomIn: () => boolean;
  /** Check if can zoom out */
  canZoomOut: () => boolean;
}
```

**Example:**

```tsx
function ZoomControls() {
  const {
    zoomIn,
    zoomOut,
    setZoom,
    currentZoom,
    minZoom,
    maxZoom,
    canZoomIn,
    canZoomOut,
  } = useZoom();

  return (
    <div>
      <button onClick={zoomOut} disabled={!canZoomOut()}>
        Zoom Out
      </button>
      <span>
        Zoom: {currentZoom} ({minZoom}-{maxZoom})
      </span>
      <button onClick={zoomIn} disabled={!canZoomIn()}>
        Zoom In
      </button>
      <button onClick={() => setZoom(2)}>Set Zoom to 2</button>
    </div>
  );
}
```

**Methods:**

- **`zoomIn(): void`** - Zooms in by one level.

- **`zoomOut(): void`** - Zooms out by one level.

- **`setZoom(zoom: number): void`** - Sets a specific zoom level.

- **`canZoomIn(): boolean`** - Checks if can zoom in. Returns `true` if current zoom is less than max zoom.

- **`canZoomOut(): boolean`** - Checks if can zoom out. Returns `true` if current zoom is greater than min zoom.

---

#### `useViewerContext()`

Hook to access viewer context directly. Usually you should use `useViewer()`, `useMarkers()`, or `useZoom()` instead.

**Returns:**

```typescript
interface ViewerContextValue {
  /** Viewer instance */
  viewer: EdgePdfViewer | null;
  /** Whether viewer is initialized */
  isInitialized: boolean;
  /** Current markers */
  markers: Marker[];
  /** Current zoom state */
  zoomState: ZoomState | null;
  /** Internal: Function to update context value */
  setContextValue: (
    updates: Partial<Omit<ViewerContextValue, 'setContextValue'>>
  ) => void;
}
```

**Throws:**

- `Error` if used outside `ViewerProvider`

**Example:**

```tsx
function MyComponent() {
  const { viewer, isInitialized, markers, zoomState } = useViewerContext();
  // Use context values...
}
```

---

## Type Exports

The library also exports TypeScript types for convenience:

```typescript
import type {
  EdgePDFViewerProps,
  ZoomControlsProps,
  ViewerContextValue,
  ViewerProviderProps,
  UseViewerReturn,
  UseMarkersReturn,
  UseZoomReturn,
} from '@edgepdf/viewer-react';
```

## Examples

### Basic Usage with Markers

```tsx
import {
  ViewerProvider,
  EdgePDFViewer,
  useMarkers,
} from '@edgepdf/viewer-react';

function MarkerList() {
  const { markers, addMarker, removeMarker } = useMarkers();

  return (
    <div>
      <h3>Markers ({markers.length})</h3>
      {markers.map((marker) => (
        <div key={marker.id}>
          <strong>{marker.label}</strong>
          <button onClick={() => removeMarker(marker.id)}>Remove</button>
        </div>
      ))}
    </div>
  );
}

function App() {
  const config = {
    tileUrl: 'https://example.com/tiles/{z}/{x}/{y}.png',
    imageInfo: {
      width: 2000,
      height: 3000,
      tileSize: 256,
      maxZoom: 5,
      minZoom: 0,
    },
  };

  return (
    <ViewerProvider>
      <div style={{ display: 'flex' }}>
        <div style={{ width: '300px' }}>
          <MarkerList />
        </div>
        <div style={{ flex: 1, height: '100vh' }}>
          <EdgePDFViewer config={config} />
        </div>
      </div>
    </ViewerProvider>
  );
}
```

### With Callbacks

```tsx
function App() {
  const config = {
    tileUrl: 'https://example.com/tiles/{z}/{x}/{y}.png',
    imageInfo: {
      width: 2000,
      height: 3000,
      tileSize: 256,
      maxZoom: 5,
      minZoom: 0,
    },
  };

  const handleMarkerClick = (marker: Marker) => {
    console.log('Marker clicked:', marker);
  };

  const handlePinsUpdate = (pins: Marker[]) => {
    console.log('Pins updated:', pins);
    // Save to backend, localStorage, etc.
  };

  return (
    <ViewerProvider>
      <EdgePDFViewer
        config={config}
        onMarkerClick={handleMarkerClick}
        onPinsUpdate={handlePinsUpdate}
        enableAnnotation={true}
      />
    </ViewerProvider>
  );
}
```

### Custom Zoom Controls

```tsx
function CustomZoomControls() {
  const { zoomIn, zoomOut, currentZoom, canZoomIn, canZoomOut } = useZoom();

  return (
    <div style={{ position: 'absolute', top: 10, right: 10, zIndex: 1000 }}>
      <button onClick={zoomOut} disabled={!canZoomOut()}>
        −
      </button>
      <span>{currentZoom}</span>
      <button onClick={zoomIn} disabled={!canZoomIn()}>
        +
      </button>
    </div>
  );
}

function App() {
  return (
    <ViewerProvider>
      <EdgePDFViewer config={config} showZoomControls={false}>
        <CustomZoomControls />
      </EdgePDFViewer>
    </ViewerProvider>
  );
}
```

### Import/Export Markers

```tsx
function MarkerManager() {
  const { markers, exportMarkers, importMarkers, clearMarkers } = useMarkers();

  const handleExport = () => {
    const data = exportMarkers();
    const blob = new Blob([JSON.stringify(data, null, 2)], {
      type: 'application/json',
    });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'markers.json';
    a.click();
    URL.revokeObjectURL(url);
  };

  const handleImport = (event: React.ChangeEvent<HTMLInputElement>) => {
    const file = event.target.files?.[0];
    if (!file) return;

    const reader = new FileReader();
    reader.onload = (e) => {
      try {
        const data = JSON.parse(e.target?.result as string);
        importMarkers(data);
      } catch (error) {
        console.error('Failed to import markers:', error);
      }
    };
    reader.readAsText(file);
  };

  return (
    <div>
      <button onClick={handleExport}>Export Markers</button>
      <input type="file" accept=".json" onChange={handleImport} />
      <button onClick={clearMarkers}>Clear All</button>
      <div>Total markers: {markers.length}</div>
    </div>
  );
}
```
