# `PhoenixKit.Annotations`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.164/lib/phoenix_kit/annotations/annotations.ex#L1)

Context for `phoenix_kit_annotations` — drawn-on-image shapes created
via the Etcher overlay layer in the MediaBrowser modal.

Most callers won't use this directly — they go through
`PhoenixKit.Modules.Storage.EtcherAdapter` which implements the
`Etcher.Storage` behaviour and dispatches to this context. The module
is exposed so admin tooling, audits, or background workers can do
CRUD without reaching for the adapter.

# `attrs`

```elixir
@type attrs() :: map()
```

# `uuid`

```elixir
@type uuid() :: String.t()
```

# `create`

```elixir
@spec create(attrs()) ::
  {:ok, PhoenixKit.Annotations.Annotation.t()} | {:error, Ecto.Changeset.t()}
```

Create an annotation for a file.

`attrs` accepts both atom- and string-keyed maps (the latter is what
flows in from LiveView events). Keys: `:file_uuid`, `:kind`,
`:geometry`, optional `:creator_uuid`, `:style`, `:metadata`,
`:position`.

# `delete`

```elixir
@spec delete(uuid()) :: :ok | {:error, :not_found | Ecto.Changeset.t()}
```

Delete an annotation by UUID.

Cascades to any linked comments — i.e. comments on the annotation's
file that carry `metadata.annotation_uuid` pointing at this row. The
cascade is a hard delete: annotation comments are conceptually owned
by their annotation, so they go with it instead of lingering as
`[removed]` placeholders in the file's thread. `phoenix_kit_comment_media`
rows fall away via their `ON DELETE CASCADE`; the underlying media
files (referenced via `comment_media.file_uuid`) stay untouched —
they're library assets, not comment-owned. No-ops cleanly when
PhoenixKitComments isn't installed.

# `get`

```elixir
@spec get(uuid()) :: PhoenixKit.Annotations.Annotation.t() | nil
```

Fetch a single annotation by UUID, or nil.

# `has_annotations?`

```elixir
@spec has_annotations?(term()) :: boolean()
```

Returns true if the file has at least one annotation.

# `list_for_file`

```elixir
@spec list_for_file(uuid()) :: [PhoenixKit.Annotations.Annotation.t()]
```

List annotations for a file, ordered by `position` then insertion time.

# `list_for_file_with_previews`

```elixir
@spec list_for_file_with_previews(uuid()) :: [map()]
```

List annotations for a file together with a tooltip-friendly preview of
their comment thread. Each element is a map:

    %{
      annotation: %Annotation{},
      first_comment: %{content, author, thumbnail_url} | nil,
      comment_count: integer()
    }

Annotation comments are stored against the file (`resource_type: "file"`,
`resource_uuid: file_uuid`) with `metadata.annotation_uuid` pointing at
the annotation, so they show up in the file's main comments thread
alongside non-annotated discussion. This loader pulls every file
comment in one query and groups by that metadata key.

If the comments package isn't installed, returns the same shape with
`first_comment: nil` and `comment_count: 0` so callers always handle
one schema.

# `resolve_comment_resources`

```elixir
@spec resolve_comment_resources([uuid()]) :: %{required(uuid()) =&gt; map()}
```

Resolves `"file"` comment resources for the comments moderation admin.

Comments on files — including annotation discussions, which are anchored to
the file with `metadata.annotation_uuid` — carry `resource_type: "file"` and
`resource_uuid: file_uuid`. This maps those file uuids to the file's display
name and admin media path so the moderation list links to the file instead
of showing a bare uuid.

Registered as the `"file"` handler by `phoenix_kit_comments`'
`resolve_comment_resources/1` dispatch (gated on this module being loaded).

# `update`

```elixir
@spec update(uuid(), attrs()) ::
  {:ok, PhoenixKit.Annotations.Annotation.t()}
  | {:error, :not_found | Ecto.Changeset.t()}
```

Update an annotation's geometry / style / metadata / position.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
