<div id={@id}>
  <div class="flex flex-col gap-2">
    <%!-- Optional heading --%>
    <div :if={@title} class="flex items-center gap-2">
      <span class="text-xs font-medium text-base-content/60">{@title}</span>
    </div>

    <%!-- Thumbnail grid via the canonical <.draggable_list> primitive.
         Grid layout (cols configurable), aspect-square thumbnails, featured
         badge on the first item (opt-in), hover-revealed remove + preview
         buttons. Add-tile sits as the trailing grid cell so the picker is
         visually part of the gallery. --%>
    <.draggable_list
      id={"#{@id}-grid"}
      items={@selected}
      item_id={& &1}
      on_reorder="reorder_images"
      target={"##{@id}"}
      cols={@cols}
      draggable={not @readonly and length(@selected) > 1}
    >
      <:item :let={uuid}>
        <div class="relative group aspect-square">
          <%!-- `image_set`'s class attr is a plain string (not a class list),
               so the rotation transform is interpolated; a nil (unrotated)
               reads as "". --%>
          <.image_set
            file_uuid={uuid}
            variants={Map.get(@variants_map, uuid, [])}
            alt=""
            class={"w-full h-full object-cover rounded-lg border-2 border-base-300 #{rotation_class(%{rotation: Map.get(@rotations_map, uuid)})}"}
            loading="eager"
          />

          <%!-- Featured badge: only the first item in @selected, only when
               the consumer opted in via featured_first={true}. --%>
          <div
            :if={@featured_first and uuid == List.first(@selected)}
            class="absolute top-1 left-1 badge badge-primary badge-xs h-auto pointer-events-none"
          >
            {gettext("Featured")}
          </div>

          <%!-- Remove button (hover-revealed, top-right) — hidden in readonly. --%>
          <button
            :if={not @readonly}
            type="button"
            class="sortable-ignore absolute top-1 right-1 btn btn-xs btn-circle btn-error opacity-0 group-hover:opacity-100 transition-opacity"
            data-role={"remove-image-#{@id}-#{uuid}"}
            phx-click="remove_image"
            phx-value-uuid={uuid}
            phx-target={@myself}
            title={gettext("Remove")}
          >
            <.icon name="hero-x-mark" class="w-3 h-3" />
          </button>

          <%!-- Eye / preview button (hover-revealed, bottom-right). Available
               in readonly mode too — lightbox is a view-only feature. --%>
          <button
            type="button"
            class="sortable-ignore absolute bottom-1 right-1 btn btn-xs btn-circle opacity-0 group-hover:opacity-100 transition-opacity"
            data-role={"preview-image-#{@id}-#{uuid}"}
            phx-click="preview_image"
            phx-value-uuid={uuid}
            phx-target={@myself}
            title={gettext("Preview")}
          >
            <.icon name="hero-eye" class="w-3 h-3" />
          </button>
        </div>
      </:item>

      <%!-- Add tile is omitted entirely (the slot itself is conditional) once the
           selection reaches its limit, so draggable_list renders no trailing cell
           at all — not even an empty wrapper. --%>
      <:add_button :if={not @readonly and not selection_at_limit?(@selected, @mode, @max_count)}>
        <button
          type="button"
          class="w-full aspect-square border-2 border-dashed rounded-lg transition-all flex flex-col items-center justify-center gap-1 text-xs font-medium border-base-300 hover:border-primary hover:bg-base-200 cursor-pointer text-base-content/60 hover:text-primary"
          data-role={"open-picker-#{@id}"}
          phx-click="open_picker"
          phx-target={@myself}
        >
          <.icon name="hero-plus" class="w-6 h-6" />
          <span>{gettext("Add")}</span>
        </button>
      </:add_button>
    </.draggable_list>

    <%!-- Media selector modal (embedded, self-contained). Only mounted once
         the user clicks the Add tile — this avoids rendering a live_component
         node in static/test contexts where `show_picker` is false and no
         LiveView process exists. --%>
    <.live_component
      :if={not @readonly and @show_picker}
      module={MediaSelectorModal}
      id={"#{@id}-media-selector"}
      show={true}
      mode={@mode}
      selected_uuids={@selected}
      max_select={@max_count}
      file_type_filter={:image}
      scope_folder_id={@scope_folder_id}
      phoenix_kit_current_user={@phoenix_kit_current_user}
      notify={{__MODULE__, @id}}
    />
  </div>

  <%!-- Lightbox — delegated to MediaViewer, inside the single root so morphdom patches it reliably --%>
  <.live_component
    :if={@preview_uuid}
    module={PhoenixKitWeb.Components.MediaViewer}
    id={"#{@id}-viewer"}
    files={@selected}
    current={@preview_uuid}
    current_user={@phoenix_kit_current_user}
    notify={{__MODULE__, @id}}
  />
</div>
