# Phase 2: Utils Extraction

## Goal

Extract constants and utility functions into dedicated files.

## Files to Create

### utils/constants.ts

```typescript
/**
 * ImageViewer constants
 */

import type { ZoomPreset } from '../types';

// =============================================================================
// SIZE LIMITS
// =============================================================================

/** Maximum image size before blocking (50MB) */
export const MAX_IMAGE_SIZE = 50 * 1024 * 1024;

/** Image size threshold for warning (10MB) */
export const WARN_IMAGE_SIZE = 10 * 1024 * 1024;

// =============================================================================
// ZOOM CONFIGURATION
// =============================================================================

/** Minimum zoom level */
export const MIN_ZOOM = 0.25;

/** Maximum zoom level */
export const MAX_ZOOM = 4;

/** Available zoom presets */
export const ZOOM_PRESETS: ZoomPreset[] = [
  { value: 0.25, label: '25%' },
  { value: 0.5, label: '50%' },
  { value: 1, label: '100%' },
  { value: 2, label: '200%' },
  { value: 4, label: '400%' },
  { value: -1, label: 'Fit' }, // Special value for fit-to-view
];

// =============================================================================
// DEFAULT VALUES
// =============================================================================

/** Default transform state */
export const DEFAULT_TRANSFORM: ImageTransform = {
  rotation: 0,
  flipH: false,
  flipV: false,
};
```

### utils/lqip.ts

```typescript
/**
 * LQIP (Low-Quality Image Placeholder) generator
 *
 * Creates a tiny blurred preview image for progressive loading.
 */

// =============================================================================
// CONSTANTS
// =============================================================================

const LQIP_SIZE = 32;
const LQIP_QUALITY = 0.5;

// =============================================================================
// GENERATOR
// =============================================================================

/**
 * Create a low-quality image placeholder from source URL
 *
 * @param src - Full quality image URL
 * @returns Data URL of tiny preview, or null on error
 */
export async function createLQIP(src: string): Promise<string | null> {
  return new Promise((resolve) => {
    const img = new Image();

    img.onload = () => {
      try {
        // Calculate aspect-preserving dimensions
        const aspectRatio = img.width / img.height;
        const width = aspectRatio >= 1 ? LQIP_SIZE : Math.round(LQIP_SIZE * aspectRatio);
        const height = aspectRatio >= 1 ? Math.round(LQIP_SIZE / aspectRatio) : LQIP_SIZE;

        // Create tiny canvas
        const canvas = document.createElement('canvas');
        canvas.width = width;
        canvas.height = height;

        // Draw and export as JPEG
        const ctx = canvas.getContext('2d');
        if (ctx) {
          ctx.drawImage(img, 0, 0, width, height);
          resolve(canvas.toDataURL('image/jpeg', LQIP_QUALITY));
        } else {
          resolve(null);
        }
      } catch {
        resolve(null);
      }
    };

    img.onerror = () => resolve(null);
    img.src = src;
  });
}
```

### utils/index.ts

```typescript
/**
 * ImageViewer utilities - Public API
 */

export { createLQIP } from './lqip';
export {
  MAX_IMAGE_SIZE,
  WARN_IMAGE_SIZE,
  MIN_ZOOM,
  MAX_ZOOM,
  ZOOM_PRESETS,
  DEFAULT_TRANSFORM,
} from './constants';
```

## Notes

- Constants are separated for easy modification
- LQIP is async and handles errors gracefully
- No dependencies on React in utils
