Carousel Composer
alphav0.2.0Multi-item media post composer — drag in photos and videos, reorder them on a rail, and edit each through a shared editor panel.
Context
The authoring counterpart to the media-carousel viewer. Composes the shipped media-editor WITHOUT modifying it: a single editor instance is loaded serially with the selected item (never N at once, so it can't trip media-editor's multi-instance guard). The procomp owns the collection concerns — file intake (drop + multiple browse, MIME-inferred so there are no photo/video capture tabs), the ordered MediaCarouselItem model, the @dnd-kit reorderable thumbnail rail, the main preview, and the edit-panel lifecycle (Edit → mount media-editor in edit-only mode → flatten export back into the item). Edits flatten on apply so the rail/preview are always publish-ready; export() is pull-only. v0.1 intake is upload-only (library clamped) and video items are preview/reorder/remove + limited edit. Output is shaped to feed media-carousel. Primary downstream consumer: content-composer's post media slot (wired in content-composer v0.2 via the mediaCarouselSlot substrate).
Installation
pnpm dlx shadcn@latest init"registries": {
"@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}pnpm dlx shadcn@latest add @ilinxa/carousel-composerAdd -fixtures for dummy data:
pnpm dlx shadcn@latest add @ilinxa/carousel-composer-fixturesPreview
Seeded from remote URLs (CMS re-edit). Select a thumb, drag to reorder, press Edit on a photo. Video Edit is deferred to v0.2.
Demo source
Usage
When to use
Reach for CarouselComposer when an author needs to assemble one or more media items into an ordered set — an Instagram-style feed post, an album, a product gallery, or chat attachments. It mixes photos and videos in a single rail, reorders by drag-and-drop, and edits any item through a shared media-editor panel. For a single hero image/video, use media-editor directly; to display a finished set, use media-carousel.
Basic example
import { CarouselComposer } from "@/components/carousel-composer"
export function PostMedia() {
const [items, setItems] = React.useState([])
return (
<CarouselComposer
value={items}
onChange={setItems}
maxItems={10}
editorProps={{ enabledTools: ["crop", "filters", "adjust"] }}
/>
)
}Pull-only export
Edits flatten into each item on Done, so the rail and preview always show publish-ready media. Call ref.current.export() at publish time to get the committed ordered array (an open, unapplied edit is excluded — gate your publish button while editing).
Notes
- Intake is file-based — the file's MIME type decides
imagevsvideo, so there are no photo/video capture tabs. - All items share one aspect (Instagram behaviour).
aspectdefaults to"auto"(derived from item 1); override with a fixedAspectRatio. - Editing mounts a single
media-editorinstance, loaded serially per item — never N at once. - v0.1: video items support preview / reorder / remove; full video editing tracks media-editor maturity.
"library"source is clamped to upload-only. - Object URLs created during intake/edit are revoked on remove, replace, reset, and unmount.
Features
- Multi-file intake — drag-and-drop dropzone + multiple Browse, mixed image + video (MIME-inferred, no capture-mode tabs)
- Ordered MediaCarouselItem model — add / remove / reorder / select
- Reorderable thumbnail rail (@dnd-kit horizontal sortable) with pointer + keyboard reorder and a dedicated drag handle
- Main preview of the selected item (image cover-fit; video on a black mat)
- Single shared media-editor edit panel — Edit pushes the selected item in, flattens the export back on Done, reloads the same panel for the next item
- Shared aspect across the carousel (Instagram behaviour) — `aspect="auto"` derives from item 1, overridable
- Controlled / uncontrolled value (value / defaultValue / onChange) + imperative handle (getItems / export / addFiles / removeItem / select / openEditor / reset)
- Pull-only export() — items already flattened on edit-apply
- Per-file type + size validation; maxItems cap (default 10, Instagram parity)
- Object-URL lifecycle managed (revoked on remove / replace / reset / unmount)