# `Fresco.Viewer`
[🔗](https://github.com/alexdont/fresco/blob/v0.5.9/lib/fresco/viewer.ex#L1)

Phoenix LiveView function component that mounts a Fresco viewer.

Renders a host `<div>` containing a stage `<div>` and an `<img>`. The
companion JS hook (`FrescoViewer` in `priv/static/fresco.js`) attaches
Pointer Events for unified mouse/touch/pen gestures, applies
`transform: translate3d(tx, ty, 0) scale(s)` on the stage element, and
publishes a handle to `window.Fresco.viewerFor(id)` so peer extensions
(Tessera, future Etcher) can attach.

The image is server-rendered inside the host so it appears immediately —
the hook reads `naturalWidth/Height` on mount and fits the image into
the viewport without any "blank box" flash.

## Usage

    <Fresco.viewer
      id="photo"
      src={~p"/uploads/photo.jpg"}
      class="w-full h-[80vh] rounded"
    />

## Interactions

- **Pan**: click/touch drag, arrow keys (after focusing the viewer)
- **Zoom**: mouse wheel (centered on cursor), pinch (two-finger on touch
  or trackpad), double-click (2× centered on cursor), `+`/`-` keys
- **Reset**: nav button, `0` key — fits the image to the viewport
- **Fullscreen**: nav button, `f` key — toggles native browser fullscreen

## Parent app setup

Import the JS hook and spread `FrescoHooks` into your LiveSocket hooks:

    import "../../deps/fresco/priv/static/fresco.js"

    let liveSocket = new LiveSocket("/live", Socket, {
      hooks: { ...window.FrescoHooks, ...colocatedHooks }
    })

# `viewer`

Renders a Fresco viewer for the given image source.

Companion JS hook attaches gesture handlers, fits the image to the
viewport, and publishes the handle for peer extensions.

## Attributes

* `id` (`:string`) (required) - DOM id; must be unique on the page.
* `src` (`:string`) (required) - URL of the image to display.

  The default behavior treats `src` as a plain image URL. Extensions can
  register source providers via `window.Fresco.registerSourceProvider/2`
  to intercept specific URL patterns; the bundled engine handles
  `{type: "image"}` sources and throws a clear error for anything else
  (Tessera-style tile sources are planned for a later release).

* `class` (`:string`) - CSS classes for the viewer container. Defaults to `"w-full h-96"`.
* `infinite_canvas` (`:boolean`) - When `true`, drops the default "image must cover viewport" clamp so the
  user can pan freely beyond the image edges and zoom out until the image
  is a thumbnail in the middle of an empty canvas. The viewer's
  background dot-grid (always present) becomes visible in the void around
  the image so it reads as "canvas," not "broken layout." Default
  `false` keeps the stock single-image viewer behavior — pan stays
  location-locked inside the image, zoom-out floor is fit-to-viewport.

  Pairs naturally with future layered overlays (e.g. Etcher annotations)
  that need to draw shapes, callouts, or labels in the white space
  around the image, Figma/Miro/Excalidraw style.

  Defaults to `false`.
* `theme` (`:atom`) - Color scheme for the viewer host background, dot grid, and nav
  buttons.

  - `:system` (default) — follow the OS / browser `prefers-color-scheme`.
  - `:light` — force light palette regardless of OS preference.
  - `:dark` — force dark palette regardless of OS preference.
  - `:inherit` — emit only the host structure; the parent app's CSS
    supplies the six `--fresco-*` custom properties. Use this to wire
    Fresco to a parent theme system (daisyUI, custom palettes, …).

  Theming is implemented as CSS custom properties on `.fresco-viewer`
  (`--fresco-bg`, `--fresco-grid-dot`, `--fresco-nav-bg`,
  `--fresco-nav-bg-hover`, `--fresco-nav-fg`, `--fresco-nav-focus`).

  Defaults to `:system`. Must be one of `:system`, `:light`, `:dark`, or `:inherit`.
* `zoom_floor` (`:float`) - Optional minimum zoom scale, in engine units (screen-px-per-image-px).
  When set, the engine clamps every zoom path — wheel, pinch,
  double-click, `fitBounds` — at this floor. `nil` (default) falls
  through to the engine's normal floor (`sFit` in clamped mode,
  `sFit * 0.05` in `:infinite_canvas` mode).

  Consumers can also set this at runtime via
  `handle.setZoomFloor(scale)` — useful when the floor depends on
  state that's known only after mount (e.g. a paged reader recomputing
  the floor each time the user navigates to a new page).

  Defaults to `nil`.
* `zoom_ceiling` (`:float`) - Optional maximum zoom scale. Symmetric to `:zoom_floor`. `nil`
  (default) uses the engine's default ceiling (8× natural pixel ratio
  capped by the 8192-px raster safety limit).

  Defaults to `nil`.
* `pan_locked` (`:boolean`) - When `true`, single-pointer pan gestures (mouse drag, touch drag,
  arrow keys) are suppressed. Two-pointer pinch still works for
  zoom. Useful for paged readers that want the page statically
  centered at fit and only allow pan once the user has zoomed in.

  Toggle at runtime via `handle.setPanLocked(true|false)` — typically
  in response to a `handle.on("zoom", …)` subscription that flips
  the lock as the user crosses the fit threshold.

  Defaults to `false`.
* `gestures` (`:list`) - Allowlist of enabled gestures. Atom list:
  `[:pan, :pinch, :wheel, :double_click, :keyboard]`. Default `nil`
  enables all. Omitted entries are disabled.

  Defaults to `nil`.
* `nav_buttons` (`:list`) - Allowlist of enabled built-in nav buttons. Atom list:
  `[:home, :zoom_in, :zoom_out, :rotate, :fullscreen]`.

  - `nil` (default) — every button enabled.
  - `[]` — every button **hidden**. Useful for consumers building
    their own chrome; wire your buttons to
    `handle.zoomIn()` / `handle.zoomOut()` / `handle.rotateBy(90)` /
    `handle.toggleFullscreen()` / `handle.requestHome()` to get
    identical behavior to the built-ins.
  - A subset list — only those buttons render.

  Defaults to `nil`.
* `initial_rotation` (`:integer`) - Initial rotation in degrees, snapped to one of `{0, 90, 180, 270}`
  at mount time. Pre-0.5.7 behavior (no rotation) corresponds to
  `0`. Consumers persisting a per-image rotation choice server-
  side pass it here so the first paint already shows the rotated
  content — no flash of unrotated → rotated.

  Runtime control via `handle.setRotation(deg)` /
  `handle.rotateBy(delta)`; the built-in `:rotate` nav button
  cycles `+90°` per click.

  Defaults to `0`.
* Global attributes are accepted.

---

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