---
title: "Events, saving & sharing — Vue"
description: "Listen for annotation changes, save them into the PDF, and move them in and out of your own database."
framework: "Vue"
source: "https://www.embedpdf.com/docs/headless/vue/annotations/saving"
---

# Events, saving & sharing

Every annotation change fires an event, whoever made it: the user, your code, or someone else on
a shared document. Listen for them to keep your own UI or server in step:

**`events.vue`**

```vue
<script setup lang="ts">
import { useAnnotationEvent } from '@embedpdf/vue/annotation';

useAnnotationEvent(
  (annotation) => annotation.onCreated,
  ({ annotation, origin }) => {
    console.log(`${annotation.author} added a ${annotation.subtype}`);
  },
);
</script>

<template>
  <slot />
</template>
```

**`App.vue`**

```vue
<script setup lang="ts">
import { DocumentGate, Viewer } from '@embedpdf/vue/runtime';
import type { OpenInput } from '@embedpdf/vue/runtime';
import { Stage, stagePlugin } from '@embedpdf/vue/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/vue/render';
import { interactionPlugin } from '@embedpdf/vue/interaction';
import { AnnotationLayer, annotationPlugin } from '@embedpdf/vue/annotation';
import { localEngine } from '@embedpdf/engine';
import ActivityLog from './ActivityLog.vue';
import AddRectangle from './AddRectangle.vue';
import Toolbar from './Toolbar.vue';

import '../events.css';

const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin(), interactionPlugin(), annotationPlugin()];

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};
</script>

<template>
  <Viewer
    :engine="engine"
    :plugins="plugins"
    :identity="{ userId: 'u_381', displayName: 'Dana Smith' }"
    :initial-documents="[{ source: ebook }]"
  >
    <DocumentGate>
      <template #fallback><p class="loading">Loading…</p></template>
      <AddRectangle />
      <Toolbar />
      <div class="viewer">
        <Stage class="stage">
          <template #page>
            <RenderLayer />
            <AnnotationLayer />
          </template>
        </Stage>
        <ActivityLog />
      </div>
    </DocumentGate>
  </Viewer>
</template>
```

**`events.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --text: #3b4b7a;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #054fb3;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The controls, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

/* One choice of a few: the pressed button is the current one */
.segmented {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 2px;
  padding: 3px;
  background: var(--tint);
  border: 1px solid var(--line);
  border-radius: 11px;
}

.segmented button {
  font: inherit;
  font-size: 12.5px;
  font-weight: 550;
  color: var(--muted);
  background: transparent;
  border: 0;
  border-radius: 8px;
  padding: 6px 12px;
  cursor: pointer;
  white-space: nowrap;
}

.segmented button:hover {
  color: var(--ink);
}

.segmented button[aria-pressed='true'] {
  color: var(--accent);
  background: var(--surface);
  box-shadow: 0 1px 2px rgb(7 32 76 / 0.1);
}

.segmented button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1.1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 12px;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:active {
  transform: translateY(1px);
}

.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.button[aria-pressed='true'] {
  color: var(--accent);
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
}

/* The log: newest first, with where each change came from */
.log {
  display: flex;
  flex-direction: column;
  gap: 4px;
  list-style: none;
}

.entry {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  padding: 6px 8px;
  font-size: 13px;
  color: var(--text);
  background: var(--tint);
  border-radius: 8px;
}

.origin {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}

.empty {
  padding: 6px 8px;
  font-size: 13px;
  color: var(--muted);
}

/* The pages and the panel: stacked, and side by side where there's room */
.viewer {
  container-type: inline-size;
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}

.stage,
.panel {
  flex: 1 1 100%;
  box-sizing: border-box;
  border: 1px solid var(--line);
  border-radius: 14px;
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.stage {
  height: 340px;
  background: var(--canvas);
}

.panel {
  max-height: 260px;
  overflow-y: auto;
  margin: 0;
  padding: 12px;
  background: var(--surface);
}

@container (min-width: 620px) {
  .stage {
    flex: 1 1 0;
    min-width: 0;
    height: 420px;
  }

  .panel {
    flex: 0 0 260px;
    max-height: 420px;
  }
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

**`ActivityLog.vue`**

```vue
<!-- Every change, once the engine has saved it, newest first. -->
<script setup lang="ts">
import { ref } from 'vue';
import { useAnnotationEvent } from '@embedpdf/vue/annotation';

interface Entry {
  id: number;
  text: string;
  origin: string;
}

const entries = ref<Entry[]>([]);
let nextId = 0;
const log = (text: string, origin: string) => {
  entries.value = [{ id: nextId++, text, origin }, ...entries.value].slice(0, 30);
};

useAnnotationEvent(
  (annotation) => annotation.onCreated,
  ({ annotation, origin }) =>
    log(`${annotation.author ?? 'Someone'} added a ${annotation.subtype}`, origin.kind),
);
useAnnotationEvent(
  (annotation) => annotation.onUpdated,
  ({ annotation, origin }) => log(`Changed a ${annotation.subtype}`, origin.kind),
);
useAnnotationEvent(
  (annotation) => annotation.onDeleted,
  ({ refs, origin }) =>
    log(
      `Deleted ${refs.length === 1 ? 'one annotation' : `${refs.length} annotations`}`,
      origin.kind,
    ),
);
useAnnotationEvent(
  (annotation) => annotation.onReordered,
  ({ order, origin }) => log(`Restacked ${order.length} on a page`, origin.kind),
);
</script>

<template>
  <ol class="panel log">
    <li v-if="entries.length === 0" class="empty">Nothing yet</li>
    <li v-for="entry in entries" :key="entry.id" class="entry">
      <span>{{ entry.text }}</span>
      <span class="origin">{{ entry.origin }}</span>
    </li>
  </ol>
</template>
```

**`AddRectangle.vue`**

```vue
<!-- On load: a rectangle on the cover, so the log starts with its event. -->
<script setup lang="ts">
import { computed, watch } from 'vue';
import { usePageList } from '@embedpdf/vue/runtime';
import { useAnnotation, useAnnotationState } from '@embedpdf/vue/annotation';

const annotation = useAnnotation();
const ready = useAnnotationState((state) => state.status === 'ready');
const pages = usePageList();
const cover = computed(() => pages.value[0]?.ref);

let added = false;
watch(
  [ready, cover],
  ([isReady, page]) => {
    if (!isReady || !page || added) return;
    added = true;
    void annotation.create(page, {
      subtype: 'square',
      box: { x: 96, y: 506, width: 178, height: 54 },
      color: '#e5484d',
      strokeWidth: 3,
    });
  },
  { immediate: true },
);
</script>

<template></template>
```

**`Toolbar.vue`**

```vue
<script setup lang="ts">
import { useInteraction, useInteractionState } from '@embedpdf/vue/interaction';
import { useAnnotation, useAnnotationState } from '@embedpdf/vue/annotation';

const TOOLS = [
  { id: 'pointer', label: 'Select' },
  { id: 'square', label: 'Rectangle' },
  { id: 'ink', label: 'Pen' },
];

const annotation = useAnnotation();
const interaction = useInteraction();
const { activeToolId } = useInteractionState();
const { selected } = useAnnotationState();
</script>

<template>
  <div class="toolbar">
    <div class="segmented" role="group" aria-label="Tool">
      <button
        v-for="tool in TOOLS"
        :key="tool.id"
        type="button"
        :aria-pressed="activeToolId === tool.id"
        @click="interaction.activateTool(tool.id)"
      >
        {{ tool.label }}
      </button>
    </div>
    <button
      type="button"
      class="button"
      :disabled="selected.length === 0"
      @click="annotation.selection.delete()"
    >
      Delete
    </button>
  </div>
</template>
```

> **Try it:** draw, move and delete. Each change is logged once the engine has saved it, as
> `local`: this viewer made it.

The composable listens while the component is mounted. Outside a component, call the event on the
plugin's API; it returns a function that stops listening:

```ts
const stop = annotation.onCreated(({ annotation }) => {
  /* … */
});
```

## Events

| Event               | Fires when                                                          | Carries                   |
| :------------------ | :------------------------------------------------------------------ | :------------------------ |
| `onCreated`         | An annotation was added.                                            | `annotation`, `origin`    |
| `onUpdated`         | An annotation was changed.                                          | `annotation`, `origin`    |
| `onDeleted`         | Annotations were deleted: one, plus its popup and replies.          | `refs`, `page`, `origin`  |
| `onReordered`       | The drawing order on a page changed.                                | `page`, `order`, `origin` |
| `onResynced`        | Annotations were read again, as after loading a document.           | `pages`                   |
| `onWriteFailed`     | The engine refused a change the user made.                          | `refs`, `error`           |
| `onSettingsChanged` | A setting changed, through `updateSettings()` or `resetSettings()`. | `settings`, `changed`     |

- **Once, and after it's saved.** `onCreated`, `onUpdated`, `onDeleted` and
  `onReordered` fire when the engine has saved
  the change, never while it's still pending. A change fires once, however
  many screens show it.
- **Loading isn't a list of creates.** When a document opens, its annotations arrive as one
  `onResynced`, not as an `onCreated` per
  annotation. The same happens after a change that touches whole pages, such as flattening or
  redaction. Then read the `pages` it names again with `list()`.
- **`annotation` is the record after the change.** It's what `get()` returns now.
- **`order` is the whole page.** After a reorder, `order` holds the page's annotations as refs,
  bottom to top, the same order `list({ pages: [page] })` returns.

The selection, a shape being drawn, text editing and hover have events of their own
(`onSelectionChanged`, `onDraftChanged`, `onEditingChanged`, `onHoverChanged`); see [Selecting & editing](selecting#events).
A tool's defaults have `tools.onDefaultsChanged`; see
[Tools & defaults](tools#events).

## Where a change came from

`origin` says who made the change:

| Field         | What it is                                                                    |
| :------------ | :---------------------------------------------------------------------------- |
| `origin.kind` | `'local'` when this viewer made the change, `'remote'` when someone else did. |
| `origin.sub`  | The user who made it (on a shared document).                                  |
| `origin.ts`   | When it happened, in milliseconds.                                            |
| `origin.tx`   | Shared by every change one call made, such as an import.                      |

**`remote-toast.vue`**

```vue
<script setup lang="ts">
import { useAnnotationEvent } from '@embedpdf/vue/annotation';
import { toast } from './toast'; // your own

useAnnotationEvent(
  (annotation) => annotation.onUpdated,
  ({ annotation, origin }) => {
    if (origin.kind === 'remote') toast(`${annotation.author} changed a comment`);
  },
);
</script>

<template>
  <slot />
</template>
```

These are the engine's own events, so the fields mean the same as on
[Events](https://www.embedpdf.com/docs/engine/concepts/events).

## Save the PDF

Annotations are part of the document. Save the document and they're in the file:

```ts
const bytes = await documents.download(); // the whole PDF, with every annotation
```

**`App.vue`**

```vue
<script setup lang="ts">
import { DocumentGate, Viewer } from '@embedpdf/vue/runtime';
import type { OpenInput } from '@embedpdf/vue/runtime';
import { Stage, stagePlugin } from '@embedpdf/vue/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/vue/render';
import { interactionPlugin } from '@embedpdf/vue/interaction';
import { AnnotationLayer, annotationPlugin } from '@embedpdf/vue/annotation';
import { localEngine } from '@embedpdf/engine';
import AddRectangle from './AddRectangle.vue';
import SaveBar from './SaveBar.vue';

import '../download.css';

const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin(), interactionPlugin(), annotationPlugin()];

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};
</script>

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: ebook }]">
    <DocumentGate>
      <template #fallback><p class="loading">Loading…</p></template>
      <AddRectangle />
      <SaveBar />
      <Stage class="stage">
        <template #page>
          <RenderLayer />
          <AnnotationLayer />
        </template>
      </Stage>
    </DocumentGate>
  </Viewer>
</template>
```

**`download.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --text: #3b4b7a;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #054fb3;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The controls, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1.1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 12px;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:active {
  transform: translateY(1px);
}

.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.button[aria-pressed='true'] {
  color: var(--accent);
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
}

/* Pushes what follows to the right */
.spacer {
  flex: 1 1 auto;
}

.readout {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

**`AddRectangle.vue`**

```vue
<!-- On load: a rectangle on the cover, so there's a change to save. -->
<script setup lang="ts">
import { computed, watch } from 'vue';
import { usePageList } from '@embedpdf/vue/runtime';
import { useAnnotation, useAnnotationState } from '@embedpdf/vue/annotation';

const annotation = useAnnotation();
const ready = useAnnotationState((state) => state.status === 'ready');
const pages = usePageList();
const cover = computed(() => pages.value[0]?.ref);

let added = false;
watch(
  [ready, cover],
  ([isReady, page]) => {
    if (!isReady || !page || added) return;
    added = true;
    void annotation.create(page, {
      subtype: 'square',
      box: { x: 96, y: 506, width: 178, height: 54 },
      color: '#e5484d',
      strokeWidth: 3,
    });
  },
  { immediate: true },
);
</script>

<template></template>
```

**`SaveBar.vue`**

```vue
<!-- The annotations are part of the document: the downloaded PDF has every one. -->
<script setup lang="ts">
import { saveFile, useDocument, useDocuments } from '@embedpdf/vue/runtime';
import { useInteraction, useInteractionState } from '@embedpdf/vue/interaction';

const documents = useDocuments();
const interaction = useInteraction();
const { activeToolId } = useInteractionState();
const { hasUnsavedChanges } = useDocument();

async function download() {
  saveFile(await documents.download(), 'ebook-annotated.pdf');
}
</script>

<template>
  <div class="toolbar">
    <button
      type="button"
      class="button"
      :aria-pressed="activeToolId === 'ink'"
      @click="interaction.activateTool(activeToolId === 'ink' ? 'pointer' : 'ink')"
    >
      Pen
    </button>
    <button type="button" class="button" @click="download">Download the PDF</button>
    <span class="spacer" />
    <output class="readout">{{ hasUnsavedChanges ? 'Unsaved changes' : 'All saved' }}</output>
  </div>
</template>
```

A save waits for pending changes, so what you save is what the user sees. For the difference
between a quick save that appends the changes and a full rewrite, see
[Saving](https://www.embedpdf.com/docs/engine/documents/saving).

## Export and import

To take annotations out of a document, as data rather than inside a PDF, export them. One call
gives you a **bundle**: every annotation with the bytes it carries (a stamp's drawing, an
attached file). One call puts a bundle into a document. `AnnotationTransfer` comes from
`@embedpdf/vue/annotation`:

```ts
const bundle = await annotation.export(); // everything
const text = AnnotationTransfer.stringify(bundle); // one JSON text, ready to store

// later, in the same document or another one
const { annotations, dropped } = await annotation.import(await AnnotationTransfer.parse(text), {
  pages: 'by-position',
});
```

Use it to:

- keep annotations in your own database, apart from the PDF;
- carry a review over to a new version of a document;
- let a user paste annotations from one document into another.

**`App.vue`**

```vue
<script setup lang="ts">
import { DocumentGate, Viewer } from '@embedpdf/vue/runtime';
import type { OpenInput } from '@embedpdf/vue/runtime';
import { Stage, stagePlugin } from '@embedpdf/vue/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/vue/render';
import { interactionPlugin } from '@embedpdf/vue/interaction';
import { AnnotationLayer, annotationPlugin } from '@embedpdf/vue/annotation';
import { localEngine } from '@embedpdf/engine';
import Transfer from './Transfer.vue';

import '../export-import.css';

const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin(), interactionPlugin(), annotationPlugin()];

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};
</script>

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: ebook }]">
    <DocumentGate>
      <template #fallback><p class="loading">Loading…</p></template>
      <div class="viewer">
        <Stage class="stage">
          <template #page>
            <RenderLayer />
            <AnnotationLayer />
          </template>
        </Stage>
        <Transfer />
      </div>
    </DocumentGate>
  </Viewer>
</template>
```

**`export-import.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --text: #3b4b7a;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #054fb3;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The controls, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1.1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 12px;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:active {
  transform: translateY(1px);
}

.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.button[aria-pressed='true'] {
  color: var(--accent);
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
}

.readout {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* The bundle, as text */
.transfer {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.transfer .toolbar {
  margin-bottom: 0;
}

.bundle {
  flex: 1 1 auto;
  min-height: 140px;
  box-sizing: border-box;
  width: 100%;
  resize: vertical;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11px;
  line-height: 1.45;
  color: var(--text);
  background: var(--canvas);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px;
}

.bundle:focus-visible {
  outline: none;
  border-color: var(--focus);
  box-shadow: 0 0 0 3px rgb(125 182 255 / 0.35);
}

/* The pages and the panel: stacked, and side by side where there's room */
.viewer {
  container-type: inline-size;
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}

.stage,
.panel {
  flex: 1 1 100%;
  box-sizing: border-box;
  border: 1px solid var(--line);
  border-radius: 14px;
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.stage {
  height: 340px;
  background: var(--canvas);
}

.panel {
  max-height: 260px;
  overflow-y: auto;
  margin: 0;
  padding: 12px;
  background: var(--surface);
}

@container (min-width: 620px) {
  .stage {
    flex: 1 1 0;
    min-width: 0;
    height: 420px;
  }

  .panel {
    flex: 0 0 280px;
    max-height: 420px;
  }
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

**`Transfer.vue`**

```vue
<!-- The bundle as one JSON text: what you'd store in your own database. -->
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { usePageList } from '@embedpdf/vue/runtime';
import {
  AnnotationTransfer,
  useAnnotation,
  useAnnotationList,
  useAnnotationState,
} from '@embedpdf/vue/annotation';

const annotation = useAnnotation();
const ready = useAnnotationState((state) => state.status === 'ready');
const pages = usePageList();
const cover = computed(() => pages.value[0]?.ref);
const annotations = useAnnotationList();
const text = ref('');
const status = ref('');

async function exportAll() {
  const bundle = await annotation.export(); // everything
  text.value = AnnotationTransfer.stringify(bundle);
  status.value = `Exported ${bundle.items.length}`;
}

async function deleteAll() {
  await Promise.all(annotation.list().map((each) => annotation.delete(each.ref)));
  status.value = 'Deleted them all';
}

async function importText() {
  try {
    const { annotations: imported, dropped } = await annotation.import(
      await AnnotationTransfer.parse(text.value),
    );
    status.value = `Imported ${imported.length}, left out ${dropped.length}`;
  } catch (error) {
    status.value = error instanceof Error ? error.message : String(error);
  }
}

// On load: a note and a rectangle on the cover, exported at once.
let started = false;
watch(
  [ready, cover],
  ([isReady, page]) => {
    if (!isReady || !page || started) return;
    started = true;
    void Promise.all([
      annotation.create(page, {
        subtype: 'text',
        rect: { x: 470, y: 232, width: 20, height: 20 },
        contents: 'Can we shorten the title?',
        color: '#facc15',
      }),
      annotation.create(page, {
        subtype: 'square',
        box: { x: 96, y: 506, width: 178, height: 54 },
        color: '#e5484d',
        strokeWidth: 3,
      }),
    ]).then(exportAll);
  },
  { immediate: true },
);
</script>

<template>
  <div class="panel transfer">
    <div class="toolbar">
      <button type="button" class="button" @click="exportAll">Export</button>
      <button type="button" class="button" :disabled="annotations.length === 0" @click="deleteAll">
        Delete all
      </button>
      <button type="button" class="button" :disabled="text === ''" @click="importText">
        Import
      </button>
    </div>
    <output class="readout">{{ status }}</output>
    <textarea
      v-model="text"
      class="bundle"
      aria-label="The exported bundle"
      spellcheck="false"
    ></textarea>
  </div>
</template>
```

> **Try it:** **Delete all**, then **Import**: they come back from the text. Import again: the
> annotations with a name are left out, since they're there already.

Export waits for pending changes, and imported annotations show at once, like any other change.

What to export:

```ts
await annotation.export({ refs: annotation.selection.list().map((a) => a.ref) }); // the selection
await annotation.export({ pages: [page.ref] }); // one page
```

A selection brings what it needs: a reply brings the comment it answers, and a comment brings its
replies.

How to import:

| Option        | What it does                                                                                     | Default     |
| :------------ | :----------------------------------------------------------------------------------------------- | :---------- |
| `pages`       | Where each page's annotations go: `'same'` (the same page), `'by-position'`, or a list of pairs. | `'same'`    |
| `attribution` | `'restore'` keeps who wrote each annotation, `'stamp'` makes the importing user the author.      | `'restore'` |

Use `'stamp'` when a user pastes annotations: they become theirs. `'restore'` writes annotations
in other people's names, so it needs a special permission; keep it for trusted jobs, such as
loading from your own database.

The result tells you what happened:

- **`annotations`**: the new annotations, in bundle order.
- **`refMap`**: each annotation's ref in the bundle, and its new ref here.
- **`dropped`**: what was left out, and why. For example, an annotation whose name is already
  taken on its page. Importing the same bundle twice doesn't duplicate annotations with a name.

The engine's [Import & export](https://www.embedpdf.com/docs/engine/annotations/import-export) page has every option,
reason and limit.

## Keep annotations in your own database

There are two ways to store annotations outside the PDF.

**Store a bundle per document.** Export when the user saves, store the text, and import it when
the document opens again. It's one row per document and one call each way:

```ts
// on save
await api.put(`/reviews/${docId}`, AnnotationTransfer.stringify(await annotation.export()));

// on open, once the annotations are loaded (status 'ready')
const text = await api.get(`/reviews/${docId}`);
if (text) await annotation.import(await AnnotationTransfer.parse(text));
```

**Store each annotation as it changes.** Listen for the events and write each change to your
server. Key your rows by the annotation's ref:

**`sync.vue`**

```vue
<script setup lang="ts">
import { annotationKey, useAnnotationEvent } from '@embedpdf/vue/annotation';
import { api } from './api'; // your own

useAnnotationEvent(
  (annotation) => annotation.onCreated,
  ({ annotation, origin }) => {
    if (origin.kind === 'local')
      api.put(`/annotations/${annotationKey(annotation.ref)}`, annotation);
  },
);
</script>

<template>
  <slot />
</template>
```

Handle `onUpdated` the same way, and `onDeleted` by deleting each of its `refs`. On
`onResynced`, compare the pages it names with `list()`. Only send `local` changes: a `remote` one was already saved by whoever made it.

### Which id to store

Store the annotation's `ref`. It's plain data, and it stays valid after the document is saved
and opened again. To use it as a key in a map or a URL, turn it into a string with
`annotationKey(ref)`. See [Refs & identity](https://www.embedpdf.com/docs/engine/concepts/identity).

## Good to know

### Unsaved changes

To warn before someone leaves with annotations that aren't downloaded yet, use
`hasUnsavedChanges` from `useDocument()`; see [Warn before changes are lost](../documents/saving#warn-before-changes-are-lost).

### Events on a shared document

The local engine's events come from this tab: the changes made here, by the user or your code.

> **Review one document together, live**
>
> With CloudPDF, annotations are stored on your server as people make them, and everyone with the
> document open gets each other's changes as events, with `origin.kind: 'remote'`. The server checks
> that people only change what they may, such as their own annotations. Your code stays the same: the
> same events, the same `list()`.
>
> [Collaborate with CloudPDF](https://www.cloudpdf.com/docs/headless/vue/annotations/saving)
