# Prompt: Înlocuiește sistemul actual de preview documente atașate cu QuickLook-style Preview

> Copiază tot conținutul de mai jos și dă-l ca instrucțiune directă modelului AI.

---

Înlocuiește complet sistemul actual de previzualizare a documentelor atașate cu un preview fullscreen inspirat de macOS Quick Look. Componentă React standalone, un singur fișier, fără dependențe externe — doar React hooks, lucide-react icons, Tailwind CSS, și un `Button` component din UI library.

## Arhitectură

**Un singur component exportat: `AttachmentPreview`**

Props:

```typescript
interface AttachmentPreviewProps {
  /** Array cu fișierele previewable (filtrate în prealabil) */
  attachments: Attachment[];
  /** Indexul inițial de afișat */
  initialIndex: number;
  /** Dacă modal-ul e deschis */
  open: boolean;
  /** Callback la închidere */
  onClose: () => void;
}

interface Attachment {
  id: string;
  name: string;
  data: string; // base64 data URI (data:image/jpeg;base64,... sau data:application/pdf;base64,...)
  type: string; // MIME type: "image/jpeg", "application/pdf", etc.
  size: number; // bytes
}
```

**Pattern de integrare (în componenta părinte):**

```tsx
// Folosește key={previewIndex} ca să forțeze remount-ul la fiecare deschidere nouă.
// Asta resetează zoom, pan, currentIndex fără setState în effects.
{
  previewIndex !== null && (
    <AttachmentPreview
      key={previewIndex}
      attachments={previewableAttachments}
      initialIndex={previewIndex}
      open
      onClose={() => setPreviewIndex(null)}
    />
  );
}
```

**Helper exportat: `getPreviewableAttachments(attachments)`**
Filtrează array-ul la atașamentele care pot fi previewate (au `data` non-empty, MIME type = `image/*` sau `application/pdf`).

## Structura UI (3 zone fixe)

### 1. Top Bar (`shrink-0`)

- Stânga: icon fișier + filename (truncate) + dimensiune KB + extensie + "N / M" counter (dacă sunt multiple fișiere)
- Dreapta: [Zoom− | 100% | Zoom+] (doar pentru imagini) | Print | Download | Close(X)
- Separatoare vizuale între grupuri de butoane: `div.w-px.h-5.bg-white/20.mx-1`

### 2. Content Area (`flex-1`)

- Container: `fixed inset-0 z-[100]`, `bg-black/90 backdrop-blur-sm`
- **Imagini:** `<img>` cu `transform: translate(panX, panY) scale(zoom)`, `transition-transform duration-150`, `draggable={false}`, `select-none`
- **PDF-uri:** `<iframe src={blobUrl} className="w-full h-full border-0">` — folosește blob URL, NU data: URI direct
- **Fallback** (tip nesuportat): icon mare + text + buton Download
- **Săgeți navigare**: cercuri absolute `left-3`/`right-3 top-1/2`, cu `e.stopPropagation()` pe click

### 3. Bottom Thumbnails (`shrink-0`, doar când sunt multiple fișiere)

- `overflow-x-auto`, `gap-2`
- Imagini: `<img className="h-10 w-14 object-cover rounded">`
- PDF-uri: `div.h-10.w-14` cu FileText icon pe fundal `bg-white/10`
- Selectat: `border-white/80 ring-1 ring-white/30`; neselectat: `opacity-50 hover:opacity-80`

## Detalii tehnice critice

### PDF rendering via Blob URL (NU data: URI)

Data URI-urile nu funcționează bine în iframe-uri. Convertește base64 la Blob:

```typescript
const pdfBlobUrl = useMemo(() => {
  if (!isPdf || !att?.data) return null;
  try {
    const base64 = att.data.split(",")[1]; // strip "data:application/pdf;base64,"
    if (!base64) return null;
    const bytes = atob(base64);
    const arr = new Uint8Array(bytes.length);
    for (let i = 0; i < bytes.length; i++) arr[i] = bytes.charCodeAt(i);
    const blob = new Blob([arr], { type: "application/pdf" });
    return URL.createObjectURL(blob);
  } catch {
    return null;
  }
}, [isPdf, att?.data]);

// IMPORTANT: revocă blob URL la cleanup ca să nu existe memory leak
useEffect(() => {
  return () => {
    if (pdfBlobUrl) URL.revokeObjectURL(pdfBlobUrl);
  };
}, [pdfBlobUrl]);
```

### Zoom (doar imagini)

- Range: 25% — 500%, step 25%
- Scroll wheel: `onWheel` cu `e.preventDefault()`, `deltaY > 0` = zoom out
- Keyboard: `+`/`=` zoom in, `-` zoom out, `0` reset la 100%
- Click pe procentajul afișat resetează la 100%

### Pan (doar imagini, doar când zoom > 1)

- `onMouseDown`: salvează start position în `useRef` (nu state! — ref e mai performant pentru drag)
- `onMouseMove`: calculează delta din ref
- `onMouseUp`/`onMouseLeave`: stop panning
- Cursor: `cursor-grab` când zoom > 1, `cursor-grabbing` când panning activ
- `isPanning` e singura stare React; coordonatele start sunt în ref: `useRef({ x: 0, y: 0, panX: 0, panY: 0 })`

### Print

- **PDF**: creează iframe ascuns cu `src=blobUrl`, `iframe.onload = () => iframe.contentWindow.print()`, cleanup cu `setTimeout(() => document.body.removeChild(iframe), 1000)`
- **Imagine**: `window.open("", "_blank")` cu HTML inline care conține `<img>`, `w.onload = () => w.print()`

### Download

```typescript
const a = document.createElement("a");
a.href = att.data; // data URI original
a.download = att.name;
a.click();
```

### Keyboard handler (useEffect)

- `Escape` → close
- `ArrowLeft`/`ArrowRight` → navigate (circular: last→first, first→last)
- `+`/`=`/`-`/`0` → zoom controls
- Cleanup obligatoriu: `removeEventListener` în return

### React 19 compatibility (FOARTE IMPORTANT)

- **NU pune `setState` în `useEffect`** — React 19 strict mode dă eroare la "Calling setState synchronously within an effect"
- **NU accesa `ref.current` direct în render body** — tot eroare: "Cannot access refs during render"
- Folosește **`key={index}`** pe componentă ca să forțezi remount cu state proaspăt la fiecare deschidere nouă
- Zoom/pan reset la navigare: apelează `resetView()` direct în callback-ul de navigare, NU într-un effect separat:

```typescript
const resetView = useCallback(() => {
  setZoom(1);
  setPan({ x: 0, y: 0 });
}, []);

// În keyboard handler și goNext/goPrev — resetView() inline:
setCurrentIndex((i) => {
  const next = i < attachments.length - 1 ? i + 1 : 0;
  resetView(); // apelat inline, nu în effect
  return next;
});
```

## Styling complet

- Container principal: `fixed inset-0 z-[100] flex flex-col bg-black/90 backdrop-blur-sm animate-in fade-in duration-200`
- Top/bottom bars: `bg-black/60 border-b/border-t border-white/10`
- Text pe dark background: `text-white`, `text-white/70`, `text-white/50`
- Buttons: `variant="ghost"` cu `text-white/70 hover:text-white hover:bg-white/10`, size `h-8 w-8`
- Zoom percentage text: `font-mono min-w-[3.5rem] text-center text-xs`
- Navigation arrows: `rounded-full bg-black/50 hover:bg-black/70 w-10 h-10`, centered cu `flex items-center justify-center`
- Imagine: `select-none transition-transform duration-150`, stil inline pentru transform
- PDF iframe: `w-full h-full border-0`

## Dependențe

- React: `useState`, `useEffect`, `useCallback`, `useMemo`, `useRef`
- lucide-react: `ChevronLeft`, `ChevronRight`, `Download`, `FileText`, `Minus`, `Plus`, `Printer`, `X`
- Tailwind CSS (classes direct pe elemente)
- Un `Button` component din UI library (cu props: `variant="ghost"`, `size="icon"`)
- `cn()` utility pentru conditional classes (opțional, poți folosi template literals)

## Ce NU trebuie să facă

- **NU depinde de Dialog/Radix** — e pur `fixed div` (simplu, zero probleme cu z-index sau portal nesting)
- **NU stochează fișiere pe server** — totul din `data:` URI-urile deja existente în state
- **NU face streaming/lazy load** — fișierul e deja în memorie ca base64
- **NU suportă video/audio** (doar `image/*` și `application/pdf`)
- **NU folosește `window.open("file:///...")` pentru fișiere locale** — browserele blochează `file:///` URLs din pagini web (security restriction)

## Checklist final

- [ ] Componenta e un singur fișier exportat
- [ ] Funcționează cu imagini (zoom cu scroll wheel, pan cu drag, reset cu 0)
- [ ] Funcționează cu PDF-uri (blob URL în iframe, nu data: URI)
- [ ] Navigare cu săgeți (keyboard + butoane) când sunt multiple fișiere
- [ ] Thumbnail strip în josul ecranului când sunt multiple
- [ ] Download și Print funcționează pentru ambele tipuri
- [ ] Escape închide preview-ul
- [ ] Zero `setState` direct în `useEffect` (React 19 compatible)
- [ ] Zero `ref.current` accesat în render body (React 19 compatible)
- [ ] `key={index}` pe componentă pentru remount clean
- [ ] `URL.revokeObjectURL()` apelat la cleanup pentru PDF blob URLs
- [ ] `e.stopPropagation()` pe butoanele de navigare (să nu trigger pan/zoom)
