# Changelog

What changed in each release.

The package follows semver. Until `1.0.0`, a minor version (`0.x.0`) may break the API; each
breaking change is listed under its release.

## v0.4.0

October 22, 2026.

### New features

- **[Cropper](/docs/components/cropper)**, at `@logic-ui/react/cropper`: an image cropper
  ported from advanced-cropper and react-advanced-cropper, in one headless entry point.
  `Cropper.Root`, `Viewport`, `Image`, `Stencil`, `Handle`, `Grid` and `Preview` render no
  styles: they expose `data-*` attributes and CSS variables. One pointer gesture engine resizes
  from corners and edges, moves the crop, pans the image, pinches and zooms with the wheel.
  `ZoomIn`, `ZoomOut`, `Rotate`, `Flip` and `Reset` act on it, the keyboard moves, resizes
  and zooms, and `actionsRef` draws the crop with `getCanvas()`. `stencilSize` keeps the crop
  a fixed size on screen while the image moves behind it, and `Cropper.Viewport`'s padding
  keeps the crop clear of bars laid over it.

## v0.3.0

October 12, 2026.

### Breaking changes

- **`Lightbox.Media`'s `width` and `height` set the box's shape** when both are given, so every
  slide can keep the same shape. The loaded media's size is used only when they are missing.
- **`Lightbox.Media` keeps the `object-fit` you set** on your media. It is contained only when
  you set none, instead of always.

### New features

- `fill` on `Lightbox.Media` makes the box fill the slide, inside its padding, whatever the
  shape of the media. The flights and the pull to dismiss use that box.

### Fixes

- A flick past the first or last item springs back at the normal pace, instead of snapping
  back in about 30 ms.
- A zoomed image keeps its zoom until it has slid out of sight, instead of snapping to 1x
  while it slides out. Swiping back to it before then keeps its zoom.

## v0.2.0

October 12, 2026.

### Breaking changes

- **Built on `@base-ui/react` 1.9.0** (was 1.8.0) and `@base-ui/utils` 0.5.0. Select,
  Combobox and Autocomplete unmount on close whether or not `actionsRef` is set; call
  `eventDetails.preventUnmountOnClose()` in `onOpenChange` to keep them mounted.
  `onItemHighlighted` receives the originating event. See the
  [Base UI 1.9.0 release notes](https://base-ui.com/react/overview/releases/v1-9-0).
- **`Gallery` is now [Lightbox](/docs/components/lightbox)**, at `@logic-ui/react/lightbox`.
  Every part, type, hook and constant is renamed (`GalleryRoot` → `LightboxRoot`,
  `GALLERY_ZOOM_SCALE` → `LIGHTBOX_ZOOM_SCALE`), and the CSS variable
  `--gallery-dismiss-progress` is now `--lightbox-dismiss-progress`. The lightbox is the
  full-screen viewer only: the page around the triggers is yours.
- **Items are picked by value, not by index.** `Root` takes `value` / `defaultValue` /
  `onValueChange` (the value of the item shown, never `null`: it stays on the last item after
  closing, and defaults to the first) instead of `index` / `defaultIndex` / `onIndexChange`.
  Whether the viewer is open stays apart, in `open` / `defaultOpen` / `onOpenChange`. An item's
  value is its `id`, or what the new `itemToValue` returns. `Trigger` takes `value` instead of
  `index`; pressing it calls `onValueChange` when it shows another item, then `onOpenChange`.
  The open item stays open when items are added, removed or reordered.
- **`Item` is now `Slide`**, and takes no `index`: return it from the function passed to
  `Viewport`. With thumbnails, the slides are their tab panels.
- **`Image` is replaced by `Media`**, which wraps your own `<img>`, `<video>` or `<iframe>`:
  `<Lightbox.Media width={photo.width} height={photo.height}><img src={photo.src} alt={photo.alt} /></Lightbox.Media>`.
  It takes the natural `width` and `height`; the `src` and `alt` go on your element. The
  whole box flies and follows the pull to dismiss, and only an `<img>` zooms. `LightboxImageStatus` is
  `LightboxMediaStatus`.
- **Items can have any shape.** `GalleryItemData` is removed. The parts take what they need
  as props, and `Description` takes a function child to show the item's caption:
  `{(photo) => photo.caption}`.
- **`Value` is now `Counter`.**
- **`ZoomValue`'s function child receives its state**, `{(state) => state.scale}`, instead of
  the scale.
- **Removed `List` and `More`.** Lay out the triggers with your own elements.
- **Removed `Toolbar` and `Separator`.** Use Base UI's [Toolbar](https://base-ui.com/react/components/toolbar)
  and render each action as a `Toolbar.Button`: `} />`.
- **Removed `Delete` and `Download`.** Build them as buttons in the popup.
- **`Image`'s `data-loaded` is replaced** by `data-loading` and `data-error`, on `Media` and
  its `Slide`.
- **`data-dragging` is now `data-swiping`**, on `Popup`, `Backdrop`, `Viewport` and
  `ThumbnailList`, like Base UI's drawer. `ThumbnailList`'s state field is `swiping`.
- **`data-popup-open` marks the trigger the image will fly back to**: the one of the item shown,
  or the one `closeTarget` picks. Before, it marked every trigger of the item shown.
- **`onValueChange` and `onOpenChange` receive `eventDetails`** as their second argument, like
  Base UI's callbacks.
- **Removed `loadImage`, `loadLightboxImage` and `LIGHTBOX_PRELOAD_TIMEOUT`.** The viewer loads
  its media itself: before it opens, it is mounted out of sight until the `<img>` or `<video>` in
  `Media` has loaded (a video, its size), for up to `LIGHTBOX_OPEN_TIMEOUT` (3 s). The trigger
  has `data-pending` meanwhile. The thumbnail and the viewer can show different URLs with no
  extra code.
- **`Viewport` and `Slide` no longer clip.** Add `overflow-hidden` to the viewport in a viewer
  smaller than the screen.
- **Closed sets are string unions, not enums, named after their part like Base UI's.** Write the
  strings (`"loading"`, `"open"`, `"left"`) instead of `LightboxMediaStatus.Loading` and the
  like. `LightboxPhase` is now `LightboxRootPhase`, and `LightboxActivationDirection` is
  `LightboxRootActivationDirection`. `ThumbnailList`'s `orientation` is typed
  `LightboxThumbnailListOrientation`.
- **The CSS variable namespaces end in `CssVariables`**, like Base UI's:
  `LightboxPopupCssVariables` instead of `LightboxPopupCssVars`, and the same for `Backdrop`,
  `Slide`, `ThumbnailIndicator` and `Viewport`.

### New features

- **`onOpenChange` can return a Promise**, and the viewer waits for it: up to 3 s before it
  opens, 300 ms before it closes (`LIGHTBOX_OPEN_TIMEOUT`, `LIGHTBOX_CLOSE_TIMEOUT`). Use it
  to get something ready, such as moving a live element into the viewer.
  `eventDetails.cancel()` works until the wait ends, after an `await` too, and a cancelled
  change never happened.
- `Lightbox.createHandle()` links triggers placed outside `Root` to it, like Base UI's detached
  triggers, and opens or closes the lightbox from code.
- **Any media.** `Media` shows a video or an embed as well as an image, and reads its loading
  state from the media's own events. A video pauses when its slide is no longer shown.
- `ThumbnailList` and `Thumbnail`: a strip of thumbnails inside the viewer, a tab list that is
  a single tab stop, with `aria-selected` on the item shown. `ThumbnailIndicator` is one
  element that glides to the active thumbnail, with its box as CSS variables.
- `Caption`: a slide's own text, which moves with it and describes it for screen readers.
- **Animation attributes.** `Popup`, `Viewport`, `Slide`, `Title`, `Description`, `Counter`,
  `Caption` and `ThumbnailIndicator` have `data-activation-direction` (`left`, `right` or
  `none`), the side the item shown came from. `Title` and `Description` with a function
  child, `Counter` and `Caption` have `data-starting-style` for a frame when the item
  changes, to animate the new text in. `Title` takes a function child, like `Description`.
- `Popup`'s `className`, `style` and `render` functions receive `zoomed`, `controlsHidden` and
  `activationDirection`. `Viewport` is a labelled carousel region.
- Content marked with Base UI's `data-base-ui-swipe-ignore` keeps its drags, such as a 3D model
  that rotates: the viewer doesn't swipe or pull to dismiss from it. `"x"` or `"y"` keeps only the
  drags along that axis.
- The arrow keys stay with a widget inside the popup that uses them itself, such as a Base UI
  toolbar, a text field or a video player, instead of also changing the image.
- **Where the image lands.** `closeTarget` on `Root` picks the trigger the image flies back to
  and focus returns to: `"active"` (the item shown, the default), `"opener"` (the trigger that
  opened the viewer) or `"nearest"` (the nearest item with a trigger, for a "+5" tile).
  `scrollToTrigger` (on by default) scrolls the page behind the viewer to keep that trigger in
  view.
- **Crossfade.** When the image flies to or from a trigger that shows something else (another
  item, or a trigger with the new `crossfade` prop, such as an avatar or a tile with a count), a
  copy of the trigger flies along and fades, so the image grows out of its look and turns back
  into it on the way home. `Trigger` has `data-crossfade`.
- **Change reasons.** `eventDetails.reason` tells what changed the item
  (`"swipe"`, `"trigger-press"`, `"escape-key"` …), and `eventDetails.cancel()` keeps the
  current item: a cancelled swipe or pull to dismiss springs back. `onOpenChangeComplete` is
  called once the image has landed.
- **A smoother flight.** It is computed on screen, so the shape and corners change evenly;
  a flight cut short starts the next from where the image is; after a pull to dismiss, the
  flight back starts at the speed of the finger. `--lightbox-flight-duration` and
  `--lightbox-flight-easing` on `Popup` and `Backdrop` let transitions go with it, and
  `--lightbox-flight-movement-x`, `-y` and `--lightbox-flight-scale` on `Popup` tell where the
  image flies, so other content can go there too.
- **Gesture variables.** `Popup` gets `--lightbox-swipe-movement-x`, `--lightbox-swipe-progress`
  and `--lightbox-swipe-strength` during a swipe, `--lightbox-zoom-scale` and
  `--lightbox-zoom-progress`, and, with `Backdrop`, `--lightbox-dismiss-movement-x` and `-y`.
- **Custom transitions.** Each `Slide` has `--lightbox-slide-progress`, its distance from the
  centre in slides, and is placed with `translate`, leaving `transform` free for a cube, a fade
  or a coverflow. `Viewport`'s `neighbors` renders more items on each side (1 by default), and `--lightbox-swipe-distance` on it sets how far the finger travels for one slide, for slides that sit closer together than the viewport is wide.
- Changing `value` while the viewer is open slides to the new item, as Next does.
- `Popup` takes focus when it opens, so its keys work at once; pass `initialFocus` to focus
  another element.
- Optional props accept `undefined`, for apps with `exactOptionalPropertyTypes`.

### From Base UI 1.9.0

- Every component exports its `data-*` attributes and CSS variables as namespaces, such as
  `DialogPopupDataAttributes` and `DialogPopupCssVariables`.
- `Menu` can filter its items with `Menu.FilterProvider` and `Menu.Input` (preview).
- `data-base-ui-swipe-ignore` takes `"x"` or `"y"` in Drawer too.

## v0.1.0

October 8, 2026. The first release.

### New components

- **[Gallery](/docs/components/lightbox)** (now Lightbox): thumbnails that open a full-screen image viewer.
  The image flies out of its thumbnail and back, and the viewer handles swipe, pinch to zoom,
  double tap, pan, pull to dismiss and the keyboard. Comes with toolbar parts for navigation,
  zoom, download and delete.
- **[Clipboard](/docs/components/clipboard)**: a button that copies a value and shows that it
  was copied. `Root`, `Trigger` and `Indicator` expose `data-copied`.

### Core components

- Dialogs, menus, popovers, selects, tabs and the other core components, each under its own
  path, such as `@logic-ui/react/dialog`.
- `@logic-ui/react/utils` exports the hooks the primitives are built on, such as
  `useControlled`, `useStableCallback` and `useTimeout`.

### Requirements

- React 19.
- Built on `@base-ui/react` 1.8.0.
