> For the complete documentation index, see [llms.txt](https://docs.amee.thiennguyen.dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.amee.thiennguyen.dev/customization/sdk-reference.md).

# The window\.amee SDK Reference

Exposed as a page global (`window.amee`), not something you `import` — skins are loaded as dynamically-constructed blob-URL ES modules, and bare package imports don't resolve from a blob URL without extra setup. One small, documented global is simpler than requiring one.

| Member                                                          | Signature                                                                                     | Notes                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| --------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `version`                                                       | `string`                                                                                      | SDK contract version (currently `"1"`). Bumped only on a breaking change to this table.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `getNowPlaying()`                                               | `() => NowPlaying \| null`                                                                    | Last-known now-playing state; `null` before the first update arrives or when nothing's playing.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `onNowPlaying(cb)`                                              | `(cb: (np: NowPlaying \| null) => void) => () => void`                                        | Subscribe to changes; call the returned function to unsubscribe.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `play()` / `pause()` / `togglePlay()` / `next()` / `previous()` | `() => Promise<void>`                                                                         | Playback transport controls.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `seek(seconds)`                                                 | `(seconds: number) => Promise<void>`                                                          | Seeks to an absolute position in the current track.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `openNowPlayingApp()`                                           | `() => Promise<void>`                                                                         | Brings the app actually playing the current track to the foreground (Chrome, Music, Spotify, whatever `getNowPlaying().bundle_identifier` reports) — launches it if it isn't running. Rejects if nothing's currently playing.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `toggleNowPlayingApp()`                                         | `() => Promise<void>`                                                                         | Like `openNowPlayingApp()`, but toggles: if the now-playing app is already frontmost, jumps back to whatever app the user was in before. Otherwise remembers the current frontmost app and opens the now-playing app. If the user manually switches to some other app in between, the next call just opens the now-playing app again — the previously remembered app is forgotten (unless "Always return to the app I toggled from" is on in Settings). Rejects if nothing's currently playing.                                                                                                                                                                                                                       |
| `getVolume()`                                                   | `() => Promise<number>`                                                                       | System output volume, `0`–`1`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `setVolume(v)`                                                  | `(v: number) => Promise<void>`                                                                | Clamped to `0`–`1` internally.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `getMuted()` / `setMuted(v)`                                    | `() => Promise<boolean>` / `(v: boolean) => Promise<void>`                                    | System mute state.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `onVolumeChange(cb)`                                            | `(cb: (state: {volume, muted}) => void) => () => void`                                        | Fires for *any* change — your slider, physical volume keys, Control Center, another app.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `getOutputDevices()`                                            | `() => Promise<OutputDevice[]>`                                                               | Lists output-capable Core Audio devices — built-in speakers, USB, Bluetooth, AirPlay, HDMI. AirPlay/Bluetooth speakers show up here like any other device once connected — see the gotcha below for why a separate AirPlay button API exists anyway.                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `setOutputDevice(id)`                                           | `(id: string) => Promise<void>`                                                               | Switches the system default output device. Rejects if `id` no longer refers to a connected device.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `onOutputDevicesChange(cb)`                                     | `(cb: (devices: OutputDevice[]) => void) => () => void`                                       | Fires on any change to the list or the current default — plug/unplug, Bluetooth connect/disconnect, or the user switching outputs in System Settings/Control Center.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `attachAirPlayButton(element)`                                  | `(element: Element) => () => void`                                                            | Makes `element`'s rect the hit-target for Apple's real AirPlay picker popover — see the [gotcha below](#a-gotcha-getoutputdevices-cant-see-an-airplay-speaker-thats-never-been-selected). Renders Apple's own AirPlay glyph directly on `element` (already reflects active-route state on its own) — size/position an empty element as your button and this fills it in. Re-syncs to `element`'s rect on resize; call the returned cleanup function when done with it (e.g. on unmount). Only one native hit-target exists per mini-player window — a second call for a different element just moves it there.                                                                                                        |
| `onSpectrum(cb)`                                                | `(cb: (bins: number[]) => void) => () => void`                                                | Same FFT magnitude data the built-in visualizer draws from, if you want to build your own. No events fire until `startVisualizer()` has been called and resolved.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `startVisualizer()`                                             | `() => Promise<void>`                                                                         | Enables the system audio tap that feeds `onSpectrum` — triggers a macOS system-audio-recording permission prompt the first time it's called. Rejects if the permission is denied or the tap fails to start.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `stopVisualizer()`                                              | `() => Promise<void>`                                                                         | Stops the tap started by `startVisualizer()`. Safe to call even if it's not running.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `onResize(cb)`                                                  | `(cb: (size: {width, height}) => void) => () => void`                                         | Fires immediately with the current content-area size (CSS px), then again on every resize. A no-op stream (fires once, never again) for a skin whose manifest isn't `resizable: true`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `getToken(name)`                                                | `(name: string) => string`                                                                    | Reads a theme CSS custom property's current resolved value, e.g. `getToken("--accent")` → `"#8b7cff"`. Lets a skin adapt to whatever [theme](/customization/themes.md) the user has chosen.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `startWindowDrag()`                                             | `() => void`                                                                                  | Starts an OS-level window drag. Call from a `mousedown` handler on whichever part of your markup should double as a drag handle — Amee doesn't impose a fixed drag region, you decide what's draggable.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `activateWindow()`                                              | `() => Promise<void>`                                                                         | Requests real OS-level focus for the mini-player window — activates Amee (foreground/Dock) and steals keyboard focus from whatever app the user was using. A system-wide side effect unlike anything else in this table — see the gotcha below before calling it from hover.                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `onWindowFocusChange(cb)`                                       | `(cb: (focused: boolean) => void) => () => void`                                              | Fires whenever the mini-player window's own OS focus state changes — `true` on gaining real focus, `false` the moment it moves to another window/app. The generic fix for any skin-drawn popover (a dropdown menu, an open panel): there's no DOM blur/focus event for "the user clicked into some other app," so without this a popover left open just stays open forever once Amee loses focus. Close it yourself on `false`.                                                                                                                                                                                                                                                                                       |
| `openSettings()`                                                | `() => Promise<void>`                                                                         | Shows and focuses Amee's settings/dashboard window — the same window the tray menu's "Settings…" item opens. It runs hidden in the background otherwise, so this is the only way for a skin to reach it.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `checkForUpdate()`                                              | `() => Promise<UpdateCheckResult>`                                                            | Checks whether a newer Amee release is published. Returns `{ available, currentVersion, latestVersion, notes }`. Unlike the Settings banner, ignores any version the user clicked "Skip this version" for, so it can report `available: true` for a release Settings is suppressing. Doesn't download or install anything — pair a positive result with `openSettings()` so the user can update from there. Rejects on network/signature failure.                                                                                                                                                                                                                                                                     |
| `quit()`                                                        | `() => Promise<void>`                                                                         | Quits Amee entirely — same effect as the tray menu's "Quit Amee" item. No confirmation prompt; the app closes immediately once this resolves (subject to graceful shutdown if your manifest opts in).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `hide()`                                                        | `() => Promise<void>`                                                                         | Hides the mini-player window without quitting Amee — same effect as the tray icon's manual hide. Bring it back via the tray icon or `activateWindow()`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `onShutdown(cb)`                                                | `(cb: () => void \| Promise<void>) => void`                                                   | Registers a callback to run when Amee is about to quit — see [Graceful shutdown](/customization/skins.md#graceful-shutdown). Only fires if your manifest sets `graceful_shutdown: true`; best-effort, bounded by `graceful_shutdown_timeout_ms`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `onShow(cb)`                                                    | `(cb: () => void) => () => void`                                                              | Fires when the mini-player transitions from hidden to visible — auto-hide un-hiding after playback resumes, or the user clicking the tray icon to bring it back. Doesn't fire on a redundant show while already visible. Use it for a re-entrance flourish (replaying your opening animation, say).                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `onHide(cb)`                                                    | `(cb: () => void \| Promise<void>) => () => void`                                             | Fires right before the mini-player is actually hidden (from `hide()` or auto-hide). The window waits for every registered callback to settle before hiding — use it to prime your UI back to a "ready to reappear" state. Best-effort, bounded to a couple hundred ms on the Rust side.                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `isFnKeyHeld()`                                                 | `() => Promise<boolean>`                                                                      | Whether the physical Fn key is currently held down. macOS only (rejects on other platforms). Fn has no web-platform representation, so this is backed by a native monitor.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `onFnKeyChange(cb)`                                             | `(cb: (held: boolean) => void) => () => void`                                                 | Fires every time the physical Fn key is pressed or released. macOS only (never fires on other platforms). Useful for modifier-held alternate actions — e.g. the `classic` skin's menu shows "Quit" while Fn is held, "Hide" otherwise.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `getDominantColors(count?)`                                     | `(count?: number) => Promise<string[]>`                                                       | Analyzes the current track's album artwork and returns its dominant color(s) as `"#rrggbb"` hex strings, most dominant first. `count` defaults to `1`. Rejects if no artwork is currently available.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `getSkinAsset(path)`                                            | `(path: string) => Promise<string>`                                                           | Resolves a file bundled in your own package (relative to its root) to a ready-to-use `data:` URI, e.g. `img.src = await amee.getSkinAsset("cover.png")`. A path that tries to escape the package is rejected.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `openSkinWindow(entry, options?)`                               | `(entry: string, options?: SkinWindowOptions) => Promise<void>`                               | Opens a normal, decorated window running any other entry file from your own package — see [Extra windows and storage](/customization/skins.md#extra-windows-and-per-skin-storage). Calling it again with the same `entry` focuses the existing window instead of opening a duplicate.                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `storage.get(key)` / `storage.set(key, value)`                  | `(key: string) => Promise<T \| undefined>` / `(key: string, value: unknown) => Promise<void>` | Free-form per-skin key-value storage.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `storage.onChange(cb)`                                          | `(cb: (key: string) => void) => () => void`                                                   | Fires whenever `storage.set()` is called for your skin from *any* of its windows, including this one.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `expandWindowFlyout(extraLogicalPx)`                            | `(extraLogicalPx: number) => Promise<{direction: "up" \| "down", extra: number}>`             | Temporarily grows the (undecorated, fixed-size) mini-player window by `extraLogicalPx` so a popover you draw can pop out above/below your own UI instead of being clipped by the window frame. `direction` is whichever side actually had room on the current monitor; `extra` is how many logical px were actually applied (clamped to a sane range). Idempotent while already expanded — collapse first if you need to request a different size. Only resolves once the native move/resize has actually landed — see the gotcha below. If you know upfront how much room your popover will ever need, prefer declaring `content_height` in your manifest instead — no window move/resize, no round trip, no gotcha. |
| `collapseWindowFlyout()`                                        | `() => Promise<void>`                                                                         | Restores the geometry `expandWindowFlyout()` changed. Safe to call even when nothing is currently expanded.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `reportContentExtent(above, below)`                             | `(extraAbove: number, extraBelow: number) => Promise<void>`                                   | If your manifest declares `content_height`, tells Amee how much of the reserved dead-space band above/below your content your UI is *currently* actually occupying (0 when idle). Report the real extent whenever it changes (e.g. on hover-in/hover-out) or clicks on the visible part of your popover in that band get swallowed as click-through. No-op for a skin without `content_height`.                                                                                                                                                                                                                                                                                                                       |
| `onPointerMove(cb)`                                             | `(cb: (pos: {x, y} \| null) => void) => () => void`                                           | Cursor position in this window's own DOM coordinate space (same units as `MouseEvent.clientX`/`clientY`), or `null` once it's left the window — keeps firing regardless of whether Amee is the focused app. See the gotcha below on why you'd reach for this instead of native hover events. Prefer `trackHover` for the common case.                                                                                                                                                                                                                                                                                                                                                                                 |
| `trackHover(element, opts?)`                                    | `(element: Element, opts?: {className?, onEnter?, onLeave?}) => () => void`                   | `:hover`-alike built on `onPointerMove` that keeps working while Amee is in the background. Toggles `opts.className` (default `"amee-hover"`) on `element` based on its `getBoundingClientRect()`, and calls `opts.onEnter`/`opts.onLeave` at the same transitions. Returns a cleanup function.                                                                                                                                                                                                                                                                                                                                                                                                                       |

`NowPlaying` is `{ title, artist, album, artwork_data_uri, playing, elapsed_seconds, duration_seconds, bundle_identifier }` (all nullable except `playing`).

`OutputDevice` is `{ id, name, kind, is_default }`, where `id` is a stable Core Audio device UID string (not a raw device index — pass it back verbatim to `setOutputDevice`) and `kind` is one of `"builtin" | "usb" | "bluetooth" | "bluetoothLE" | "airplay" | "hdmi" | "other"`.

`UpdateCheckResult` is `{ available, currentVersion, latestVersion, notes }` — `available` is `boolean`, `currentVersion` is the running build's version string (e.g. `"1.4.0"`), `latestVersion` and `notes` are `string | null` (null when no update is available).

## A gotcha: `getOutputDevices()` can't see an AirPlay speaker that's never been selected

`getOutputDevices()` enumerates real Core Audio device objects — but macOS only creates one for an AirPlay speaker once it's been selected as the output at least once, and tears it down again the moment the user switches away from it. Until then, it simply doesn't exist as far as any Core Audio API (this one included) can see, no matter how you poll. That's not a bug here; it's how AirPlay routing on macOS actually works — Control Center's own AirPlay icon works around it by not using Core Audio enumeration at all, instead relying on Apple's separate, non-queryable AirPlay route-discovery mechanism exposed only through a real, clickable native button (`AVRoutePickerView`).

`amee.attachAirPlayButton(element)` is that button, positioned over `element` (rendering Apple's own AirPlay glyph there — it already reflects active-route state on its own), so a real click there opens Apple's picker showing every AirPlay receiver currently discoverable — not just ones `getOutputDevices()` already knows about. `getOutputDevices()`/`onOutputDevicesChange` are still useful alongside it for anything else your skin wants to know (e.g. listing non-AirPlay outputs), just not for drawing this particular icon.

## A gotcha: `:hover` doesn't update while Amee is unfocused

Native CSS `:hover` (and the `mouseenter`/`mouseleave`/`mouseover`/`mouseout` DOM events that drive it) only reflect the real cursor while Amee is the *focused* app. macOS doesn't run hit-testing for a window that isn't key, so the moment the user clicks into some other app, the mini player — despite still being the topmost, always-on-top window on screen — stops seeing real hover input entirely. This isn't an Amee bug to work around per-skin; it's a platform limitation every skin runs into for any hover-driven UI (a flyout, a tooltip, a hover-to-reveal button), so the SDK solves it once: `amee.onPointerMove` tracks the cursor natively regardless of focus, and `amee.trackHover` wraps it as a drop-in `:hover` replacement.

To make a hover effect keep working while Amee is unfocused, add a companion rule keyed off `trackHover`'s class next to your existing `:hover` one (or switch to it outright) — **track and toggle the class on the element that actually grows**, not a static ancestor it grows past:

```css
.my-popover:hover,
.my-popover.amee-hover {
  /* ... */
}
```

```js
amee.trackHover(myPopover, {
  onEnter: () => {/* same as your old "mouseenter" handler */},
  onLeave: () => {/* same as your old "mouseleave" handler */},
});
```

`trackHover` hit-tests via plain `getBoundingClientRect()` — unlike native `:hover`, which bubbles from a hovered descendant up to every ancestor regardless of the ancestor's own box. If your popover is a `position: absolute` child that visually grows past a smaller, non-resizing wrapper, and you track the *wrapper* instead of the popover itself, you'll get a spurious `onLeave` the instant the cursor moves off the wrapper's small box and onto the still-visible popover. Track the popover element directly instead.

See the bundled `classic` skin's volume flyout (`main.js`/`App.css`) for a complete example — it tracks hover on the capsule that actually expands, not the fixed-size row it sits in. `:focus-within`/`document.activeElement` aren't affected by any of this — keyboard focus state is plain DOM state, not something that depends on real-time hit-testing.

`trackHover` alone already fixes hover *visually* without any focus cost — reach for `amee.activateWindow()` (below) on top of it only when a hovered control should also be genuinely interactive while Amee is unfocused (real keyboard input, a slider that should drag like a normal focused control), and you're knowingly accepting the trade-off described in the next gotcha.

## A gotcha: `activateWindow()` steals focus system-wide

`amee.activateWindow()` requests real OS-level focus for the mini-player window. Amee is a regular Dock app (no accessory/menu-bar activation policy), so under the hood this is AppKit's `makeKeyAndOrderFront:` — the same call any other app window's focus request would make. That means calling it doesn't just make the mini player's own webview more responsive, it **activates Amee**: brings it to the foreground/Dock and takes keyboard focus away from whatever app the user was just using, exactly as if they'd clicked Amee's Dock icon.

Calling it from hover (rather than gating it behind a click/drag-start) means that focus-steal can happen from mere mouse movement, with no click involved — weigh that before doing it. The bundled `classic` skin's volume flyout pairs `trackHover`'s `onEnter` with `amee.activateWindow()` so the slider that pops open is immediately a fully-focused, normally-draggable control, and accepts the focus-steal as the cost of that. If your hover effect is purely visual (a tooltip, a reveal-on-hover button) you don't need `activateWindow()` at all.

## A gotcha: `expandWindowFlyout`'s completion is a round trip away

If your own markup needs a compensating CSS shift while the window is expanded (e.g. to keep something visually anchored while the window grows *upward*), only apply it once `expandWindowFlyout()`'s promise has actually resolved — never speculatively beforehand. The promise doesn't resolve until the native move/resize has genuinely landed, so touching compensating CSS any earlier means it's now describing a window that doesn't exist yet, which reads as your content jumping to the wrong spot until the real window catches up a beat later. Same in reverse for `collapseWindowFlyout()` — reset any compensating CSS only *after* it resolves, not before.

## A gotcha: interactive controls inside a drag region

If you wire a container's `mousedown` to `amee.startWindowDrag()` and then nest an interactive control — a seek bar, a button — inside that same container, its clicks will also trigger a window drag unless you stop that. Call `e.stopPropagation()` in the control's own `mousedown` listener before it bubbles up:

```js
progressBar.addEventListener("mousedown", (e) => e.stopPropagation());
progressBar.addEventListener("click", (e) => { /* seek */ });
```

See the bundled `classic` skin's progress bar for a complete example.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.amee.thiennguyen.dev/customization/sdk-reference.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
