Sign In

scrollsheet

Package Overview
Dependencies
Maintainers
1
Versions
7
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

scrollsheet - npm Package Compare versions

Comparing version
1.0.0-beta.2
to
1.0.0-beta.3
dist/auto/drawer-CY9M15SC.mjs

Sorry, the diff of this file is too big to display

+567
import * as React from "react";
//#region packages/scrollsheet/src/internal/detents.d.ts
/**
* Detents — the stops a sheet can rest at, in the spirit of
* UISheetPresentationController's `detents`.
*
* A detent resolves to the visible height of the sheet in px.
* Accepted forms:
* - 'full' → viewport height minus top inset
* - 'medium' → 50% of viewport
* - 'content' → natural content height (measured, capped at full)
* - number in (0, 1] → fraction of viewport
* - `${number}px` → absolute pixels
*/
type DetentSpec = "full" | "medium" | "content" | number | `${number}px`;
//#endregion
//#region packages/scrollsheet/src/motion/geometry.d.ts
/**
* Per-side axis geometry.
*
* The whole engine (content.tsx) is written once against an abstract
* "revealed px" concept: 0 = fully hidden, `maxDetent` = fully revealed. The
* DOM's scroll position is the mechanism, but which raw value corresponds to
* "revealed" depends on which canvas edge the panel is pinned to:
* bottom/right sit at the canvas's *far* end, so raw scroll IS revealed px
* directly; top/left sit at the *near* end (flush with the screen when fully
* open), so raw scroll is mirrored: revealed = maxDetent - rawScroll.
* `mapScroll` is the self-inverse conversion between the two spaces.
*/
type Side = "bottom" | "top" | "left" | "right";
//#endregion
//#region packages/scrollsheet/src/context.d.ts
/**
* onTravel's third argument. Reused and mutated in place every travel frame
* (see content.tsx's updateTravel) — read it synchronously, don't retain the
* reference across frames.
*/
interface TravelInfo {
/** [0, maxDetent] — the full resolved travel range in px, this frame. */
range: readonly [number, number];
/**
* 0-1 progress toward EACH configured detent, keyed by the detent's
* resolved height in px (not its spec — a spec like 'content' isn't a
* stable identity across resizes, but its resolved height this frame is
* what a consumer interpolates against).
*/
progressAtDetents: ReadonlyMap<number, number>;
}
//#endregion
//#region packages/scrollsheet/src/root.d.ts
/** Imperative escape hatch — see `actionsRef` on `<Sheet.Root>`. */
interface SheetActions {
open(): void;
close(): void;
/**
* Moves the sheet to `detent` through the same `setActiveDetent` path a
* controlled `activeDetent` change uses, so it animates identically. Warns
* in dev (once) if `detent` isn't one of this sheet's configured
* `detents` — only the panel's *rest position* resolves to the nearest
* configured detent in that case; `activeDetent`/`onActiveDetentChange`
* (and Handle's `aria-valuenow`/`aria-valuetext`) still reflect the
* literal value passed in here, not the resolved one.
*/
snapTo(detent: DetentSpec): void;
}
interface SheetRootProps {
children?: React.ReactNode;
/** Controlled open state. */
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
/**
* Fires once the open/close *transition* finishes (not just the state
* flip) — `true` when the sheet has fully settled open, `false` once it's
* fully closed and unmounted. Backed by the same phase timers that drive
* `data-scrollsheet-state`/`data-scrollsheet-side` (see Content) — no
* separate timer of its own.
*/
onOpenChangeComplete?: (open: boolean) => void;
/**
* Imperative escape hatch for cases a controlled `open`/`activeDetent`
* prop is awkward for (deep links, push notifications, a descendant
* component reaching up without prop-drilling): `ref.current?.close()` /
* `.open()` / `.snapTo(detent)`. Thin wrappers over the same `setOpen`/
* `setActiveDetent` the rest of the sheet uses — not a separate code path.
*/
actionsRef?: React.Ref<SheetActions>;
/**
* The stops the sheet can rest at, UISheetPresentationController-style.
* `'content'` (natural height), `'medium'` (50%), `'full'`, a 0–1 fraction,
* or `'320px'`.
* @default ['content']
*/
detents?: readonly DetentSpec[];
/** Controlled active detent (one of `detents`). */
activeDetent?: DetentSpec;
onActiveDetentChange?: (detent: DetentSpec) => void;
/**
* Master switch: when false, swipe/drag-to-dismiss, backdrop tap and Esc
* all stop closing the sheet. `escapeDismissible`/`backdropDismissible`
* below let you turn off just one of the pointer/keyboard paths while
* leaving the others (and swipe-to-dismiss, which only this prop gates)
* alone — both default to whatever `dismissible` is.
* @default true
*/
dismissible?: boolean;
/** Whether Esc closes the sheet. @default `dismissible` */
escapeDismissible?: boolean;
/** Whether tapping the backdrop/empty track closes the sheet. @default `dismissible` */
backdropDismissible?: boolean;
/** CSP nonce for the injected style tag. */
nonce?: string;
/**
* Blend the page's `<meta name="theme-color">` toward black as the sheet
* travels, so the browser chrome (iOS Safari tints both the status bar area
* and the bottom toolbar from this tag; Android tints the status bar) dims
* along with the backdrop. Every theme-color tag is dimmed from its own
* base color, so the usual media-scoped light/dark pair works and stays
* correct even if the OS scheme flips while the sheet is open. On a
* bottom-anchored attached sheet, the strip behind the bottom toolbar takes
* the panel's own color instead (via a fixed sentinel element), so the bar
* reads as part of the sheet, not the dimmed page. No-ops if no
* tag holds a parseable color, or on platforms that ignore theme-color.
* @default false
*/
themeColorDimming?: boolean;
/**
* Fires on every frame the sheet travels: revealed px, 0-1 progress
* (against the first detent, same number `--scrollsheet-progress` uses),
* and `info` — per-detent progress plus the resolved travel range. `info`
* is reused and mutated in place every frame (this is the highest-
* frequency callback in the library); read it synchronously, don't store
* the reference. Keep this handler cheap — it runs on every travel frame.
*/
onTravel?: (revealedPx: number, progress: number, info: TravelInfo) => void;
/** Which edge the sheet is anchored to. @default 'bottom' */
side?: Side;
/**
* When false, the page behind stays fully interactive and scrollable — no
* backdrop, no focus trap, no inert-ing the rest of the page. Renders on a
* `<div popover="manual">` (top layer, non-blocking) where supported,
* falling back to a non-modal `<dialog>` (`.show()`) otherwise.
* @default true
*/
modal?: boolean;
/**
* Applies an iOS-style card effect to the page element marked
* `data-scrollsheet-background`, driven by this sheet's own travel
* progress. `'scale'` scales/insets/rounds it; `'parallax'` only shifts it.
* No-ops if no element is marked.
*
* Left unset, a full-height bottom sheet in the mobile presentation gets
* `'scale'` — that is how the platform presents a sheet that covers the
* screen. Desktop drawers dock beside the page and never move it, and a
* detached (floating-card) sheet never claims the full screen, so neither
* defaults on. `'none'` opts out anywhere.
*/
backgroundEffect?: "scale" | "parallax" | "none";
/**
* Explicit target for backgroundEffect, instead of the default document-wide
* `[data-scrollsheet-background]` query. Pass the same ref you'd otherwise
* mark with the attribute. Skips the ownership check the implicit default
* applies (an explicit ref is unambiguous about which page element it
* means) — matches how an explicit `backgroundEffect` prop today already
* bypasses the implicit default's full-height/opener-containment check.
*/
backgroundRef?: React.RefObject<HTMLElement | null>;
/**
* Inner-content-overflow scrollbar style, for the panel's own scroll at
* max detent (or any content taller than the panel). `'overlay'`: a thin
* auto-hiding thumb (iOS/macOS-style). `'hidden'`: no thumb, no native
* scrollbar either. `'native'`: let the browser draw
* its own.
* @default 'overlay'
*/
scrollbar?: "overlay" | "hidden" | "native";
/**
* Scoped to exactly two things: the backdrop's opacity and
* `themeColorDimming`. Both stay fully transparent/undimmed while the
* sheet is at or below this detent's resolved height, then interpolate
* from there up to the next detent above it (or the tallest detent, if
* this is already the tallest). Unset (default): dims across the full
* range from closed to the first detent, today's behavior. Does *not*
* affect `onTravel`'s progress argument, `--scrollsheet-progress`,
* stacking recede, or `backgroundEffect` — those all track the sheet's
* full-range travel 1:1 regardless of this prop.
*/
largestUndimmedDetent?: DetentSpec;
/**
* Drag sessions (mouse, and touch on non-modal sheets) only start from
* `<Sheet.Handle>` — a pointerdown elsewhere on the panel no-ops. Modal
* touch (native scroll drives the gesture) is restricted via
* `touch-action` instead, except at the tallest detent, where the panel's
* own content scroll still needs it.
* @default false
*/
handleOnly?: boolean;
/**
* No drag sessions at all, handle included — detent changes are
* programmatic (`activeDetent`, `actionsRef`) or click/keyboard
* (`<Sheet.Handle>`) only. Esc and backdrop dismissal are unaffected.
* @default false
*/
disableDrag?: boolean;
/**
* A release never skips over an intermediate detent — the resolved target
* is clamped to the immediate neighbor of the detent the gesture started
* from (or, for a native-scroll settle, the last settled detent).
* @default false
*/
sequentialDetents?: boolean;
/**
* Fraction (0-1) of the first detent below which a release dismisses the
* sheet, replacing the built-in half-of-first-detent rule.
* @default 0.5
*/
closeThreshold?: number;
/**
* Bottom sheets only. When the software keyboard opens with a text field
* focused inside the sheet, promote to the tallest detent so the field
* has the whole remaining viewport to scroll within — a short peek detent
* can't show a field the keyboard would otherwise cover. Restores the
* previous detent on blur unless something else (a drag, a controlled
* change) moved the sheet in between. Gated on the keyboard actually
* appearing (a nonzero measured inset), never on focus alone, so a
* desktop click into an input doesn't expand anything. Promotion goes
* through the same `setActiveDetent` path a controlled change uses — a
* controlled sheet must apply `onActiveDetentChange` back to
* `activeDetent` or this prop has no visible effect.
* @default false
*/
keyboardExpands?: boolean;
/**
* Fires once a drag session resolves, with the real pointer event (the
* `buttons===0` recovery path may pass the triggering `pointermove`
* instead of a `pointerup`) and whether the sheet is staying open —
* `false` only when the release resolved to a dismiss.
*/
onRelease?: (event: PointerEvent, willRemainOpen: boolean) => void;
}
declare function Root$1({ children, open: openProp, defaultOpen, onOpenChange, onOpenChangeComplete, actionsRef, detents, activeDetent: activeDetentProp, onActiveDetentChange, dismissible, escapeDismissible, backdropDismissible, nonce, themeColorDimming, onTravel, side, modal, backgroundEffect, backgroundRef, scrollbar, largestUndimmedDetent, handleOnly, disableDrag, sequentialDetents, closeThreshold, keyboardExpands, onRelease }: SheetRootProps): React.JSX.Element;
//#endregion
//#region packages/scrollsheet/src/trigger.d.ts
interface SheetTriggerProps extends React.ComponentProps<"button"> {
/**
* Render the child element instead of a `<button>`, merging the trigger's
* props (aria, onClick) into it — for custom button components or links.
*/
asChild?: boolean;
}
declare const Trigger: React.ForwardRefExoticComponent<Omit<SheetTriggerProps, "ref"> & React.RefAttributes<HTMLButtonElement>>;
//#endregion
//#region packages/scrollsheet/src/content.d.ts
interface SheetContentProps extends React.ComponentProps<"div"> {
/** Accessible name when no <Sheet.Title> is rendered. */
"aria-label"?: string;
/**
* Stretches [data-scrollsheet-body] to fill the panel (flex column, the
* body itself flex:1/min-height:0) instead of sizing to its natural content
* height. Promotes the docs "Full-height content" recipe into the library —
* write your own inner-scroll child (flex:1, overflow-y:auto) with no CSS
* of your own required on the wrapper.
* @default false
*/
fill?: boolean;
/**
* Render the child element instead of scrollsheet's own panel `<div>`,
* merging the panel's props (role, tabIndex, ref, `data-scrollsheet-*`)
* into it — matches Radix `Dialog.Content asChild`: the child replaces the
* panel outright rather than nesting inside it. The child must be a single
* non-Fragment element; whatever it was given as its own children is
* hoisted into the body wrapper below (detent measurement and stacking
* still need a real DOM descendant there regardless of which element ends
* up as the panel).
* If `children` isn't a single non-Fragment element (missing, a string,
* multiple elements, a `<>...</>` Fragment…), `asChild` is ignored for
* that render — the default panel `<div>` is used instead, with a
* one-time dev warning.
* @default false
*/
asChild?: boolean;
}
declare const Content$1: React.ForwardRefExoticComponent<Omit<SheetContentProps, "ref"> & React.RefAttributes<HTMLDivElement>>;
//#endregion
//#region packages/scrollsheet/src/misc.d.ts
interface SheetTitleProps extends React.ComponentProps<"h2"> {
/** Render the child element instead of an `<h2>`, merging props into it. */
asChild?: boolean;
}
declare const Title: React.ForwardRefExoticComponent<Omit<SheetTitleProps, "ref"> & React.RefAttributes<HTMLHeadingElement>>;
interface SheetDescriptionProps extends React.ComponentProps<"p"> {
/** Render the child element instead of a `<p>`, merging props into it. */
asChild?: boolean;
}
declare const Description: React.ForwardRefExoticComponent<Omit<SheetDescriptionProps, "ref"> & React.RefAttributes<HTMLParagraphElement>>;
interface SheetCloseProps extends React.ComponentProps<"button"> {
/** Render the child element instead of a `<button>`, merging props into it. */
asChild?: boolean;
}
declare const Close: React.ForwardRefExoticComponent<Omit<SheetCloseProps, "ref"> & React.RefAttributes<HTMLButtonElement>>;
//#endregion
//#region packages/scrollsheet/src/handle.d.ts
interface SheetHandleProps extends React.ComponentProps<"button"> {
/** Render the child element instead of a `<button>`, merging props into it. */
asChild?: boolean;
/**
* Where the pill sits. `'inside'` (default) flows at the top of the
* sheet's content. `'floating'` overlays the content — absolutely
* positioned over the panel's top edge, for full-bleed content (maps,
* photos) a flow pill would push down. `'outside'` floats in the backdrop
* above the sheet's top edge (bottom sheets only; other sides render as
* `'inside'`) — it rides the canvas layer outside the panel's clip and
* drags, clicks, and keys exactly like the others.
* @default 'inside'
*/
variant?: "inside" | "floating" | "outside";
}
/**
* The grabber pill. Click cycles detents; ArrowUp/ArrowDown (or Left/Right
* for side sheets) move between them, Home/End jump to the first/last — so
* multi-detent sheets are fully keyboard-operable. With two or more detents
* it exposes itself as a slider (role, value min/max/now, value text) so
* screen readers announce the current stop, not just "button".
*
* The handle is optional. It's purely a click/keyboard affordance layered on
* top of the drag engine, which listens on the whole panel, not the handle —
* omit `<Sheet.Handle>` and the whole panel still drags and dismisses, in
* every mode (modal and non-modal) and every side (bottom/top/left/right).
* There's no boolean prop for this; omission is the API.
*/
declare const Handle$1: React.ForwardRefExoticComponent<Omit<SheetHandleProps, "ref"> & React.RefAttributes<HTMLButtonElement>>;
//#endregion
//#region packages/scrollsheet/src/drawer/index.d.ts
/** vaul's snap point shape: a 0–1 fraction, an absolute `'###px'` string, or `'fit-content'`. */
type VaulSnapPoint = number | string;
/**
* Resolves vaul's `fadeFromIndex` against `snapPoints` into a
* `largestUndimmedDetent` spec, mirroring real vaul's own default (its
* `src/index.tsx`: `fadeFromIndex = snapPoints && snapPoints.length - 1`) —
* omitted with `snapPoints` set defaults to the *topmost* snap point index
* (no dim until the last snap point), not "no undimmed range at all"
* (scrollsheet's own default when `largestUndimmedDetent` is never touched).
* No `snapPoints`, or an out-of-range explicit index, resolves to
* `undefined` (today's full-dim-range behavior) — pulled out as its own pure
* function so this index math is unit-testable independent of rendering.
*/
declare function resolveFadeFromIndex(snapPoints: readonly VaulSnapPoint[] | undefined, fadeFromIndex: number | undefined): DetentSpec | undefined;
/**
* Resolves vaul's `closeThreshold` against whether `snapPoints` is set,
* CONVERTING between opposite conventions: vaul counts the fraction dragged
* AWAY (its 0.25 default = dismiss after a quarter-height drag), scrollsheet
* counts the fraction still VISIBLE (`isBelowCloseThreshold`: dismiss when
* `revealed < firstDetent * closeThreshold`, so higher = easier). A raw
* passthrough inverts the migrated feel — vaul's 0.25 became "drag 75% to
* dismiss", harder than scrollsheet's own 0.5 default instead of easier —
* so the mapping is `1 - value`, and an omitted prop resolves to vaul's own
* `CLOSE_THRESHOLD` default of 0.25 (=> 0.75 here), never `Sheet.Root`'s
* unrelated 0.5.
* `snapPoints` set makes `closeThreshold` dead code in real vaul (its
* `onRelease` returns through the snap-points branch before ever reading
* it), matched here by always resolving to `undefined` in that case.
* Pulled out as its own pure function, mirroring `resolveFadeFromIndex`,
* so the conversion math is unit-testable independent of rendering.
*/
declare function resolveCloseThreshold(snapPoints: readonly VaulSnapPoint[] | undefined, closeThreshold: number | undefined): number | undefined;
/**
* Composes vaul's `onClose` on top of `onOpenChange`: `onClose` fires
* whenever the transition target is `false`, then `onOpenChange` always
* fires. Real vaul's `closeDrawer()` calls `onClose` from every dismiss
* path (swipe past threshold, Esc, backdrop, imperative); scrollsheet's own
* `onOpenChange` is already the single funnel every one of *its* dismiss
* paths goes through (`useControllableState`'s `onChange`, which fires
* exactly once per real open/false transition), so layering `onClose` on
* top of that wiring reproduces "every dismiss path" without a second one.
* Returns `undefined` when neither callback is given, matching the
* conditional-wrapper convention `onRelease`'s cast uses below. Pulled out
* as its own pure function so the composition is unit-testable without a
* live DOM/click event.
*/
declare function composeOpenChange(onOpenChange: ((open: boolean) => void) | undefined, onClose: (() => void) | undefined): ((open: boolean) => void) | undefined;
/**
* vaul's own props, translated — plus (via the Pick) the scrollsheet-native
* props that have no vaul counterpart and forward to `Sheet.Root`
* untranslated: `backdropDismissible` (the escape hatch vaul served with the
* Radix onPointerDownOutside/onInteractOutside preventDefault idiom),
* `escapeDismissible` (same for onEscapeKeyDown), `keyboardExpands`,
* `onTravel` (the live counterpart of vaul's ignored `onDrag`), `scrollbar`,
* and `actionsRef`. Only props with no vaul name-collision are forwarded —
* anything vaul also has (`closeThreshold`, `modal`, `dismissible`, …) keeps
* its translated vaul semantics above.
*/
interface DrawerRootProps extends Pick<SheetRootProps, "actionsRef" | "backdropDismissible" | "escapeDismissible" | "keyboardExpands" | "onTravel" | "scrollbar"> {
children?: React.ReactNode;
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
/**
* Fires whenever the drawer closes — every dismiss path (swipe past
* threshold, Esc, backdrop tap, imperative `close()`) funnels through the
* same `onOpenChange` wiring this is layered on top of, matching real
* vaul's `closeDrawer()`, which calls `onClose` on every one of those
* paths too.
*/
onClose?: () => void;
/** @default true */
dismissible?: boolean;
/** Fractions (0–1), `'###px'` strings, or `'fit-content'` — translated to scrollsheet `detents`. */
snapPoints?: readonly VaulSnapPoint[];
/** Optionally-controlled active snap point; `null` means "no explicit snap point". */
activeSnapPoint?: VaulSnapPoint | null;
setActiveSnapPoint?: (snapPoint: VaulSnapPoint | null) => void;
/** Fires when the open/close transition actually completes — wired to `onOpenChangeComplete`, exact rather than vaul's timer. */
onAnimationEnd?: (open: boolean) => void;
/** Ignored — the native `<dialog>` manages focus itself. */
autoFocus?: boolean;
/** Ignored — nesting is automatic: a `<Drawer.Content>` rendered inside another registers itself. */
nested?: boolean;
/** Maps to scrollsheet `side` — all four edges. */
direction?: "top" | "bottom" | "left" | "right";
/** Maps to scrollsheet `modal` — `false` renders non-modal (Popover top layer, page stays interactive). */
modal?: boolean;
/**
* Maps to `backgroundEffect="scale"`; vaul's own `[data-vaul-drawer-wrapper]`
* is picked up as the target. Left false (vaul's default), this maps to
* `'none'`, not unset — vaul never scales the page unless asked, so the
* scrollsheet default for full-height sheets must not leak in here.
*/
shouldScaleBackground?: boolean;
/**
* Maps to scrollsheet `closeThreshold` — but only when `snapPoints` is
* unset. In real vaul, `closeThreshold` is dead code once `snapPoints`
* exist (its `onRelease` returns through the snap-points branch before
* ever reading it); this compat layer matches that rather than giving the
* prop unconditional live effect. Passing both together warns once in dev
* — use the native `Sheet.Root closeThreshold` directly if you want it to
* combine with detents.
*
* Omitted (with `snapPoints` unset) resolves to vaul's own default of
* `0.25` (`CLOSE_THRESHOLD` in vaul's `src/constants.ts`) — not
* scrollsheet's own `0.5` default, which is twice the drag distance.
*/
closeThreshold?: number;
/**
* Maps to `largestUndimmedDetent`, resolved against `snapPoints` at this
* index. Omitted (with `snapPoints` set) defaults to `snapPoints.length -
* 1` — vaul's own default, meaning no dim until the topmost snap point —
* not "dim across the full range" (scrollsheet's own default when
* `largestUndimmedDetent` is never touched at all).
*/
fadeFromIndex?: number;
/** Maps to `sequentialDetents`. */
snapToSequentialPoint?: boolean;
/** Maps to `handleOnly` directly. */
handleOnly?: boolean;
/**
* Maps to `onRelease`. Typed as vaul's own `React.PointerEvent<HTMLDivElement>`
* (rather than scrollsheet's wider native-`PointerEvent` type) so a
* handler already typed against vaul's declaration still assigns cleanly —
* see the internal cast in `Root` for why this is safe at runtime.
*/
onRelease?: (event: React.PointerEvent<HTMLDivElement>, open: boolean) => void;
setBackgroundColorOnScale?: boolean;
noBodyStyles?: boolean;
disablePreventScroll?: boolean;
preventScrollRestoration?: boolean;
repositionInputs?: boolean;
scrollLockTimeout?: number;
onDrag?: (event: React.PointerEvent, percentageDragged: number) => void;
container?: HTMLElement | null;
/**
* Ignored — real vaul's `fixed` swaps its own keyboard-avoidance strategy
* (resize instead of translate); scrollsheet's `useKeyboardViewport` has
* no equivalent toggle.
*/
fixed?: boolean;
}
declare function Root(props: DrawerRootProps): React.JSX.Element;
/**
* Nested drawers just work: a `<Drawer.Content>` rendered inside another
* `<Drawer.Content>` automatically registers with the parent's stacking
* context (the parent recedes while the child is open), the way iOS stacks
* sheets. `NestedRoot` is an alias of `Root` kept for drop-in compatibility
* with vaul's API surface — there's nothing distinct for it to do.
*/
declare const NestedRoot: typeof Root;
type DrawerNestedRootProps = DrawerRootProps;
interface DrawerPortalProps {
children?: React.ReactNode;
/** Ignored — the native `<dialog>` always renders into the browser's top layer. */
container?: HTMLElement | null;
}
declare function Portal({ children, container }: DrawerPortalProps): React.JSX.Element;
type DrawerOverlayProps = React.ComponentProps<"div">;
/**
* scrollsheet draws its own backdrop (`.scrollsheet-backdrop`, themeable via
* the `--scrollsheet-backdrop` CSS variable) that tracks scroll progress, so
* there's nothing for a separate overlay element to render. This accepts
* vaul's `<Drawer.Overlay className .../>` so existing JSX doesn't crash —
* restyle the backdrop through the CSS variable instead.
*/
declare function Overlay(_props: DrawerOverlayProps): null;
/**
* Whether translated `snapPoints` produced more than one distinct detent —
* feeds Content's `data-vaul-snap-points` attribute. vaul's own
* CSS-selector convention treats a single snap point the same as none
* (scrollsheet's own single-detent default also resolves to `false` here).
* Pulled out as its own pure function, mirroring `resolveFadeFromIndex` /
* `resolveCloseThreshold`, so it's unit-testable independent of rendering
* (`<Drawer.Content>`'s actual DOM output is behind a client-only mount
* gate — see content.tsx — so SSR can't observe the attribute directly).
*/
declare function hasMultipleSnapPoints(detents: readonly DetentSpec[]): boolean;
interface DrawerContentProps extends SheetContentProps {
onPointerDownOutside?: (event: CustomEvent) => void;
onOpenAutoFocus?: (event: Event) => void;
onEscapeKeyDown?: (event: KeyboardEvent) => void;
onCloseAutoFocus?: (event: Event) => void;
onInteractOutside?: (event: CustomEvent) => void;
onFocusOutside?: (event: CustomEvent) => void;
forceMount?: boolean;
}
declare const Content: React.ForwardRefExoticComponent<Omit<DrawerContentProps, "ref"> & React.RefAttributes<HTMLDivElement>>;
/**
* Composes the click handler Handle passes to `SheetHandle`: the caller's
* own `onClick` always fires first, then — when `preventCycle` is set —
* `event.preventDefault()`, the same signal `SheetHandle`'s own
* click-to-cycle logic already checks (`if (event.defaultPrevented) return`
* in handle.tsx) to skip advancing to the next detent. Mirrors real vaul's
* `preventCycle`, read inside its own `handleCycleSnapPoints` guard. Pulled
* out as its own pure function so the composition is unit-testable without
* a live DOM/click event.
*/
declare function composeHandleClick(preventCycle: boolean | undefined, onClick: ((event: React.MouseEvent<HTMLButtonElement>) => void) | undefined): (event: React.MouseEvent<HTMLButtonElement>) => void;
interface DrawerHandleProps extends SheetHandleProps {
/**
* Suppresses the click-to-cycle-detents behavior — the handle still
* renders, drags, and is keyboard-operable as usual, but a click no
* longer advances to the next detent. Mirrors real vaul's own
* `preventCycle`, which guards the same click path (vaul's handle has no
* keyboard cycling to suppress).
*/
preventCycle?: boolean;
}
declare const Handle: React.ForwardRefExoticComponent<Omit<DrawerHandleProps, "ref"> & React.RefAttributes<HTMLButtonElement>>;
/**
* Sheet.Close's self-closing form renders a styled ✕ default; vaul's own
* `<Drawer.Close />` renders an empty unstyled button. A migrating user
* must not get a surprise icon button from a version swap, so the compat
* Close pins children to null (defined, so the styled-default branch never
* arms) unless the caller passed some.
*/
declare const DrawerClose: React.ForwardRefExoticComponent<Omit<SheetCloseProps, "ref"> & React.RefAttributes<HTMLButtonElement>>;
/** Namespace-style access matching vaul's `<Drawer.Root>…</Drawer.Root>` shape. */
declare const Drawer: {
Root: typeof Root;
NestedRoot: typeof Root;
Trigger: React.ForwardRefExoticComponent<Omit<SheetTriggerProps, "ref"> & React.RefAttributes<HTMLButtonElement>>;
Portal: typeof Portal;
Overlay: typeof Overlay;
Content: React.ForwardRefExoticComponent<Omit<DrawerContentProps, "ref"> & React.RefAttributes<HTMLDivElement>>;
Close: React.ForwardRefExoticComponent<Omit<SheetCloseProps, "ref"> & React.RefAttributes<HTMLButtonElement>>;
Title: React.ForwardRefExoticComponent<Omit<SheetTitleProps, "ref"> & React.RefAttributes<HTMLHeadingElement>>;
Description: React.ForwardRefExoticComponent<Omit<SheetDescriptionProps, "ref"> & React.RefAttributes<HTMLParagraphElement>>;
Handle: React.ForwardRefExoticComponent<Omit<DrawerHandleProps, "ref"> & React.RefAttributes<HTMLButtonElement>>;
};
//#endregion
export { SheetContentProps as A, Close as C, SheetTitleProps as D, SheetDescriptionProps as E, SheetRootProps as F, TravelInfo as I, Side as L, Trigger as M, Root$1 as N, Title as O, SheetActions as P, DetentSpec as R, SheetHandleProps as S, SheetCloseProps as T, composeOpenChange as _, DrawerHandleProps as a, resolveFadeFromIndex as b, DrawerPortalProps as c, NestedRoot as d, Overlay as f, composeHandleClick as g, VaulSnapPoint as h, DrawerContentProps as i, SheetTriggerProps as j, Content$1 as k, DrawerRootProps as l, Root as m, Drawer as n, DrawerNestedRootProps as o, Portal as p, DrawerClose as r, DrawerOverlayProps as s, Content as t, Handle as u, hasMultipleSnapPoints as v, Description as w, Handle$1 as x, resolveCloseThreshold as y };
import * as React from "react";
//#region packages/scrollsheet/src/toast/state.d.ts
/** Drives the row's icon/role and the `data-type` attribute the stylesheet reads. */
type ToastType = "default" | "success" | "error" | "warning" | "info" | "loading";
/**
* The six-value position union (matches real Sonner's own set). Every value
* routes for real at this layer (see `selectPositionToasts` below) — how
* many of the six a given `<Toaster>` actually renders is that component's
* own call (`toaster.tsx`'s `resolveToasterPosition`), not something this
* module gates.
*/
type ToastPosition = "top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right";
/** Compat alias for the pre-neutral-naming name — new code should use `ToastPosition`. */
type SonnerPosition = ToastPosition;
/**
* Real Sonner's own swipe-dismiss edge union (types.ts:152). A Toaster-level
* `swipeDirections` override (see `ToasterProps` in `../toaster`) understands
* these four values regardless of which position they're applied to — lives
* here, not in the shell folder, so both the Sheet-backed `toaster.tsx` and
* the new shell can reference the type without either depending on the
* other's own directory.
*/
type SwipeDirection = "top" | "right" | "bottom" | "left";
interface ToastAction {
label: React.ReactNode;
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
}
/** Per-slot extra class names, mirrors real Sonner's own `ToastClassnames` field-for-field. */
interface ToastClassnames {
toast?: string;
title?: string;
description?: string;
loader?: string;
closeButton?: string;
cancelButton?: string;
actionButton?: string;
success?: string;
error?: string;
info?: string;
warning?: string;
loading?: string;
default?: string;
content?: string;
icon?: string;
}
/** Per-type icon overrides, mirrors real Sonner's own `ToastIcons`. */
interface ToastIcons {
success?: React.ReactNode;
info?: React.ReactNode;
warning?: React.ReactNode;
error?: React.ReactNode;
loading?: React.ReactNode;
close?: React.ReactNode;
}
/** The `data` argument to `toast()` / `.success()` / etc — mirrors Sonner's own `ExternalToast`. */
interface ToastData {
/** Update-in-place: reusing an id already in the queue merges onto that entry instead of pushing a new one. */
id?: number | string;
description?: React.ReactNode;
/** Milliseconds before auto-dismiss, or `Infinity` to disable it. @default 4000 (Toaster's own `duration`, or `toastOptions.duration`) */
duration?: number;
/** @default true */
dismissible?: boolean;
icon?: React.ReactNode;
action?: ToastAction;
cancel?: ToastAction;
className?: string;
classNames?: ToastClassnames;
onDismiss?: (toast: ToastRecord) => void;
onAutoClose?: (toast: ToastRecord) => void;
/** Routes this toast to the `<Toaster toasterId>` with the matching id instead of the default, un-keyed toaster. */
toasterId?: string;
/** Which position's `<ol>` group this toast renders in — omitted routes to the owning `<Toaster>`'s own `position` prop instead (see `selectPositionToasts`). Valid regardless of which positions that Toaster actually renders. */
position?: ToastPosition;
/** Rendered as `data-testid` on the row — mirrors real Sonner's own `testId`. */
testId?: string;
/** Overrides the close button's accessible name for this toast only. @default "Close toast" (or the Toaster's own `toastOptions.closeButtonAriaLabel`) */
closeButtonAriaLabel?: string;
/**
* Inline styles for this toast's row element, merged over the row's own
* stacking variables and `toastOptions.style` (last wins, so `top`,
* `zIndex`, or any `--scrollsheet-toast-*` variable can be overridden
* per-toast) — mirrors real Sonner's own `ExternalToast.style`.
*/
style?: React.CSSProperties;
}
/** A live queue entry — what `useSonner()` and each row render from. */
interface ToastRecord extends ToastData {
id: number | string;
type: ToastType;
title?: React.ReactNode;
/** Set by `toast.custom()` — its presence means "render this node verbatim, skip the card chrome" (see ToastRow). */
jsx?: React.ReactNode;
createdAt: number;
}
/**
* A `success`/`error` handler may resolve to this instead of a plain node —
* `message` becomes the title, every other field (description, action,
* icon, ...) applies to the settled toast. Mirrors real Sonner's own
* `PromiseIExtendedResult` (state.ts's `isExtendedResult` check).
*/
type ToastPromiseExtendedResult = Omit<ToastData, "id"> & {
message?: React.ReactNode;
};
interface ToastPromiseData<T> extends Omit<ToastData, "id" | "description"> {
id?: number | string;
loading?: React.ReactNode;
success?: React.ReactNode | ((data: T) => React.ReactNode | ToastPromiseExtendedResult | Promise<React.ReactNode | ToastPromiseExtendedResult>);
error?: React.ReactNode | ((error: unknown) => React.ReactNode | ToastPromiseExtendedResult | Promise<React.ReactNode | ToastPromiseExtendedResult>);
/**
* A static value carries through to the settled toast unchanged; a
* function is called with the settle value (the resolved data, or the
* error/HTTP-status string on the error branches) — matches real Sonner's
* own per-branch description resolution (it never wipes a static one).
*/
description?: React.ReactNode | ((value: unknown) => React.ReactNode | Promise<React.ReactNode>);
finally?: () => void;
}
type ToastFn = ((message: React.ReactNode, data?: ToastData) => number | string) & {
success: (message: React.ReactNode, data?: ToastData) => number | string;
error: (message: React.ReactNode, data?: ToastData) => number | string;
info: (message: React.ReactNode, data?: ToastData) => number | string;
warning: (message: React.ReactNode, data?: ToastData) => number | string;
loading: (message: React.ReactNode, data?: ToastData) => number | string;
/** Alias for the base call, with no type — matches real Sonner's `toast.message()`. */
message: (message: React.ReactNode, data?: ToastData) => number | string;
/**
* `jsx` is a render function receiving the resolved id (matching real
* Sonner), so a custom toast can call `toast.dismiss(id)` on itself. The
* resulting node is stored on the record's `jsx` field, which ToastRow
* renders verbatim, skipping the card chrome (icon/title/description)
* entirely.
*/
custom: (jsx: (id: number | string) => React.ReactNode, data?: ToastData) => number | string;
promise: <T>(promiseOrFn: Promise<T> | (() => Promise<T>), data: ToastPromiseData<T>) => (number | string) & {
unwrap: () => Promise<T>;
};
dismiss: (id?: number | string) => number | string | undefined;
/** Every live (not yet dismissed) toast across every toaster — mirrors real Sonner's own `toast.getToasts()`. */
getToasts: () => readonly ToastRecord[];
/** Every toast ever created, oldest-first, capped at 100 — mirrors real Sonner's own `toast.getHistory()`. */
getHistory: () => readonly ToastRecord[];
};
declare const toast: ToastFn;
/**
* Subscribes to the same store `toast()` writes to. Real Sonner's own
* `useSonner()` takes no arguments and has no visible per-toaster filtering
* in its public surface — matched here unfiltered, across every toaster.
* Not verified byte-for-byte against Sonner's source; matches its observed
* behavior.
*/
declare function useSonner(): {
toasts: ToastRecord[];
};
/** Neutral-named alias for `useSonner` — same implementation, the name fresh docs teach. */
declare const useToasts: typeof useSonner;
//#endregion
//#region packages/scrollsheet/src/toast/toaster.d.ts
/** Pure, exported for tests. @default 3, matching real Sonner's own default. */
declare function resolveVisibleToasts(count: number | undefined): number;
interface ToasterProps {
/** Routes this Toaster to only the toasts created with a matching `toasterId` — omitted (the common case) renders the default, un-keyed queue. */
id?: string;
/** @deprecated Use `id` instead — matches real Sonner's own `Toaster` prop name. */
toasterId?: string;
/** Which corner (or edge-center) this Toaster's own default `<ol>` renders in. All six positions are real. @default 'bottom-right' */
position?: ToastPosition;
/**
* v1 always renders the static light card real Sonner ships by default.
* 'dark'/'system' aren't implemented yet (a v1.1 follow-up) and warn once
* rather than silently no-op — forcing a real visual mismatch would be
* worse than an honest warning.
*/
theme?: "light" | "dark" | "system";
/** Not implemented yet (v1.1) — warns once if set. */
richColors?: boolean;
/** Force the row list open (instead of the collapsed front-card-plus-ghosts) even without hover/focus. */
expand?: boolean;
/**
* Maximum toasts visible (interactive, full opacity) at once per position
* group. Beyond this, older toasts become hidden — `data-visible="false"`,
* still a real DOM node, still timing — rather than evicted; nothing is
* ever dismissed by overflow, and `dismissible: false` earns no special
* protection (it overflows into hidden exactly like any other toast).
* @default 3 (matches real Sonner's own visibleToasts default)
*/
visibleToasts?: number;
/** @default false */
closeButton?: boolean;
/** Milliseconds before auto-dismiss; a per-toast `duration` wins over this. @default 4000 */
duration?: number;
/** Gap between stacked rows, px. @default 14 */
gap?: number;
/** Distance from the viewport edge, px (or any CSS length). @default 24 (16 at the <600px full-bleed breakpoint) */
offset?: number | string;
/**
* Which edge(s) each toast may be swiped away from, overriding the
* per-position default (a corner position allows both its own y and x
* edges; a *-center position only its own y edge). One setting for the
* whole Toaster, applied uniformly to every position group it renders —
* matches real Sonner's own single Toaster-wide `swipeDirections` prop
* (it has no per-toast override).
*/
swipeDirections?: readonly SwipeDirection[];
/** Defaults merged underneath each individual `toast()` call's own options. */
toastOptions?: Omit<ToastData, "id" | "toasterId">;
/** Per-type icon overrides; a per-toast `icon` always wins over these. */
icons?: ToastIcons;
className?: string;
style?: React.CSSProperties;
/** Accessible name for the notifications region. @default "Notifications" */
containerAriaLabel?: string;
/**
* Global keyboard shortcut that expands the stack and moves focus into it
* — every field must be truthy on the event (a modifier like `altKey`, or
* a `KeyboardEvent.code` match), same contract as real Sonner's own
* `hotkey` prop. Escape while focus is inside the region collapses it
* back. Pass `[]` to disable.
* @default ['altKey', 'KeyT']
*/
hotkey?: readonly string[];
/** CSP nonce for the injected style tag. */
nonce?: string;
}
//#endregion
//#region packages/scrollsheet/src/toast/shell/toaster-shell.d.ts
declare function ToasterShell({ id, toasterId, position, theme, richColors, expand: forceExpand, visibleToasts: visibleToastsProp, closeButton, duration: durationProp, gap, offset, swipeDirections, toastOptions, icons, className, style, containerAriaLabel, hotkey, nonce }: ToasterProps): React.ReactPortal | null;
//#endregion
//#region packages/scrollsheet/src/toast/toast-styles.d.ts
/**
* Injects the toast stylesheet into a shadow root via adoptedStyleSheets
* instead of document.head — mirrors src/internal/styles.ts's
* injectStylesInto. Call once per shadow root that hosts a <Toaster>,
* before it first renders. A sibling export rather than folding into
* injectStylesInto: the toast layer is its own chunk (dist/toast.mjs) that
* never loads for a core-only consumer, so its shadow-root injector has to
* be reachable without pulling core's own chunk (and vice versa) — one
* shared call would break that tree-shaking boundary.
*
* Falls back silently to a <style> element appended to the shadow root
* itself on engines without constructable stylesheets (Safari <16.4).
* `nonce` only applies on that fallback path: it sets the injected
* `<style>` element's `nonce` attribute so a CSP `style-src` policy with
* `'nonce-...'` allows it. The `adoptedStyleSheets` path constructs a
* `CSSStyleSheet` and adopts it directly — it is never parsed as an inline
* style element, so CSP has no inline-style check to gate there and `nonce`
* is accepted (for a stable signature across both paths) but unused.
*/
declare function injectToastStylesInto(root: ShadowRoot, nonce?: string): void;
//#endregion
export { SonnerPosition as a, ToastData as c, ToastPromiseData as d, ToastRecord as f, useToasts as g, useSonner as h, resolveVisibleToasts as i, ToastIcons as l, toast as m, ToasterShell as n, ToastAction as o, ToastType as p, ToasterProps as r, ToastClassnames as s, injectToastStylesInto as t, ToastPosition as u };
"use client";
import { c as prefersReducedMotion, h as warnOnce, n as useCloseWatcher, r as createStyleInjector, t as cn } from "./cn-CActSFFr.mjs";
import * as React from "react";
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
import { createPortal } from "react-dom";
//#region packages/scrollsheet/src/toast/state.ts
let toasts = [];
const listeners = new Set();
let uid = 0;
const MAX_HISTORY_SIZE = 100;
let history = [];
function trimHistory() {
let toRemove = history.length - MAX_HISTORY_SIZE;
if (toRemove <= 0) return;
const liveIds = new Set(toasts.map((t) => t.id));
history = history.filter((record) => {
if (toRemove > 0 && !liveIds.has(record.id)) {
toRemove -= 1;
return false;
}
return true;
});
}
function recordHistory(record) {
const index = history.findIndex((t) => t.id === record.id);
if (index === -1) {
history = [...history, record];
trimHistory();
return;
}
history = history.map((t, i) => i === index ? record : t);
}
function nextId() {
uid += 1;
return uid;
}
function publish() {
for (const listener of listeners) listener();
}
function subscribe(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
}
function getSnapshot() {
return toasts;
}
const EMPTY_TOASTS = [];
function getServerSnapshot() {
return EMPTY_TOASTS;
}
function upsert(id, patch, fallbackType) {
const resolvedId = id ?? nextId();
const index = toasts.findIndex((t) => t.id === resolvedId);
const type = patch.type ?? fallbackType;
let record;
if (index === -1) {
record = {
dismissible: true,
...patch,
id: resolvedId,
type,
createdAt: Date.now()
};
toasts = [...toasts, record];
} else {
record = {
...toasts[index],
...patch,
id: resolvedId,
type
};
toasts = toasts.map((t, i) => i === index ? record : t);
}
recordHistory(record);
publish();
return resolvedId;
}
function dismiss(id) {
if (id === void 0) {
const swept = new Set(toasts);
for (const t of swept) {
if (!toasts.includes(t)) continue;
t.onDismiss?.(t);
}
toasts = toasts.filter((t) => !swept.has(t));
publish();
return;
}
const existing = toasts.find((t) => t.id === id);
if (!existing) return id;
existing.onDismiss?.(existing);
toasts = toasts.filter((t) => t !== existing);
publish();
return id;
}
function expire(id) {
const existing = toasts.find((t) => t.id === id);
if (!existing) return;
existing.onAutoClose?.(existing);
toasts = toasts.filter((t) => t !== existing);
publish();
}
function baseToast(message, data) {
return upsert(data?.id, {
...data,
title: message
}, "default");
}
function withType(type) {
return (message, data) => upsert(data?.id, {
...data,
title: message
}, type);
}
const toastImpl = baseToast;
toastImpl.success = withType("success");
toastImpl.error = withType("error");
toastImpl.info = withType("info");
toastImpl.warning = withType("warning");
toastImpl.loading = withType("loading");
toastImpl.message = withType("default");
toastImpl.custom = (jsx, data) => {
const id = data?.id ?? nextId();
return upsert(id, {
...data,
jsx: jsx(id)
}, "default");
};
function isHttpResponse(value) {
return typeof value === "object" && value !== null && "ok" in value && typeof value.ok === "boolean" && "status" in value && typeof value.status === "number";
}
function isPromiseExtendedResult(value) {
return typeof value === "object" && value !== null && !React.isValidElement(value);
}
function applyPromiseSettlement(id, resolved, fallbackTitle, description, type) {
if (isPromiseExtendedResult(resolved)) {
const { message, ...rest } = resolved;
upsert(id, {
description,
...rest,
title: message ?? fallbackTitle,
type
}, type);
return;
}
upsert(id, {
title: resolved ?? fallbackTitle,
description,
type
}, type);
}
function promiseImpl(promiseOrFn, data) {
const { loading, success, error, finally: onFinally, description, ...rest } = data;
const id = upsert(data.id, {
...rest,
description: typeof description === "function" ? void 0 : description,
title: loading,
type: "loading"
}, "loading");
const settle = typeof promiseOrFn === "function" ? promiseOrFn() : promiseOrFn;
let settled;
const chain = settle.then(async (result) => {
if (isHttpResponse(result) && !result.ok) {
settled = {
ok: false,
reason: result
};
const statusMessage = `HTTP error! status: ${result.status}`;
const resolvedDescription = typeof description === "function" ? await description(statusMessage) : description;
const resolved = typeof error === "function" ? await error(statusMessage) : error;
applyPromiseSettlement(id, resolved, "Error", resolvedDescription, "error");
return;
}
if (result instanceof Error) {
settled = {
ok: false,
reason: result
};
const resolvedDescription = typeof description === "function" ? await description(result) : description;
const resolved = typeof error === "function" ? await error(result) : error;
applyPromiseSettlement(id, resolved, "Error", resolvedDescription, "error");
return;
}
settled = {
ok: true,
value: result
};
const resolvedDescription = typeof description === "function" ? await description(result) : description;
const resolved = typeof success === "function" ? await success(result) : success;
applyPromiseSettlement(id, resolved, "Success", resolvedDescription, "success");
}).catch(async (err) => {
settled = {
ok: false,
reason: err
};
const resolvedDescription = typeof description === "function" ? await description(err) : description;
const resolved = typeof error === "function" ? await error(err) : error;
applyPromiseSettlement(id, resolved, "Error", resolvedDescription, "error");
}).finally(() => onFinally?.());
const unwrap = () => chain.then(() => {
if (!settled) throw new Error("scrollsheet toast.promise: chain settled with no outcome");
if (settled.ok) return settled.value;
throw settled.reason;
});
return Object.assign(id, { unwrap });
}
toastImpl.promise = promiseImpl;
toastImpl.dismiss = dismiss;
toastImpl.getToasts = () => toasts;
toastImpl.getHistory = () => history;
const toast = toastImpl;
function useSonner() {
return { toasts: React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) };
}
const useToasts = useSonner;
function selectToasterToasts(all, toasterId) {
return all.filter((t) => toasterId === void 0 ? t.toasterId === void 0 : t.toasterId === toasterId);
}
function selectPositionToasts(all, position, defaultPosition) {
return all.filter((t) => t.position === void 0 ? position === defaultPosition : t.position === position);
}
function selectToastWindow(toasts, visibleToasts) {
const newestFirst = [...toasts].reverse();
return {
visible: newestFirst.slice(0, visibleToasts),
hidden: newestFirst.slice(visibleToasts)
};
}
//#endregion
//#region packages/scrollsheet/src/toast/toast-styles.ts
const { injectDocument, injectShadowRoot } = createStyleInjector(`.scrollsheet-toast{border-radius:var(--scrollsheet-toast-radius,14px);color:#18181b;background:#fff;border:1px solid #00000014;align-items:flex-start;gap:10px;padding:16px;font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,sans-serif;font-size:13px;display:flex;position:relative;box-shadow:0 4px 12px #0000001a}.scrollsheet-toast[data-scrollsheet-custom]{box-shadow:none;background:0 0;border:none;padding:0}.scrollsheet-toast-icon{color:#fff;background:#6b7280;border-radius:50%;flex:none;place-items:center;width:18px;height:18px;margin-top:1px;font-size:11px;line-height:1;display:grid}.scrollsheet-toast-icon[data-type=success]{background:#22c55e}.scrollsheet-toast-icon[data-type=error]{background:#ef4444}.scrollsheet-toast-icon[data-type=warning]{background:#f59e0b}.scrollsheet-toast-icon[data-type=info]{background:#3b82f6}.scrollsheet-toast-icon[data-type=loading]{background:0 0}.scrollsheet-toast-spinner{border:2px solid #00000026;border-top-color:#0000008c;border-radius:50%;width:14px;height:14px;animation:.6s linear infinite scrollsheet-toast-spin}@keyframes scrollsheet-toast-spin{to{transform:rotate(360deg)}}@media (prefers-reduced-motion:reduce){.scrollsheet-toast-spinner{animation-duration:1.6s}}.scrollsheet-toast-body{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.scrollsheet-toast-title{font-size:13px;font-weight:500;line-height:1.35}.scrollsheet-toast-description{color:#0009;font-size:13px;line-height:1.4}.scrollsheet-toast-actions{flex:none;align-items:center;gap:6px;display:flex}.scrollsheet-toast-action,.scrollsheet-toast-cancel{cursor:pointer;border:none;border-radius:4px;height:24px;padding:0 8px;font-size:12px;font-weight:500}.scrollsheet-toast-action{color:#fff;background:#18181b}.scrollsheet-toast-cancel{color:#18181b;background:#0000000f}.scrollsheet-toast-close{color:#00000080;cursor:pointer;background:0 0;border:none;border-radius:50%;flex:none;place-items:center;width:20px;height:20px;padding:0;font-size:14px;line-height:1;display:grid}.scrollsheet-toast-close:hover{background:#0000000f}[data-scrollsheet-toaster]{z-index:2147483647;width:min(356px, calc(100vw - 2 * var(--scrollsheet-toast-offset,var(--sonner-offset,24px))));outline:none;margin:0;padding:0;list-style:none;transition:transform .4s;position:fixed}[data-scrollsheet-toaster][data-x-position=right]{right:var(--scrollsheet-toast-offset,var(--sonner-offset,24px))}[data-scrollsheet-toaster][data-x-position=left]{left:var(--scrollsheet-toast-offset,var(--sonner-offset,24px))}[data-scrollsheet-toaster][data-x-position=center]{left:50%;transform:translate(-50%)}[data-scrollsheet-toaster][data-y-position=top]{top:var(--scrollsheet-toast-offset,var(--sonner-offset,24px))}[data-scrollsheet-toaster][data-y-position=bottom]{bottom:var(--scrollsheet-toast-offset,var(--sonner-offset,24px))}.scrollsheet-toast{--scrollsheet-toast-lift:-1;--scrollsheet-toast-y:translateY(100%);opacity:0;transform:var(--scrollsheet-toast-y);touch-action:none;box-sizing:border-box;overflow-wrap:anywhere;transition:transform .25s,opacity .25s,height .25s;position:absolute;left:0;right:0}.scrollsheet-toast[data-y-position=top]{--scrollsheet-toast-lift:1;--scrollsheet-toast-y:translateY(-100%);top:0}.scrollsheet-toast[data-y-position=bottom]{--scrollsheet-toast-lift:-1;--scrollsheet-toast-y:translateY(100%);bottom:0}.scrollsheet-toast[data-mounted]{--scrollsheet-toast-y:translateY(0);opacity:1}.scrollsheet-toast[data-expanded=false][data-front=false]{--scrollsheet-toast-y:translateY(calc(var(--scrollsheet-toast-lift) * var(--scrollsheet-toast-gap,var(--sonner-gap,14px)) * var(--scrollsheet-toast-toasts-before,var(--sonner-toasts-before,0)))) scale(calc(1 - var(--scrollsheet-toast-toasts-before,var(--sonner-toasts-before,0)) * .05));height:var(--scrollsheet-toast-front-height,var(--sonner-front-height,auto));overflow:hidden}.scrollsheet-toast[data-expanded=false][data-front=false]>*{opacity:0}.scrollsheet-toast[data-mounted][data-expanded=true]{--scrollsheet-toast-y:translateY(calc(var(--scrollsheet-toast-lift) * var(--scrollsheet-toast-stack-offset,var(--sonner-stack-offset,0px))));height:var(--scrollsheet-toast-initial-height,var(--sonner-initial-height,auto))}.scrollsheet-toast[data-visible=false]{opacity:0;pointer-events:none}.scrollsheet-toast[data-removed]{pointer-events:none}.scrollsheet-toast[data-removed][data-front=true]{--scrollsheet-toast-y:translateY(calc(var(--scrollsheet-toast-lift) * -100%));opacity:0}.scrollsheet-toast[data-removed][data-front=false][data-expanded=true]{--scrollsheet-toast-y:translateY(calc(var(--scrollsheet-toast-lift) * var(--scrollsheet-toast-stack-offset,var(--sonner-stack-offset,0px)) + var(--scrollsheet-toast-lift) * -100%));opacity:0}.scrollsheet-toast[data-removed][data-front=false][data-expanded=false]{--scrollsheet-toast-y:translateY(40%);opacity:0}.scrollsheet-toast[data-swiping=true]{transform:var(--scrollsheet-toast-y) translateY(var(--scrollsheet-toast-swipe-y,var(--sonner-swipe-y,0px))) translateX(var(--scrollsheet-toast-swipe-x,var(--sonner-swipe-x,0px)));transition:none}.scrollsheet-toast[data-swiped=true]{-webkit-user-select:none;user-select:none}.scrollsheet-toast[data-swipe-out=true]{animation:.2s ease-out forwards scrollsheet-toast-swipe-out}.scrollsheet-toast[data-swipe-direction=left]{--scrollsheet-toast-swipe-out-x:-1}.scrollsheet-toast[data-swipe-direction=right]{--scrollsheet-toast-swipe-out-x:1}.scrollsheet-toast[data-swipe-direction=up]{--scrollsheet-toast-swipe-out-y:-1}.scrollsheet-toast[data-swipe-direction=down]{--scrollsheet-toast-swipe-out-y:1}@keyframes scrollsheet-toast-swipe-out{0%{transform:var(--scrollsheet-toast-y) translateY(var(--scrollsheet-toast-swipe-y,var(--sonner-swipe-y,0px))) translateX(var(--scrollsheet-toast-swipe-x,var(--sonner-swipe-x,0px)));opacity:1}to{transform:var(--scrollsheet-toast-y) translateY(calc(var(--scrollsheet-toast-swipe-y,var(--sonner-swipe-y,0px)) + var(--scrollsheet-toast-swipe-out-y,var(--sonner-swipe-out-y,0)) * 100%)) translateX(calc(var(--scrollsheet-toast-swipe-x,var(--sonner-swipe-x,0px)) + var(--scrollsheet-toast-swipe-out-x,var(--sonner-swipe-out-x,0)) * 100%));opacity:0}}@media (prefers-reduced-motion:reduce){[data-scrollsheet-toaster],.scrollsheet-toast{transition:none!important;animation:none!important}}@media (max-width:600px){[data-scrollsheet-toaster]{right:var(--scrollsheet-toast-mobile-offset,var(--sonner-mobile-offset,16px));left:var(--scrollsheet-toast-mobile-offset,var(--sonner-mobile-offset,16px));width:100%}[data-scrollsheet-toaster] .scrollsheet-toast{width:calc(100% - var(--scrollsheet-toast-mobile-offset,var(--sonner-mobile-offset,16px)) * 2);left:0;right:0}[data-scrollsheet-toaster][data-x-position=left]{left:var(--scrollsheet-toast-mobile-offset,var(--sonner-mobile-offset,16px))}[data-scrollsheet-toaster][data-y-position=bottom]{bottom:var(--scrollsheet-toast-mobile-offset,var(--sonner-mobile-offset,16px))}[data-scrollsheet-toaster][data-y-position=top]{top:var(--scrollsheet-toast-mobile-offset,var(--sonner-mobile-offset,16px))}[data-scrollsheet-toaster][data-x-position=center]{left:var(--scrollsheet-toast-mobile-offset,var(--sonner-mobile-offset,16px));right:var(--scrollsheet-toast-mobile-offset,var(--sonner-mobile-offset,16px));transform:none}}`, "data-sonner-toast-styles");
function injectToastStyles(nonce) {
injectDocument(nonce);
}
function injectToastStylesInto(root, nonce) {
injectShadowRoot(root, nonce);
}
//#endregion
//#region packages/scrollsheet/src/toast/toaster.tsx
function resolveVisibleToasts(count) {
if (count === void 0) return 3;
return Math.max(1, Math.floor(count));
}
function useMountedFlag() {
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => {
if (prefersReducedMotion()) {
setMounted(true);
return;
}
let raf2 = 0;
const raf1 = requestAnimationFrame(() => {
raf2 = requestAnimationFrame(() => setMounted(true));
});
return () => {
cancelAnimationFrame(raf1);
cancelAnimationFrame(raf2);
};
}, []);
return mounted;
}
function useIsDocumentHidden() {
const [hidden, setHidden] = React.useState(() => typeof document !== "undefined" && document.hidden);
React.useEffect(() => {
const onVisibilityChange = () => setHidden(document.hidden);
document.addEventListener("visibilitychange", onVisibilityChange);
return () => document.removeEventListener("visibilitychange", onVisibilityChange);
}, []);
return hidden;
}
const EMPTY_RECORDS = [];
const EMPTY_MAP = new Map();
function toIdMap(records) {
return new Map(records.map((r) => [r.id, r]));
}
function sameIds(a, b) {
if (a.size !== b.size) return false;
for (const id of a.keys()) if (!b.has(id)) return false;
return true;
}
function useToastExit(live, queued = EMPTY_RECORDS, exitMs = 220) {
const [exiting, setExiting] = React.useState(() => EMPTY_MAP);
const [prevLive, setPrevLive] = React.useState(() => toIdMap(live));
const timersRef = React.useRef(new Map());
const liveIds = new Set(live.map((r) => r.id));
let renderExiting = exiting;
if (!sameIds(prevLive, liveIds)) {
const queuedIds = new Set(queued.map((r) => r.id));
const justRemoved = [];
for (const [id, record] of prevLive) {
if (liveIds.has(id)) continue;
if (queuedIds.has(id)) continue;
justRemoved.push(record);
}
setPrevLive(toIdMap(live));
if (justRemoved.length > 0) {
const next = new Map(exiting);
for (const record of justRemoved) next.set(record.id, record);
renderExiting = next;
setExiting(next);
}
}
React.useEffect(() => {
const reduced = prefersReducedMotion();
for (const [id] of exiting) {
if (timersRef.current.has(id)) continue;
const timer = setTimeout(() => {
timersRef.current.delete(id);
setExiting((prev) => {
if (!prev.has(id)) return prev;
const next = new Map(prev);
next.delete(id);
return next;
});
}, reduced ? 0 : exitMs);
timersRef.current.set(id, timer);
}
for (const [id, timer] of timersRef.current) if (!exiting.has(id)) {
clearTimeout(timer);
timersRef.current.delete(id);
}
}, [exiting, exitMs]);
React.useEffect(() => {
const timers = timersRef.current;
return () => {
for (const timer of timers.values()) clearTimeout(timer);
timers.clear();
};
}, []);
const rows = live.map((record) => ({
record,
exiting: false
}));
for (const [id, record] of renderExiting) if (!liveIds.has(id)) rows.push({
record,
exiting: true
});
return rows;
}
//#endregion
//#region packages/scrollsheet/src/toast/shell/shell-selectors.ts
function splitPosition(position) {
const [y, x] = position.split("-");
return {
y,
x
};
}
function computePossiblePositions(defaultPosition, toasts) {
const seen = new Set([defaultPosition]);
for (const t of toasts) if (t.position) seen.add(t.position);
return [...seen];
}
function computeRowOffsets(newestFirst, heights, gap) {
let cumulativeHeight = 0;
return newestFirst.map((record, index) => {
const stackOffset = index * gap + cumulativeHeight;
cumulativeHeight += heights.get(record.id) ?? 0;
return {
id: record.id,
index,
toastsBefore: index,
stackOffset
};
});
}
function findNewestDismissible(toasts) {
for (let i = toasts.length - 1; i >= 0; i -= 1) {
const t = toasts[i];
if (t && t.dismissible !== false) return t;
}
}
const SWIPE_VELOCITY_THRESHOLD = .11;
function getDefaultSwipeDirections(position) {
const { y, x } = splitPosition(position);
const directions = [y];
if (x === "left" || x === "right") directions.push(x);
return directions;
}
function lockSwipeAxis(dx, dy) {
if (Math.abs(dx) <= 1 && Math.abs(dy) <= 1) return null;
return Math.abs(dx) > Math.abs(dy) ? "x" : "y";
}
function dampenSwipeDelta(delta) {
const dampened = delta * (1 / (1.5 + Math.abs(delta) / 20));
return Math.abs(dampened) < Math.abs(delta) ? dampened : delta;
}
function computeSwipeAxisAmount(axis, delta, directions) {
const negativeDir = axis === "x" ? "left" : "top";
const positiveDir = axis === "x" ? "right" : "bottom";
if (!(directions.includes(negativeDir) || directions.includes(positiveDir))) return 0;
return directions.includes(negativeDir) && delta < 0 || directions.includes(positiveDir) && delta > 0 ? delta : dampenSwipeDelta(delta);
}
function isSwipeReleaseAllowed(axis, amount, directions) {
if (axis === "x") return directions.includes(amount > 0 ? "right" : "left");
return directions.includes(amount > 0 ? "bottom" : "top");
}
function shouldDismissOnSwipeRelease(amount, velocity, thresholdPx = 45, velocityThreshold = SWIPE_VELOCITY_THRESHOLD) {
return Math.abs(amount) >= thresholdPx || velocity > velocityThreshold;
}
function resolveSwipeOutDirection(axis, amount) {
if (axis === "x") return amount > 0 ? "right" : "left";
return amount > 0 ? "down" : "up";
}
//#endregion
//#region packages/scrollsheet/src/toast/shell/use-toast-swipe.ts
const ZERO_AMOUNT = {
x: 0,
y: 0
};
function useToastSwipe(elRef, enabled, directions, onSwipeDismiss) {
const draggingRef = React.useRef(false);
const axisRef = React.useRef(null);
const startRef = React.useRef(ZERO_AMOUNT);
const amountRef = React.useRef(ZERO_AMOUNT);
const dragStartRef = React.useRef(0);
const onSwipeDismissRef = React.useRef(onSwipeDismiss);
onSwipeDismissRef.current = onSwipeDismiss;
const onPointerDown = React.useCallback((event) => {
if (!enabled || event.button !== 0) return;
if (event.target.tagName === "BUTTON") return;
const el = elRef.current;
if (!el) return;
draggingRef.current = true;
axisRef.current = null;
amountRef.current = ZERO_AMOUNT;
startRef.current = {
x: event.clientX,
y: event.clientY
};
dragStartRef.current = Date.now();
el.setPointerCapture(event.pointerId);
el.setAttribute("data-swiping", "true");
}, [enabled, elRef]);
const onPointerMove = React.useCallback((event) => {
if (!draggingRef.current) return;
const el = elRef.current;
if (!el) return;
if ((window.getSelection?.()?.toString().length ?? 0) > 0) return;
const start = startRef.current;
const dx = event.clientX - start.x;
const dy = event.clientY - start.y;
if (axisRef.current === null) axisRef.current = lockSwipeAxis(dx, dy);
const axis = axisRef.current;
if (axis === null) return;
const resolved = computeSwipeAxisAmount(axis, axis === "x" ? dx : dy, directions);
const amount = axis === "x" ? {
x: resolved,
y: 0
} : {
x: 0,
y: resolved
};
amountRef.current = amount;
if (resolved !== 0) el.setAttribute("data-swiped", "true");
el.style.setProperty("--scrollsheet-toast-swipe-x", `${amount.x}px`);
el.style.setProperty("--scrollsheet-toast-swipe-y", `${amount.y}px`);
}, [elRef, directions]);
const release = React.useCallback(() => {
if (!draggingRef.current) return;
draggingRef.current = false;
const el = elRef.current;
const axis = axisRef.current;
axisRef.current = null;
if (!el) return;
if (axis === null) {
el.setAttribute("data-swiping", "false");
return;
}
const amount = axis === "x" ? amountRef.current.x : amountRef.current.y;
const elapsed = Math.max(1, Date.now() - dragStartRef.current);
const velocity = Math.abs(amount) / elapsed;
if (isSwipeReleaseAllowed(axis, amount, directions) && shouldDismissOnSwipeRelease(amount, velocity)) {
const direction = resolveSwipeOutDirection(axis, amount);
el.setAttribute("data-swipe-direction", direction);
if (prefersReducedMotion()) {
el.setAttribute("data-swipe-out", "true");
onSwipeDismissRef.current();
return;
}
const onAnimEnd = () => {
el.removeEventListener("animationend", onAnimEnd);
onSwipeDismissRef.current();
};
el.addEventListener("animationend", onAnimEnd);
el.setAttribute("data-swipe-out", "true");
return;
}
el.setAttribute("data-swiping", "false");
el.setAttribute("data-swiped", "false");
el.style.setProperty("--scrollsheet-toast-swipe-x", "0px");
el.style.setProperty("--scrollsheet-toast-swipe-y", "0px");
}, [elRef, directions]);
return {
onPointerDown,
onPointerMove,
onPointerUp: React.useCallback((event) => {
if (elRef.current?.hasPointerCapture(event.pointerId)) elRef.current.releasePointerCapture(event.pointerId);
release();
}, [elRef, release]),
onPointerCancel: React.useCallback(() => release(), [release])
};
}
//#endregion
//#region packages/scrollsheet/src/toast/shell/toast-row.tsx
function ToastIcon({ type, icons, spinnerClassName }) {
if (type === "loading") {
if (icons?.loading) return jsx(Fragment, { children: icons.loading });
return jsx("span", { className: spinnerClassName });
}
switch (type) {
case "success": return jsx(Fragment, { children: icons?.success ?? "✓" });
case "error": return jsx(Fragment, { children: icons?.error ?? "✕" });
case "warning": return jsx(Fragment, { children: icons?.warning ?? "!" });
case "info": return jsx(Fragment, { children: icons?.info ?? "i" });
default: return null;
}
}
function ToastRow({ record, index, total, toastsBefore, stackOffset, height, frontHeight, visible, expanded, removed, yPosition, xPosition, closeButton, directions, onDismiss, observe, toasterClassNames, icons, toasterCloseButtonAriaLabel, toasterStyle }) {
const role = record.type === "error" ? "alert" : "status";
const mounted = useMountedFlag();
const elRef = React.useRef(null);
const dismissible = record.dismissible !== false;
const isFront = index === 0;
const setRefs = React.useCallback((el) => {
elRef.current = el;
observe(record.id, el);
}, [observe, record.id]);
const swipeHandlers = useToastSwipe(elRef, !removed && dismissible && record.type !== "loading", directions, () => onDismiss(record));
React.useEffect(() => {
const el = elRef.current;
if (el) el.inert = removed;
}, [removed]);
const style = {
"--scrollsheet-toast-toasts-before": toastsBefore,
"--scrollsheet-toast-stack-offset": `${stackOffset}px`,
"--scrollsheet-toast-front-height": frontHeight !== void 0 ? `${frontHeight}px` : "0px",
"--scrollsheet-toast-initial-height": height !== void 0 ? `${height}px` : "auto",
zIndex: Math.max(0, total - index),
...toasterStyle,
...record.style
};
const rootClassName = cn("scrollsheet-toast", "sonner-toast", record.className, toasterClassNames?.toast, record.classNames?.toast, toasterClassNames?.default, toasterClassNames?.[record.type], record.classNames?.[record.type]);
const motionAttrs = {
"data-mounted": mounted ? "" : void 0,
"data-removed": removed ? "" : void 0,
"data-visible": visible ? "true" : "false",
"data-front": isFront ? "true" : "false",
"data-expanded": expanded ? "true" : "false",
"data-y-position": yPosition,
"data-x-position": xPosition,
"data-index": index,
"data-dismissible": dismissible ? "true" : "false",
"data-swiping": "false",
"data-swiped": "false",
"aria-hidden": removed ? "true" : void 0
};
if (record.jsx !== void 0) return jsx("div", {
ref: setRefs,
className: rootClassName,
"data-scrollsheet-toast": "",
"data-sonner-toast": "",
"data-scrollsheet-custom": "",
"data-sonner-custom": "",
"data-testid": record.testId,
role,
style,
...motionAttrs,
...swipeHandlers,
children: record.jsx
});
const showCloseButton = closeButton && dismissible && record.type !== "loading";
const hasActions = Boolean(record.action || record.cancel || showCloseButton);
const closeButtonAriaLabel = record.closeButtonAriaLabel ?? toasterCloseButtonAriaLabel ?? "Close toast";
return jsxs("div", {
ref: setRefs,
className: rootClassName,
"data-scrollsheet-toast": "",
"data-sonner-toast": "",
"data-type": record.type,
"data-testid": record.testId,
role,
style,
...motionAttrs,
...swipeHandlers,
children: [
jsx("span", {
className: cn("scrollsheet-toast-icon", "sonner-toast-icon", toasterClassNames?.icon, record.classNames?.icon),
"data-type": record.type,
"aria-hidden": "true",
children: record.icon ?? jsx(ToastIcon, {
type: record.type,
icons,
spinnerClassName: cn("scrollsheet-toast-spinner", "sonner-toast-spinner", toasterClassNames?.loader, record.classNames?.loader)
})
}),
jsxs("div", {
className: cn("scrollsheet-toast-body", "sonner-toast-body", toasterClassNames?.content, record.classNames?.content),
children: [record.title !== void 0 && jsx("div", {
className: cn("scrollsheet-toast-title", "sonner-toast-title", toasterClassNames?.title, record.classNames?.title),
children: record.title
}), record.description !== void 0 && jsx("div", {
className: cn("scrollsheet-toast-description", "sonner-toast-description", toasterClassNames?.description, record.classNames?.description),
children: record.description
})]
}),
hasActions && jsxs("div", {
className: "scrollsheet-toast-actions sonner-toast-actions",
children: [
record.cancel && jsx("button", {
type: "button",
className: cn("scrollsheet-toast-cancel", "sonner-toast-cancel", toasterClassNames?.cancelButton, record.classNames?.cancelButton),
onClick: (event) => {
if (!dismissible) return;
record.cancel?.onClick(event);
onDismiss(record);
},
children: record.cancel.label
}),
record.action && jsx("button", {
type: "button",
className: cn("scrollsheet-toast-action", "sonner-toast-action", toasterClassNames?.actionButton, record.classNames?.actionButton),
onClick: (event) => {
record.action?.onClick(event);
if (event.defaultPrevented) return;
onDismiss(record);
},
children: record.action.label
}),
showCloseButton && jsx("button", {
type: "button",
className: cn("scrollsheet-toast-close", "sonner-toast-close", toasterClassNames?.closeButton, record.classNames?.closeButton),
"aria-label": closeButtonAriaLabel,
onClick: () => onDismiss(record),
children: icons?.close ?? "×"
})
]
})
]
});
}
//#endregion
//#region packages/scrollsheet/src/toast/shell/use-toast-heights.ts
function useToastHeights() {
const [heights, setHeights] = React.useState(() => new Map());
const observersRef = React.useRef(new Map());
const setHeight = React.useCallback((id, height) => {
setHeights((prev) => prev.get(id) === height ? prev : new Map(prev).set(id, height));
}, []);
const forget = React.useCallback((id) => {
observersRef.current.get(id)?.disconnect();
observersRef.current.delete(id);
setHeights((prev) => {
if (!prev.has(id)) return prev;
const next = new Map(prev);
next.delete(id);
return next;
});
}, []);
const observe = React.useCallback((id, el) => {
const observers = observersRef.current;
observers.get(id)?.disconnect();
observers.delete(id);
if (!el || typeof ResizeObserver === "undefined") return;
const ro = new ResizeObserver(() => setHeight(id, el.getBoundingClientRect().height));
ro.observe(el);
observers.set(id, ro);
setHeight(id, el.getBoundingClientRect().height);
}, [setHeight]);
React.useEffect(() => {
const observers = observersRef.current;
return () => {
for (const ro of observers.values()) ro.disconnect();
observers.clear();
};
}, []);
return {
heights,
observe,
forget
};
}
//#endregion
//#region packages/scrollsheet/src/toast/shell/toaster-shell.tsx
const DEFAULT_HOTKEY = ["altKey", "KeyT"];
function PositionGroup({ position, defaultPosition, toasts, visibleToasts, gap, expanded, closeButton, swipeDirections, toastOptions, icons, className, baseStyle, registerListEl, dismissRow, onHoverEnter, onHoverLeave, onInteractingChange }) {
const directions = React.useMemo(() => swipeDirections ?? getDefaultSwipeDirections(position), [swipeDirections, position]);
const positionToasts = React.useMemo(() => selectPositionToasts(toasts, position, defaultPosition), [
toasts,
position,
defaultPosition
]);
const newestFirst = React.useMemo(() => [...positionToasts].reverse(), [positionToasts]);
const window_ = React.useMemo(() => selectToastWindow(positionToasts, visibleToasts), [positionToasts, visibleToasts]);
const visibleIds = React.useMemo(() => new Set(window_.visible.map((t) => t.id)), [window_]);
const { heights, observe, forget } = useToastHeights();
const exitRows = useToastExit(newestFirst);
const exitingIds = React.useMemo(() => new Set(exitRows.filter((r) => r.exiting).map((r) => r.record.id)), [exitRows]);
React.useEffect(() => {
for (const id of exitingIds) forget(id);
}, [exitingIds, forget]);
const offsets = React.useMemo(() => computeRowOffsets(newestFirst, heights, gap), [
newestFirst,
heights,
gap
]);
const offsetById = React.useMemo(() => new Map(offsets.map((o) => [o.id, o])), [offsets]);
const lastOffsetRef = React.useRef(new Map());
for (const o of offsets) lastOffsetRef.current.set(o.id, o);
if (lastOffsetRef.current.size > offsets.length) {
const keep = new Set([...offsets.map((o) => o.id), ...exitingIds]);
for (const id of [...lastOffsetRef.current.keys()]) if (!keep.has(id)) lastOffsetRef.current.delete(id);
}
const frontHeight = heights.get(newestFirst[0]?.id ?? "");
const { y, x } = splitPosition(position);
const listRef = React.useCallback((el) => registerListEl(position, el), [registerListEl, position]);
if (exitRows.length === 0) return null;
return jsx("ol", {
ref: listRef,
"data-scrollsheet-toaster": "",
"data-sonner-toaster": "",
"data-scrollsheet-theme": "light",
"data-sonner-theme": "light",
"data-y-position": y,
"data-x-position": x,
tabIndex: -1,
className,
style: {
...baseStyle,
"--scrollsheet-toast-gap": `${gap}px`,
"--scrollsheet-toast-front-height": frontHeight !== void 0 ? `${frontHeight}px` : void 0
},
onMouseEnter: onHoverEnter,
onMouseMove: onHoverEnter,
onMouseLeave: onHoverLeave,
onPointerDown: (event) => {
if (event.target.dataset.dismissible === "false") return;
onInteractingChange(true);
},
onPointerUp: () => onInteractingChange(false),
children: exitRows.map(({ record, exiting }) => {
const offset = offsetById.get(record.id) ?? lastOffsetRef.current.get(record.id);
const index = offset?.index ?? newestFirst.length;
return jsx(ToastRow, {
record,
index,
total: newestFirst.length,
toastsBefore: offset?.toastsBefore ?? index,
stackOffset: offset?.stackOffset ?? 0,
height: heights.get(record.id),
frontHeight,
visible: visibleIds.has(record.id),
expanded,
removed: exiting,
yPosition: y,
xPosition: x,
closeButton,
directions,
onDismiss: dismissRow,
observe,
toasterClassNames: toastOptions?.classNames,
icons,
toasterCloseButtonAriaLabel: toastOptions?.closeButtonAriaLabel,
toasterStyle: toastOptions?.style
}, record.id);
})
});
}
function resolveShellPosition(position) {
return position ?? "bottom-right";
}
function ToasterShell({ id, toasterId, position, theme, richColors, expand: forceExpand, visibleToasts: visibleToastsProp, closeButton = false, duration: durationProp, gap = 14, offset, swipeDirections, toastOptions, icons, className, style, containerAriaLabel = "Notifications", hotkey = DEFAULT_HOTKEY, nonce }) {
const resolvedId = id ?? toasterId;
if (toasterId !== void 0) warnOnce("toaster-id-deprecated", "[scrollsheet Toaster] Toaster's \"toasterId\" prop is deprecated — use \"id\" instead.");
const { toasts: allToasts } = useSonner();
const toasts = React.useMemo(() => selectToasterToasts(allToasts, resolvedId), [allToasts, resolvedId]);
const defaultPosition = resolveShellPosition(position);
const possiblePositions = React.useMemo(() => computePossiblePositions(defaultPosition, toasts), [defaultPosition, toasts]);
const visibleToasts = resolveVisibleToasts(visibleToastsProp);
const defaultDuration = durationProp ?? toastOptions?.duration ?? 4e3;
if (theme !== void 0 && theme !== "light") warnOnce("shell-theme", `[scrollsheet Toaster] theme="${theme}" isn't implemented yet — v1 always renders the static light card real Sonner ships by default.`);
if (richColors) warnOnce("shell-rich-colors", "[scrollsheet Toaster] richColors isn't implemented yet — toasts render with their default icon color only.");
React.useEffect(() => {
injectToastStyles(nonce);
}, [nonce]);
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => setMounted(true), []);
const [hovered, setHovered] = React.useState(false);
const [hotkeyExpanded, setHotkeyExpanded] = React.useState(false);
const [interacting, setInteracting] = React.useState(false);
const expanded = Boolean(forceExpand) || hovered || hotkeyExpanded;
const isDocumentHidden = useIsDocumentHidden();
const handleHoverEnter = React.useCallback(() => setHovered(true), []);
const handleHoverLeave = React.useCallback(() => {
if (!interacting) setHovered(false);
}, [interacting]);
const dismissRow = React.useCallback((record) => {
toast.dismiss(record.id);
}, []);
const listElsRef = React.useRef(new Map());
const registerListEl = React.useCallback((pos, el) => {
if (el) listElsRef.current.set(pos, el);
else listElsRef.current.delete(pos);
}, []);
React.useEffect(() => {
if (hotkey.length === 0) return;
const handleKeyDown = (event) => {
if (hotkey.every((key) => event[key] || event.code === key)) {
setHotkeyExpanded(true);
const [firstList] = listElsRef.current.values();
firstList?.focus({ preventScroll: true });
}
if (event.code !== "Escape") return;
const active = document.activeElement;
const focusedEntry = [...listElsRef.current.entries()].find(([, el]) => active === el || el.contains(active));
if (!focusedEntry) return;
if (hotkeyExpanded) {
setHotkeyExpanded(false);
return;
}
const [focusedPosition] = focusedEntry;
const positionToasts = selectPositionToasts(toasts, focusedPosition, defaultPosition);
const front = positionToasts[positionToasts.length - 1];
if (front) dismissRow(front);
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [
hotkey,
hotkeyExpanded,
toasts,
defaultPosition,
dismissRow
]);
const timersRef = React.useRef(new Map());
React.useEffect(() => {
const timers = timersRef.current;
const liveIds = new Set(toasts.map((t) => t.id));
for (const [id, entry] of timers) {
if (liveIds.has(id)) continue;
if (entry.timer) clearTimeout(entry.timer);
timers.delete(id);
}
const paused = expanded || interacting || isDocumentHidden;
for (const t of toasts) {
if (t.type === "loading") {
const stale = timers.get(t.id);
if (stale) {
if (stale.timer) clearTimeout(stale.timer);
timers.delete(t.id);
}
continue;
}
const ms = t.duration ?? toastOptions?.duration ?? defaultDuration;
const existing = timers.get(t.id);
if (!existing || existing.record !== t) {
if (existing?.timer) clearTimeout(existing.timer);
const entry = {
timer: null,
startedAt: 0,
remaining: ms,
record: t
};
timers.set(t.id, entry);
if (Number.isFinite(ms) && !paused) {
entry.startedAt = Date.now();
entry.timer = setTimeout(() => {
timersRef.current.delete(t.id);
expire(t.id);
}, ms);
}
continue;
}
if (paused) {
if (existing.timer) {
clearTimeout(existing.timer);
const elapsed = Date.now() - existing.startedAt;
existing.remaining = Math.max(0, existing.remaining - elapsed);
existing.timer = null;
}
continue;
}
if (existing.timer || !Number.isFinite(existing.remaining)) continue;
existing.startedAt = Date.now();
existing.timer = setTimeout(() => {
timersRef.current.delete(t.id);
expire(t.id);
}, existing.remaining);
}
}, [
toasts,
expanded,
interacting,
isDocumentHidden,
defaultDuration,
toastOptions?.duration
]);
React.useEffect(() => {
const timers = timersRef.current;
return () => {
for (const entry of timers.values()) if (entry.timer) clearTimeout(entry.timer);
timers.clear();
};
}, []);
useCloseWatcher({
present: toasts.length > 0,
nonModal: true,
escapeDismissible: true,
onClose: () => {
const target = findNewestDismissible(toasts);
if (target) dismissRow(target);
}
});
if (!mounted || typeof document === "undefined") return null;
const offsetValue = offset !== void 0 ? typeof offset === "number" ? `${offset}px` : offset : void 0;
const baseStyle = {
...style,
"--scrollsheet-toast-offset": offsetValue
};
return createPortal(jsx("section", {
"aria-label": containerAriaLabel,
tabIndex: -1,
"aria-live": "polite",
"aria-relevant": "additions text",
"aria-atomic": "false",
suppressHydrationWarning: true,
"data-react-aria-top-layer": "",
children: possiblePositions.map((groupPosition) => jsx(PositionGroup, {
position: groupPosition,
defaultPosition,
toasts,
visibleToasts,
gap,
expanded,
closeButton,
swipeDirections,
toastOptions,
icons,
className,
baseStyle,
registerListEl,
dismissRow,
onHoverEnter: handleHoverEnter,
onHoverLeave: handleHoverLeave,
onInteractingChange: setInteracting
}, groupPosition))
}), document.body);
}
//#endregion
export { useSonner as a, toast as i, resolveVisibleToasts as n, useToasts as o, injectToastStylesInto as r, ToasterShell as t };

Sorry, the diff of this file is too big to display

//#region packages/scrollsheet/src/motion/geometry.d.ts
/**
* Per-side axis geometry.
*
* The whole engine (content.tsx) is written once against an abstract
* "revealed px" concept: 0 = fully hidden, `maxDetent` = fully revealed. The
* DOM's scroll position is the mechanism, but which raw value corresponds to
* "revealed" depends on which canvas edge the panel is pinned to:
* bottom/right sit at the canvas's *far* end, so raw scroll IS revealed px
* directly; top/left sit at the *near* end (flush with the screen when fully
* open), so raw scroll is mirrored: revealed = maxDetent - rawScroll.
* `mapScroll` is the self-inverse conversion between the two spaces.
*/
type Side = "bottom" | "top" | "left" | "right";
interface SideGeometry {
side: Side;
axis: "x" | "y";
/** DOM scroll property driving reveal for this side. */
scrollProp: "scrollTop" | "scrollLeft";
/** Track (viewport) size property along the scroll axis. */
clientSizeProp: "clientHeight" | "clientWidth";
/** Content natural-size property along the scroll axis (for 'content' detents). */
offsetSizeProp: "offsetHeight" | "offsetWidth";
/** +1: raw scroll equals revealed px. -1: raw scroll is mirrored (maxDetent - revealed). */
sign: 1 | -1;
/** transform-origin for the "receded" stacked-parent scale — the panel's free/growing edge. */
recedeOrigin: string;
}
declare function geometryFor(side: Side): SideGeometry;
/**
* Convert between raw scroll position and "revealed px" — self-inverse for a
* given `maxDetent`, so the same call does either direction:
* `mapScroll(rawScrollValue, maxDetent, sign)` → revealed, and
* `mapScroll(revealedValue, maxDetent, sign)` → raw scroll target.
*/
declare function mapScroll(value: number, maxDetent: number, sign: 1 | -1): number;
/**
* translate3d(...) for the receded-parent transform, scale plus a slight
* shift along the panel's free axis (toward wherever "deeper" is for this
* side) — deliberately no filter/brightness (composites badly on iOS); see
* the ::before dim overlay in styles.ts for the darkening instead.
*/
declare function recedeTransform(geometry: SideGeometry, progress: number, scale: number): string;
//#endregion
export { recedeTransform as a, mapScroll as i, SideGeometry as n, geometryFor as r, Side as t };
//#region packages/scrollsheet/src/motion/spring.d.ts
/**
* Spring physics → CSS `linear()` easing.
*
* Simulates a damped spring and samples it into a `linear()` timing function,
* so transitions get real spring motion with zero JS on the animation path.
* The output runs on the compositor; the simulation runs once (and is cached).
*
* The per-regime closed-form solutions in `createSpringSolver` are adapted from
* motion-dom's spring generator (MIT license) —
* https://github.com/motiondivision/motion, packages/motion-dom/src/animation/generators/spring.ts —
* re-derived for this library's fixed 0 → 1 progress convention instead of motion's
* generic origin/target pair.
*/
interface SpringConfig {
/** Stiffness (N/m). Higher = snappier. */
stiffness?: number;
/** Damping coefficient. Higher = less oscillation. */
damping?: number;
/** Mass. Higher = more inertia. */
mass?: number;
/** Initial velocity in units of total-distance per second (from a gesture handoff). */
velocity?: number;
/** Rest threshold as a fraction of distance. */
restDelta?: number;
}
interface SpringCurve {
/** CSS timing function: `linear(0, 0.29 4.3%, …, 1)` */
easing: string;
/** Duration in ms that pairs with the easing. */
durationMs: number;
}
/**
* Simulate the spring in closed form, then sample down to `linear()` keypoints.
* Throws if the config cannot settle inside the search horizon (for example
* damping 0) instead of silently truncating the curve.
*/
declare function spring(config?: SpringConfig): SpringCurve;
/**
* Sample the same closed-form solution at a single elapsed time — progress and
* velocity (progress-units per second) at `atMs`. This is what lets an
* in-flight WAAPI enter/exit leg be interrupted mid-travel: the browser is
* playing a `linear()` curve sampled from this exact solution, so
* re-evaluating it at the elapsed wall-clock time recovers the current
* position and velocity without parsing any computed style (see
* internal/animate.ts). O(1): a direct analytic evaluation, never a
* re-simulation from t=0.
*/
declare function sampleSpringAt(config: SpringConfig, atMs: number): {
value: number;
velocity: number;
};
//#endregion
//#region packages/scrollsheet/src/motion/animate.d.ts
interface AnimateHandle {
/** Resolves on natural completion only — never on stop()/cancel(), never rejects. */
readonly finished: Promise<void>;
/** True once the leg is no longer driving the element (finished, stopped, or canceled). */
readonly settled: boolean;
/** The value the leg is heading toward. */
readonly to: number;
/** Paired duration of the generated curve, for backstop timers. */
readonly durationMs: number;
/**
* Freeze the element at its current position (commitStyles + cancel) and
* return the JS-computed value + velocity (value-units per second) there —
* the handoff for a retargeted or gesture-grabbed leg.
*/
stop(): {
value: number;
velocity: number;
};
/** Drop the animation without committing anything. */
cancel(): void;
}
/**
* Animate `el`'s `prop` from `from` to `to` (numeric, in whatever unit space
* `toCss` maps out of) along a spring's `linear()` curve. `config` must be
* the same spring the curve was generated from — `stop()` re-simulates it.
*
* A zero/absent duration, a missing `el.animate` (no WAAPI), or a throwing
* `animate()` call (e.g. an easing the engine can't parse) all degrade to an
* immediately-finished no-op handle: the caller's CSS resting state for the
* leg's end is what shows, an instant jump instead of a crash.
*/
declare function animate(el: HTMLElement, prop: string, from: number, to: number, toCss: (value: number) => string, curve: SpringCurve, config: SpringConfig): AnimateHandle;
//#endregion
//#region packages/scrollsheet/src/motion/scroll-animator.d.ts
/**
* Programmatic detent travel.
*
* Native smooth `scrollTo` fights concurrent touch input on iOS Safari
* (WebKit #238497), so programmatic moves run a small rAF tween instead —
* and any real user input (pointerdown / wheel / touchstart) cancels it
* immediately so the finger always wins.
*/
interface ScrollAnimation {
cancel(): void;
finished: Promise<boolean>;
}
declare function animateScrollTo(el: HTMLElement, target: number, durationMs: number, axis?: "x" | "y",
/**
* Opt-in only — see the doc comment below. Every caller except
* content-morph omits this, since their scroll targets are already
* registered snap stops; content-morph's target can be a position the
* container's current snap-stop set doesn't know about yet.
*/
suspendSnap?: boolean): ScrollAnimation;
//#endregion
export { SpringConfig as a, spring as c, animate as i, animateScrollTo as n, SpringCurve as o, AnimateHandle as r, sampleSpringAt as s, ScrollAnimation as t };
import { t as Side } from "./geometry-C78uYrYJ.mjs";
import * as React from "react";
//#region packages/scrollsheet/src/internal/detents.d.ts
/**
* Detents — the stops a sheet can rest at, in the spirit of
* UISheetPresentationController's `detents`.
*
* A detent resolves to the visible height of the sheet in px.
* Accepted forms:
* - 'full' → viewport height minus top inset
* - 'medium' → 50% of viewport
* - 'content' → natural content height (measured, capped at full)
* - number in (0, 1] → fraction of viewport
* - `${number}px` → absolute pixels
*/
type DetentSpec = "full" | "medium" | "content" | number | `${number}px`;
//#endregion
//#region packages/scrollsheet/src/context.d.ts
/**
* onTravel's third argument. Reused and mutated in place every travel frame
* (see content.tsx's updateTravel) — read it synchronously, don't retain the
* reference across frames.
*/
interface TravelInfo {
/** [0, maxDetent] — the full resolved travel range in px, this frame. */
range: readonly [number, number];
/**
* 0-1 progress toward EACH configured detent, keyed by the detent's
* resolved height in px (not its spec — a spec like 'content' isn't a
* stable identity across resizes, but its resolved height this frame is
* what a consumer interpolates against).
*/
progressAtDetents: ReadonlyMap<number, number>;
}
//#endregion
//#region packages/scrollsheet/src/root.d.ts
/** Imperative escape hatch — see `actionsRef` on `<Sheet.Root>`. */
interface SheetActions {
open(): void;
close(): void;
/**
* Moves the sheet to `detent` through the same `setActiveDetent` path a
* controlled `activeDetent` change uses, so it animates identically. Warns
* in dev (once) if `detent` isn't one of this sheet's configured
* `detents` — only the panel's *rest position* resolves to the nearest
* configured detent in that case; `activeDetent`/`onActiveDetentChange`
* (and Handle's `aria-valuenow`/`aria-valuetext`) still reflect the
* literal value passed in here, not the resolved one.
*/
snapTo(detent: DetentSpec): void;
}
interface SheetRootProps {
children?: React.ReactNode;
/** Controlled open state. */
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
/**
* Fires once the open/close *transition* finishes (not just the state
* flip) — `true` when the sheet has fully settled open, `false` once it's
* fully closed and unmounted. Backed by the same phase timers that drive
* `data-scrollsheet-state`/`data-scrollsheet-side` (see Content) — no
* separate timer of its own.
*/
onOpenChangeComplete?: (open: boolean) => void;
/**
* Imperative escape hatch for cases a controlled `open`/`activeDetent`
* prop is awkward for (deep links, push notifications, a descendant
* component reaching up without prop-drilling): `ref.current?.close()` /
* `.open()` / `.snapTo(detent)`. Thin wrappers over the same `setOpen`/
* `setActiveDetent` the rest of the sheet uses — not a separate code path.
*/
actionsRef?: React.Ref<SheetActions>;
/**
* The stops the sheet can rest at, UISheetPresentationController-style.
* `'content'` (natural height), `'medium'` (50%), `'full'`, a 0–1 fraction,
* or `'320px'`.
* @default ['content']
*/
detents?: readonly DetentSpec[];
/** Controlled active detent (one of `detents`). */
activeDetent?: DetentSpec;
onActiveDetentChange?: (detent: DetentSpec) => void;
/**
* Master switch: when false, swipe/drag-to-dismiss, backdrop tap and Esc
* all stop closing the sheet. `escapeDismissible`/`backdropDismissible`
* below let you turn off just one of the pointer/keyboard paths while
* leaving the others (and swipe-to-dismiss, which only this prop gates)
* alone — both default to whatever `dismissible` is.
* @default true
*/
dismissible?: boolean;
/** Whether Esc closes the sheet. @default `dismissible` */
escapeDismissible?: boolean;
/** Whether tapping the backdrop/empty track closes the sheet. @default `dismissible` */
backdropDismissible?: boolean;
/** CSP nonce for the injected style tag. */
nonce?: string;
/**
* Blend the page's `<meta name="theme-color">` toward black as the sheet
* travels, so the browser chrome (iOS Safari tints both the status bar area
* and the bottom toolbar from this tag; Android tints the status bar) dims
* along with the backdrop. Every theme-color tag is dimmed from its own
* base color, so the usual media-scoped light/dark pair works and stays
* correct even if the OS scheme flips while the sheet is open. On a
* bottom-anchored attached sheet, the strip behind the bottom toolbar takes
* the panel's own color instead (via a fixed sentinel element), so the bar
* reads as part of the sheet, not the dimmed page. No-ops if no
* tag holds a parseable color, or on platforms that ignore theme-color.
* @default false
*/
themeColorDimming?: boolean;
/**
* Fires on every frame the sheet travels: revealed px, 0-1 progress
* (against the first detent, same number `--scrollsheet-progress` uses),
* and `info` — per-detent progress plus the resolved travel range. `info`
* is reused and mutated in place every frame (this is the highest-
* frequency callback in the library); read it synchronously, don't store
* the reference. Keep this handler cheap — it runs on every travel frame.
*/
onTravel?: (revealedPx: number, progress: number, info: TravelInfo) => void;
/** Which edge the sheet is anchored to. @default 'bottom' */
side?: Side;
/**
* When false, the page behind stays fully interactive and scrollable — no
* backdrop, no focus trap, no inert-ing the rest of the page. Renders on a
* `<div popover="manual">` (top layer, non-blocking) where supported,
* falling back to a non-modal `<dialog>` (`.show()`) otherwise.
* @default true
*/
modal?: boolean;
/**
* Applies an iOS-style card effect to the page element marked
* `data-scrollsheet-background`, driven by this sheet's own travel
* progress. `'scale'` scales/insets/rounds it; `'parallax'` only shifts it.
* No-ops if no element is marked.
*
* Left unset, a full-height bottom sheet in the mobile presentation gets
* `'scale'` — that is how the platform presents a sheet that covers the
* screen. Desktop drawers dock beside the page and never move it, and a
* detached (floating-card) sheet never claims the full screen, so neither
* defaults on. `'none'` opts out anywhere.
*/
backgroundEffect?: "scale" | "parallax" | "none";
/**
* Explicit target for backgroundEffect, instead of the default document-wide
* `[data-scrollsheet-background]` query. Pass the same ref you'd otherwise
* mark with the attribute. Skips the ownership check the implicit default
* applies (an explicit ref is unambiguous about which page element it
* means) — matches how an explicit `backgroundEffect` prop today already
* bypasses the implicit default's full-height/opener-containment check.
*/
backgroundRef?: React.RefObject<HTMLElement | null>;
/**
* Inner-content-overflow scrollbar style, for the panel's own scroll at
* max detent (or any content taller than the panel). `'overlay'`: a thin
* auto-hiding thumb (iOS/macOS-style). `'hidden'`: no thumb, no native
* scrollbar either. `'native'`: let the browser draw
* its own.
* @default 'overlay'
*/
scrollbar?: "overlay" | "hidden" | "native";
/**
* Scoped to exactly two things: the backdrop's opacity and
* `themeColorDimming`. Both stay fully transparent/undimmed while the
* sheet is at or below this detent's resolved height, then interpolate
* from there up to the next detent above it (or the tallest detent, if
* this is already the tallest). Unset (default): dims across the full
* range from closed to the first detent, today's behavior. Does *not*
* affect `onTravel`'s progress argument, `--scrollsheet-progress`,
* stacking recede, or `backgroundEffect` — those all track the sheet's
* full-range travel 1:1 regardless of this prop.
*/
largestUndimmedDetent?: DetentSpec;
/**
* Drag sessions (mouse, and touch on non-modal sheets) only start from
* `<Sheet.Handle>` — a pointerdown elsewhere on the panel no-ops. Modal
* touch (native scroll drives the gesture) is restricted via
* `touch-action` instead, except at the tallest detent, where the panel's
* own content scroll still needs it.
* @default false
*/
handleOnly?: boolean;
/**
* No drag sessions at all, handle included — detent changes are
* programmatic (`activeDetent`, `actionsRef`) or click/keyboard
* (`<Sheet.Handle>`) only. Esc and backdrop dismissal are unaffected.
* @default false
*/
disableDrag?: boolean;
/**
* A release never skips over an intermediate detent — the resolved target
* is clamped to the immediate neighbor of the detent the gesture started
* from (or, for a native-scroll settle, the last settled detent).
* @default false
*/
sequentialDetents?: boolean;
/**
* Fraction (0-1) of the first detent below which a release dismisses the
* sheet, replacing the built-in half-of-first-detent rule.
* @default 0.5
*/
closeThreshold?: number;
/**
* Bottom sheets only. When the software keyboard opens with a text field
* focused inside the sheet, promote to the tallest detent so the field
* has the whole remaining viewport to scroll within — a short peek detent
* can't show a field the keyboard would otherwise cover. Restores the
* previous detent on blur unless something else (a drag, a controlled
* change) moved the sheet in between. Gated on the keyboard actually
* appearing (a nonzero measured inset), never on focus alone, so a
* desktop click into an input doesn't expand anything. Promotion goes
* through the same `setActiveDetent` path a controlled change uses — a
* controlled sheet must apply `onActiveDetentChange` back to
* `activeDetent` or this prop has no visible effect.
* @default false
*/
keyboardExpands?: boolean;
/**
* Fires once a drag session resolves, with the real pointer event (the
* `buttons===0` recovery path may pass the triggering `pointermove`
* instead of a `pointerup`) and whether the sheet is staying open —
* `false` only when the release resolved to a dismiss.
*/
onRelease?: (event: PointerEvent, willRemainOpen: boolean) => void;
}
declare function Root$1({ children, open: openProp, defaultOpen, onOpenChange, onOpenChangeComplete, actionsRef, detents, activeDetent: activeDetentProp, onActiveDetentChange, dismissible, escapeDismissible, backdropDismissible, nonce, themeColorDimming, onTravel, side, modal, backgroundEffect, backgroundRef, scrollbar, largestUndimmedDetent, handleOnly, disableDrag, sequentialDetents, closeThreshold, keyboardExpands, onRelease }: SheetRootProps): React.JSX.Element;
//#endregion
//#region packages/scrollsheet/src/trigger.d.ts
interface SheetTriggerProps extends React.ComponentProps<"button"> {
/**
* Render the child element instead of a `<button>`, merging the trigger's
* props (aria, onClick) into it — for custom button components or links.
*/
asChild?: boolean;
}
declare const Trigger: React.ForwardRefExoticComponent<Omit<SheetTriggerProps, "ref"> & React.RefAttributes<HTMLButtonElement>>;
//#endregion
//#region packages/scrollsheet/src/content.d.ts
interface SheetContentProps extends React.ComponentProps<"div"> {
/** Accessible name when no <Sheet.Title> is rendered. */
"aria-label"?: string;
/**
* Stretches [data-scrollsheet-body] to fill the panel (flex column, the
* body itself flex:1/min-height:0) instead of sizing to its natural content
* height. Promotes the docs "Full-height content" recipe into the library —
* write your own inner-scroll child (flex:1, overflow-y:auto) with no CSS
* of your own required on the wrapper.
* @default false
*/
fill?: boolean;
/**
* Render the child element instead of scrollsheet's own panel `<div>`,
* merging the panel's props (role, tabIndex, ref, `data-scrollsheet-*`)
* into it — matches Radix `Dialog.Content asChild`: the child replaces the
* panel outright rather than nesting inside it. The child must be a single
* non-Fragment element; whatever it was given as its own children is
* hoisted into the body wrapper below (detent measurement and stacking
* still need a real DOM descendant there regardless of which element ends
* up as the panel).
* If `children` isn't a single non-Fragment element (missing, a string,
* multiple elements, a `<>...</>` Fragment…), `asChild` is ignored for
* that render — the default panel `<div>` is used instead, with a
* one-time dev warning.
* @default false
*/
asChild?: boolean;
}
declare const Content$1: React.ForwardRefExoticComponent<Omit<SheetContentProps, "ref"> & React.RefAttributes<HTMLDivElement>>;
//#endregion
//#region packages/scrollsheet/src/misc.d.ts
interface SheetTitleProps extends React.ComponentProps<"h2"> {
/** Render the child element instead of an `<h2>`, merging props into it. */
asChild?: boolean;
}
declare const Title: React.ForwardRefExoticComponent<Omit<SheetTitleProps, "ref"> & React.RefAttributes<HTMLHeadingElement>>;
interface SheetDescriptionProps extends React.ComponentProps<"p"> {
/** Render the child element instead of a `<p>`, merging props into it. */
asChild?: boolean;
}
declare const Description: React.ForwardRefExoticComponent<Omit<SheetDescriptionProps, "ref"> & React.RefAttributes<HTMLParagraphElement>>;
interface SheetCloseProps extends React.ComponentProps<"button"> {
/** Render the child element instead of a `<button>`, merging props into it. */
asChild?: boolean;
}
declare const Close: React.ForwardRefExoticComponent<Omit<SheetCloseProps, "ref"> & React.RefAttributes<HTMLButtonElement>>;
//#endregion
//#region packages/scrollsheet/src/handle.d.ts
interface SheetHandleProps extends React.ComponentProps<"button"> {
/** Render the child element instead of a `<button>`, merging props into it. */
asChild?: boolean;
/**
* Where the pill sits. `'inside'` (default) flows at the top of the
* sheet's content. `'floating'` overlays the content — absolutely
* positioned over the panel's top edge, for full-bleed content (maps,
* photos) a flow pill would push down. `'outside'` floats in the backdrop
* above the sheet's top edge (bottom sheets only; other sides render as
* `'inside'`) — it rides the canvas layer outside the panel's clip and
* drags, clicks, and keys exactly like the others.
* @default 'inside'
*/
variant?: "inside" | "floating" | "outside";
}
/**
* The grabber pill. Click cycles detents; ArrowUp/ArrowDown (or Left/Right
* for side sheets) move between them, Home/End jump to the first/last — so
* multi-detent sheets are fully keyboard-operable. With two or more detents
* it exposes itself as a slider (role, value min/max/now, value text) so
* screen readers announce the current stop, not just "button".
*
* The handle is optional. It's purely a click/keyboard affordance layered on
* top of the drag engine, which listens on the whole panel, not the handle —
* omit `<Sheet.Handle>` and the whole panel still drags and dismisses, in
* every mode (modal and non-modal) and every side (bottom/top/left/right).
* There's no boolean prop for this; omission is the API.
*/
declare const Handle$1: React.ForwardRefExoticComponent<Omit<SheetHandleProps, "ref"> & React.RefAttributes<HTMLButtonElement>>;
//#endregion
//#region packages/scrollsheet/src/drawer/index.d.ts
/** vaul's snap point shape: a 0–1 fraction, an absolute `'###px'` string, or `'fit-content'`. */
type VaulSnapPoint = number | string;
/**
* Resolves vaul's `fadeFromIndex` against `snapPoints` into a
* `largestUndimmedDetent` spec, mirroring real vaul's own default (its
* `src/index.tsx`: `fadeFromIndex = snapPoints && snapPoints.length - 1`) —
* omitted with `snapPoints` set defaults to the *topmost* snap point index
* (no dim until the last snap point), not "no undimmed range at all"
* (scrollsheet's own default when `largestUndimmedDetent` is never touched).
* No `snapPoints`, or an out-of-range explicit index, resolves to
* `undefined` (today's full-dim-range behavior) — pulled out as its own pure
* function so this index math is unit-testable independent of rendering.
*/
declare function resolveFadeFromIndex(snapPoints: readonly VaulSnapPoint[] | undefined, fadeFromIndex: number | undefined): DetentSpec | undefined;
/**
* Resolves vaul's `closeThreshold` against whether `snapPoints` is set,
* CONVERTING between opposite conventions: vaul counts the fraction dragged
* AWAY (its 0.25 default = dismiss after a quarter-height drag), scrollsheet
* counts the fraction still VISIBLE (`isBelowCloseThreshold`: dismiss when
* `revealed < firstDetent * closeThreshold`, so higher = easier). A raw
* passthrough inverts the migrated feel — vaul's 0.25 became "drag 75% to
* dismiss", harder than scrollsheet's own 0.5 default instead of easier —
* so the mapping is `1 - value`, and an omitted prop resolves to vaul's own
* `CLOSE_THRESHOLD` default of 0.25 (=> 0.75 here), never `Sheet.Root`'s
* unrelated 0.5.
* `snapPoints` set makes `closeThreshold` dead code in real vaul (its
* `onRelease` returns through the snap-points branch before ever reading
* it), matched here by always resolving to `undefined` in that case.
* Pulled out as its own pure function, mirroring `resolveFadeFromIndex`,
* so the conversion math is unit-testable independent of rendering.
*/
declare function resolveCloseThreshold(snapPoints: readonly VaulSnapPoint[] | undefined, closeThreshold: number | undefined): number | undefined;
/**
* Composes vaul's `onClose` on top of `onOpenChange`: `onClose` fires
* whenever the transition target is `false`, then `onOpenChange` always
* fires. Real vaul's `closeDrawer()` calls `onClose` from every dismiss
* path (swipe past threshold, Esc, backdrop, imperative); scrollsheet's own
* `onOpenChange` is already the single funnel every one of *its* dismiss
* paths goes through (`useControllableState`'s `onChange`, which fires
* exactly once per real open/false transition), so layering `onClose` on
* top of that wiring reproduces "every dismiss path" without a second one.
* Returns `undefined` when neither callback is given, matching the
* conditional-wrapper convention `onRelease`'s cast uses below. Pulled out
* as its own pure function so the composition is unit-testable without a
* live DOM/click event.
*/
declare function composeOpenChange(onOpenChange: ((open: boolean) => void) | undefined, onClose: (() => void) | undefined): ((open: boolean) => void) | undefined;
/**
* vaul's own props, translated — plus (via the Pick) the scrollsheet-native
* props that have no vaul counterpart and forward to `Sheet.Root`
* untranslated: `backdropDismissible` (the escape hatch vaul served with the
* Radix onPointerDownOutside/onInteractOutside preventDefault idiom),
* `escapeDismissible` (same for onEscapeKeyDown), `keyboardExpands`,
* `onTravel` (the live counterpart of vaul's ignored `onDrag`), `scrollbar`,
* and `actionsRef`. Only props with no vaul name-collision are forwarded —
* anything vaul also has (`closeThreshold`, `modal`, `dismissible`, …) keeps
* its translated vaul semantics above.
*/
interface DrawerRootProps extends Pick<SheetRootProps, "actionsRef" | "backdropDismissible" | "escapeDismissible" | "keyboardExpands" | "onTravel" | "scrollbar"> {
children?: React.ReactNode;
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
/**
* Fires whenever the drawer closes — every dismiss path (swipe past
* threshold, Esc, backdrop tap, imperative `close()`) funnels through the
* same `onOpenChange` wiring this is layered on top of, matching real
* vaul's `closeDrawer()`, which calls `onClose` on every one of those
* paths too.
*/
onClose?: () => void;
/** @default true */
dismissible?: boolean;
/** Fractions (0–1), `'###px'` strings, or `'fit-content'` — translated to scrollsheet `detents`. */
snapPoints?: readonly VaulSnapPoint[];
/** Optionally-controlled active snap point; `null` means "no explicit snap point". */
activeSnapPoint?: VaulSnapPoint | null;
setActiveSnapPoint?: (snapPoint: VaulSnapPoint | null) => void;
/** Fires when the open/close transition actually completes — wired to `onOpenChangeComplete`, exact rather than vaul's timer. */
onAnimationEnd?: (open: boolean) => void;
/** Ignored — the native `<dialog>` manages focus itself. */
autoFocus?: boolean;
/** Ignored — nesting is automatic: a `<Drawer.Content>` rendered inside another registers itself. */
nested?: boolean;
/** Maps to scrollsheet `side` — all four edges. */
direction?: "top" | "bottom" | "left" | "right";
/** Maps to scrollsheet `modal` — `false` renders non-modal (Popover top layer, page stays interactive). */
modal?: boolean;
/**
* Maps to `backgroundEffect="scale"`; vaul's own `[data-vaul-drawer-wrapper]`
* is picked up as the target. Left false (vaul's default), this maps to
* `'none'`, not unset — vaul never scales the page unless asked, so the
* scrollsheet default for full-height sheets must not leak in here.
*/
shouldScaleBackground?: boolean;
/**
* Maps to scrollsheet `closeThreshold` — but only when `snapPoints` is
* unset. In real vaul, `closeThreshold` is dead code once `snapPoints`
* exist (its `onRelease` returns through the snap-points branch before
* ever reading it); this compat layer matches that rather than giving the
* prop unconditional live effect. Passing both together warns once in dev
* — use the native `Sheet.Root closeThreshold` directly if you want it to
* combine with detents.
*
* Omitted (with `snapPoints` unset) resolves to vaul's own default of
* `0.25` (`CLOSE_THRESHOLD` in vaul's `src/constants.ts`) — not
* scrollsheet's own `0.5` default, which is twice the drag distance.
*/
closeThreshold?: number;
/**
* Maps to `largestUndimmedDetent`, resolved against `snapPoints` at this
* index. Omitted (with `snapPoints` set) defaults to `snapPoints.length -
* 1` — vaul's own default, meaning no dim until the topmost snap point —
* not "dim across the full range" (scrollsheet's own default when
* `largestUndimmedDetent` is never touched at all).
*/
fadeFromIndex?: number;
/** Maps to `sequentialDetents`. */
snapToSequentialPoint?: boolean;
/** Maps to `handleOnly` directly. */
handleOnly?: boolean;
/**
* Maps to `onRelease`. Typed as vaul's own `React.PointerEvent<HTMLDivElement>`
* (rather than scrollsheet's wider native-`PointerEvent` type) so a
* handler already typed against vaul's declaration still assigns cleanly —
* see the internal cast in `Root` for why this is safe at runtime.
*/
onRelease?: (event: React.PointerEvent<HTMLDivElement>, open: boolean) => void;
setBackgroundColorOnScale?: boolean;
noBodyStyles?: boolean;
disablePreventScroll?: boolean;
preventScrollRestoration?: boolean;
repositionInputs?: boolean;
scrollLockTimeout?: number;
onDrag?: (event: React.PointerEvent, percentageDragged: number) => void;
container?: HTMLElement | null;
/**
* Ignored — real vaul's `fixed` swaps its own keyboard-avoidance strategy
* (resize instead of translate); scrollsheet's `useKeyboardViewport` has
* no equivalent toggle.
*/
fixed?: boolean;
}
declare function Root(props: DrawerRootProps): React.JSX.Element;
/**
* Nested drawers just work: a `<Drawer.Content>` rendered inside another
* `<Drawer.Content>` automatically registers with the parent's stacking
* context (the parent recedes while the child is open), the way iOS stacks
* sheets. `NestedRoot` is an alias of `Root` kept for drop-in compatibility
* with vaul's API surface — there's nothing distinct for it to do.
*/
declare const NestedRoot: typeof Root;
type DrawerNestedRootProps = DrawerRootProps;
interface DrawerPortalProps {
children?: React.ReactNode;
/** Ignored — the native `<dialog>` always renders into the browser's top layer. */
container?: HTMLElement | null;
}
declare function Portal({ children, container }: DrawerPortalProps): React.JSX.Element;
type DrawerOverlayProps = React.ComponentProps<"div">;
/**
* scrollsheet draws its own backdrop (`.scrollsheet-backdrop`, themeable via
* the `--scrollsheet-backdrop` CSS variable) that tracks scroll progress, so
* there's nothing for a separate overlay element to render. This accepts
* vaul's `<Drawer.Overlay className .../>` so existing JSX doesn't crash —
* restyle the backdrop through the CSS variable instead.
*/
declare function Overlay(_props: DrawerOverlayProps): null;
/**
* Whether translated `snapPoints` produced more than one distinct detent —
* feeds Content's `data-vaul-snap-points` attribute. vaul's own
* CSS-selector convention treats a single snap point the same as none
* (scrollsheet's own single-detent default also resolves to `false` here).
* Pulled out as its own pure function, mirroring `resolveFadeFromIndex` /
* `resolveCloseThreshold`, so it's unit-testable independent of rendering
* (`<Drawer.Content>`'s actual DOM output is behind a client-only mount
* gate — see content.tsx — so SSR can't observe the attribute directly).
*/
declare function hasMultipleSnapPoints(detents: readonly DetentSpec[]): boolean;
interface DrawerContentProps extends SheetContentProps {
onPointerDownOutside?: (event: CustomEvent) => void;
onOpenAutoFocus?: (event: Event) => void;
onEscapeKeyDown?: (event: KeyboardEvent) => void;
onCloseAutoFocus?: (event: Event) => void;
onInteractOutside?: (event: CustomEvent) => void;
onFocusOutside?: (event: CustomEvent) => void;
forceMount?: boolean;
}
declare const Content: React.ForwardRefExoticComponent<Omit<DrawerContentProps, "ref"> & React.RefAttributes<HTMLDivElement>>;
/**
* Composes the click handler Handle passes to `SheetHandle`: the caller's
* own `onClick` always fires first, then — when `preventCycle` is set —
* `event.preventDefault()`, the same signal `SheetHandle`'s own
* click-to-cycle logic already checks (`if (event.defaultPrevented) return`
* in handle.tsx) to skip advancing to the next detent. Mirrors real vaul's
* `preventCycle`, read inside its own `handleCycleSnapPoints` guard. Pulled
* out as its own pure function so the composition is unit-testable without
* a live DOM/click event.
*/
declare function composeHandleClick(preventCycle: boolean | undefined, onClick: ((event: React.MouseEvent<HTMLButtonElement>) => void) | undefined): (event: React.MouseEvent<HTMLButtonElement>) => void;
interface DrawerHandleProps extends SheetHandleProps {
/**
* Suppresses the click-to-cycle-detents behavior — the handle still
* renders, drags, and is keyboard-operable as usual, but a click no
* longer advances to the next detent. Mirrors real vaul's own
* `preventCycle`, which guards the same click path (vaul's handle has no
* keyboard cycling to suppress).
*/
preventCycle?: boolean;
}
declare const Handle: React.ForwardRefExoticComponent<Omit<DrawerHandleProps, "ref"> & React.RefAttributes<HTMLButtonElement>>;
/**
* Sheet.Close's self-closing form renders a styled ✕ default; vaul's own
* `<Drawer.Close />` renders an empty unstyled button. A migrating user
* must not get a surprise icon button from a version swap, so the compat
* Close pins children to null (defined, so the styled-default branch never
* arms) unless the caller passed some.
*/
declare const DrawerClose: React.ForwardRefExoticComponent<Omit<SheetCloseProps, "ref"> & React.RefAttributes<HTMLButtonElement>>;
/** Namespace-style access matching vaul's `<Drawer.Root>…</Drawer.Root>` shape. */
declare const Drawer: {
Root: typeof Root;
NestedRoot: typeof Root;
Trigger: React.ForwardRefExoticComponent<Omit<SheetTriggerProps, "ref"> & React.RefAttributes<HTMLButtonElement>>;
Portal: typeof Portal;
Overlay: typeof Overlay;
Content: React.ForwardRefExoticComponent<Omit<DrawerContentProps, "ref"> & React.RefAttributes<HTMLDivElement>>;
Close: React.ForwardRefExoticComponent<Omit<SheetCloseProps, "ref"> & React.RefAttributes<HTMLButtonElement>>;
Title: React.ForwardRefExoticComponent<Omit<SheetTitleProps, "ref"> & React.RefAttributes<HTMLHeadingElement>>;
Description: React.ForwardRefExoticComponent<Omit<SheetDescriptionProps, "ref"> & React.RefAttributes<HTMLParagraphElement>>;
Handle: React.ForwardRefExoticComponent<Omit<DrawerHandleProps, "ref"> & React.RefAttributes<HTMLButtonElement>>;
};
//#endregion
export { SheetContentProps as A, Close as C, SheetTitleProps as D, SheetDescriptionProps as E, SheetRootProps as F, TravelInfo as I, DetentSpec as L, Trigger as M, Root$1 as N, Title as O, SheetActions as P, SheetHandleProps as S, SheetCloseProps as T, composeOpenChange as _, DrawerHandleProps as a, resolveFadeFromIndex as b, DrawerPortalProps as c, NestedRoot as d, Overlay as f, composeHandleClick as g, VaulSnapPoint as h, DrawerContentProps as i, SheetTriggerProps as j, Content$1 as k, DrawerRootProps as l, Root as m, Drawer as n, DrawerNestedRootProps as o, Portal as p, DrawerClose as r, DrawerOverlayProps as s, Content as t, Handle as u, hasMultipleSnapPoints as v, Description as w, Handle$1 as x, resolveCloseThreshold as y };
import * as React from "react";
//#region packages/scrollsheet/src/toast/state.d.ts
/** Drives the row's icon/role and the `data-type` attribute the stylesheet reads. */
type ToastType = "default" | "success" | "error" | "warning" | "info" | "loading";
/**
* The six-value position union (matches real Sonner's own set). Every value
* routes for real at this layer (see `selectPositionToasts` below) — how
* many of the six a given `<Toaster>` actually renders is that component's
* own call (`toaster.tsx`'s `resolveToasterPosition`), not something this
* module gates.
*/
type ToastPosition = "top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right";
/** Compat alias for the pre-neutral-naming name — new code should use `ToastPosition`. */
type SonnerPosition = ToastPosition;
/**
* Real Sonner's own swipe-dismiss edge union (types.ts:152). A Toaster-level
* `swipeDirections` override (see `ToasterProps` in `../toaster`) understands
* these four values regardless of which position they're applied to — lives
* here, not in the shell folder, so both the Sheet-backed `toaster.tsx` and
* the new shell can reference the type without either depending on the
* other's own directory.
*/
type SwipeDirection = "top" | "right" | "bottom" | "left";
interface ToastAction {
label: React.ReactNode;
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
}
/** Per-slot extra class names, mirrors real Sonner's own `ToastClassnames` field-for-field. */
interface ToastClassnames {
toast?: string;
title?: string;
description?: string;
loader?: string;
closeButton?: string;
cancelButton?: string;
actionButton?: string;
success?: string;
error?: string;
info?: string;
warning?: string;
loading?: string;
default?: string;
content?: string;
icon?: string;
}
/** Per-type icon overrides, mirrors real Sonner's own `ToastIcons`. */
interface ToastIcons {
success?: React.ReactNode;
info?: React.ReactNode;
warning?: React.ReactNode;
error?: React.ReactNode;
loading?: React.ReactNode;
close?: React.ReactNode;
}
/** The `data` argument to `toast()` / `.success()` / etc — mirrors Sonner's own `ExternalToast`. */
interface ToastData {
/** Update-in-place: reusing an id already in the queue merges onto that entry instead of pushing a new one. */
id?: number | string;
description?: React.ReactNode;
/** Milliseconds before auto-dismiss, or `Infinity` to disable it. @default 4000 (Toaster's own `duration`, or `toastOptions.duration`) */
duration?: number;
/** @default true */
dismissible?: boolean;
icon?: React.ReactNode;
action?: ToastAction;
cancel?: ToastAction;
className?: string;
classNames?: ToastClassnames;
onDismiss?: (toast: ToastRecord) => void;
onAutoClose?: (toast: ToastRecord) => void;
/** Routes this toast to the `<Toaster toasterId>` with the matching id instead of the default, un-keyed toaster. */
toasterId?: string;
/** Which position's `<ol>` group this toast renders in — omitted routes to the owning `<Toaster>`'s own `position` prop instead (see `selectPositionToasts`). Valid regardless of which positions that Toaster actually renders. */
position?: ToastPosition;
/** Rendered as `data-testid` on the row — mirrors real Sonner's own `testId`. */
testId?: string;
/** Overrides the close button's accessible name for this toast only. @default "Close toast" (or the Toaster's own `toastOptions.closeButtonAriaLabel`) */
closeButtonAriaLabel?: string;
/**
* Inline styles for this toast's row element, merged over the row's own
* stacking variables and `toastOptions.style` (last wins, so `top`,
* `zIndex`, or any `--scrollsheet-toast-*` variable can be overridden
* per-toast) — mirrors real Sonner's own `ExternalToast.style`.
*/
style?: React.CSSProperties;
}
/** A live queue entry — what `useSonner()` and each row render from. */
interface ToastRecord extends ToastData {
id: number | string;
type: ToastType;
title?: React.ReactNode;
/** Set by `toast.custom()` — its presence means "render this node verbatim, skip the card chrome" (see ToastRow). */
jsx?: React.ReactNode;
createdAt: number;
}
/**
* A `success`/`error` handler may resolve to this instead of a plain node —
* `message` becomes the title, every other field (description, action,
* icon, ...) applies to the settled toast. Mirrors real Sonner's own
* `PromiseIExtendedResult` (state.ts's `isExtendedResult` check).
*/
type ToastPromiseExtendedResult = Omit<ToastData, "id"> & {
message?: React.ReactNode;
};
interface ToastPromiseData<T> extends Omit<ToastData, "id" | "description"> {
id?: number | string;
loading?: React.ReactNode;
success?: React.ReactNode | ((data: T) => React.ReactNode | ToastPromiseExtendedResult | Promise<React.ReactNode | ToastPromiseExtendedResult>);
error?: React.ReactNode | ((error: unknown) => React.ReactNode | ToastPromiseExtendedResult | Promise<React.ReactNode | ToastPromiseExtendedResult>);
/**
* A static value carries through to the settled toast unchanged; a
* function is called with the settle value (the resolved data, or the
* error/HTTP-status string on the error branches) — matches real Sonner's
* own per-branch description resolution (it never wipes a static one).
*/
description?: React.ReactNode | ((value: unknown) => React.ReactNode | Promise<React.ReactNode>);
finally?: () => void;
}
type ToastFn = ((message: React.ReactNode, data?: ToastData) => number | string) & {
success: (message: React.ReactNode, data?: ToastData) => number | string;
error: (message: React.ReactNode, data?: ToastData) => number | string;
info: (message: React.ReactNode, data?: ToastData) => number | string;
warning: (message: React.ReactNode, data?: ToastData) => number | string;
loading: (message: React.ReactNode, data?: ToastData) => number | string;
/** Alias for the base call, with no type — matches real Sonner's `toast.message()`. */
message: (message: React.ReactNode, data?: ToastData) => number | string;
/**
* `jsx` is a render function receiving the resolved id (matching real
* Sonner), so a custom toast can call `toast.dismiss(id)` on itself. The
* resulting node is stored on the record's `jsx` field, which ToastRow
* renders verbatim, skipping the card chrome (icon/title/description)
* entirely.
*/
custom: (jsx: (id: number | string) => React.ReactNode, data?: ToastData) => number | string;
promise: <T>(promiseOrFn: Promise<T> | (() => Promise<T>), data: ToastPromiseData<T>) => (number | string) & {
unwrap: () => Promise<T>;
};
dismiss: (id?: number | string) => number | string | undefined;
/** Every live (not yet dismissed) toast across every toaster — mirrors real Sonner's own `toast.getToasts()`. */
getToasts: () => readonly ToastRecord[];
/** Every toast ever created, oldest-first, capped at 100 — mirrors real Sonner's own `toast.getHistory()`. */
getHistory: () => readonly ToastRecord[];
};
declare const toast: ToastFn;
/**
* Subscribes to the same store `toast()` writes to. Real Sonner's own
* `useSonner()` takes no arguments and has no visible per-toaster filtering
* in its public surface — matched here unfiltered, across every toaster.
* Not verified byte-for-byte against Sonner's source; matches its observed
* behavior.
*/
declare function useSonner(): {
toasts: ToastRecord[];
};
/** Neutral-named alias for `useSonner` — same implementation, the name fresh docs teach. */
declare const useToasts: typeof useSonner;
//#endregion
//#region packages/scrollsheet/src/toast/toaster.d.ts
/** Pure, exported for tests. @default 3, matching real Sonner's own default. */
declare function resolveVisibleToasts(count: number | undefined): number;
interface ToasterProps {
/** Routes this Toaster to only the toasts created with a matching `toasterId` — omitted (the common case) renders the default, un-keyed queue. */
id?: string;
/** @deprecated Use `id` instead — matches real Sonner's own `Toaster` prop name. */
toasterId?: string;
/** Which corner (or edge-center) this Toaster's own default `<ol>` renders in. All six positions are real. @default 'bottom-right' */
position?: ToastPosition;
/**
* v1 always renders the static light card real Sonner ships by default.
* 'dark'/'system' aren't implemented yet (a v1.1 follow-up) and warn once
* rather than silently no-op — forcing a real visual mismatch would be
* worse than an honest warning.
*/
theme?: "light" | "dark" | "system";
/** Not implemented yet (v1.1) — warns once if set. */
richColors?: boolean;
/** Force the row list open (instead of the collapsed front-card-plus-ghosts) even without hover/focus. */
expand?: boolean;
/**
* Maximum toasts visible (interactive, full opacity) at once per position
* group. Beyond this, older toasts become hidden — `data-visible="false"`,
* still a real DOM node, still timing — rather than evicted; nothing is
* ever dismissed by overflow, and `dismissible: false` earns no special
* protection (it overflows into hidden exactly like any other toast).
* @default 3 (matches real Sonner's own visibleToasts default)
*/
visibleToasts?: number;
/** @default false */
closeButton?: boolean;
/** Milliseconds before auto-dismiss; a per-toast `duration` wins over this. @default 4000 */
duration?: number;
/** Gap between stacked rows, px. @default 14 */
gap?: number;
/** Distance from the viewport edge, px (or any CSS length). @default 24 (16 at the <600px full-bleed breakpoint) */
offset?: number | string;
/**
* Which edge(s) each toast may be swiped away from, overriding the
* per-position default (a corner position allows both its own y and x
* edges; a *-center position only its own y edge). One setting for the
* whole Toaster, applied uniformly to every position group it renders —
* matches real Sonner's own single Toaster-wide `swipeDirections` prop
* (it has no per-toast override).
*/
swipeDirections?: readonly SwipeDirection[];
/** Defaults merged underneath each individual `toast()` call's own options. */
toastOptions?: Omit<ToastData, "id" | "toasterId">;
/** Per-type icon overrides; a per-toast `icon` always wins over these. */
icons?: ToastIcons;
className?: string;
style?: React.CSSProperties;
/** Accessible name for the notifications region. @default "Notifications" */
containerAriaLabel?: string;
/**
* Global keyboard shortcut that expands the stack and moves focus into it
* — every field must be truthy on the event (a modifier like `altKey`, or
* a `KeyboardEvent.code` match), same contract as real Sonner's own
* `hotkey` prop. Escape while focus is inside the region collapses it
* back. Pass `[]` to disable.
* @default ['altKey', 'KeyT']
*/
hotkey?: readonly string[];
/** CSP nonce for the injected style tag. */
nonce?: string;
}
//#endregion
//#region packages/scrollsheet/src/toast/shell/toaster-shell.d.ts
declare function ToasterShell({ id, toasterId, position, theme, richColors, expand: forceExpand, visibleToasts: visibleToastsProp, closeButton, duration: durationProp, gap, offset, swipeDirections, toastOptions, icons, className, style, containerAriaLabel, hotkey, nonce }: ToasterProps): React.ReactPortal | null;
//#endregion
//#region packages/scrollsheet/src/toast/toast-styles.d.ts
/**
* Injects the toast stylesheet into a shadow root via adoptedStyleSheets
* instead of document.head — mirrors src/internal/styles.ts's
* injectStylesInto. Call once per shadow root that hosts a <Toaster>,
* before it first renders. A sibling export rather than folding into
* injectStylesInto: the toast layer is its own chunk (dist/toast.mjs) that
* never loads for a core-only consumer, so its shadow-root injector has to
* be reachable without pulling core's own chunk (and vice versa) — one
* shared call would break that tree-shaking boundary.
*
* Falls back silently to a <style> element appended to the shadow root
* itself on engines without constructable stylesheets (Safari <16.4).
* `nonce` only applies on that fallback path: it sets the injected
* `<style>` element's `nonce` attribute so a CSP `style-src` policy with
* `'nonce-...'` allows it. The `adoptedStyleSheets` path constructs a
* `CSSStyleSheet` and adopts it directly — it is never parsed as an inline
* style element, so CSP has no inline-style check to gate there and `nonce`
* is accepted (for a stable signature across both paths) but unused.
*/
declare function injectToastStylesInto(root: ShadowRoot, nonce?: string): void;
//#endregion
export { SonnerPosition as a, ToastData as c, ToastPromiseData as d, ToastRecord as f, useToasts as g, useSonner as h, resolveVisibleToasts as i, ToastIcons as l, toast as m, ToasterShell as n, ToastAction as o, ToastType as p, ToasterProps as r, ToastClassnames as s, injectToastStylesInto as t, ToastPosition as u };
"use client";
import { c as prefersReducedMotion, h as warnOnce, n as useCloseWatcher, r as createStyleInjector, t as cn } from "./cn-CActSFFr.mjs";
import * as React from "react";
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
import { createPortal } from "react-dom";
//#region packages/scrollsheet/src/toast/state.ts
let toasts = [];
const listeners = new Set();
let uid = 0;
const MAX_HISTORY_SIZE = 100;
let history = [];
function trimHistory() {
let toRemove = history.length - MAX_HISTORY_SIZE;
if (toRemove <= 0) return;
const liveIds = new Set(toasts.map((t) => t.id));
history = history.filter((record) => {
if (toRemove > 0 && !liveIds.has(record.id)) {
toRemove -= 1;
return false;
}
return true;
});
}
function recordHistory(record) {
const index = history.findIndex((t) => t.id === record.id);
if (index === -1) {
history = [...history, record];
trimHistory();
return;
}
history = history.map((t, i) => i === index ? record : t);
}
function nextId() {
uid += 1;
return uid;
}
function publish() {
for (const listener of listeners) listener();
}
function subscribe(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
}
function getSnapshot() {
return toasts;
}
const EMPTY_TOASTS = [];
function getServerSnapshot() {
return EMPTY_TOASTS;
}
function upsert(id, patch, fallbackType) {
const resolvedId = id ?? nextId();
const index = toasts.findIndex((t) => t.id === resolvedId);
const type = patch.type ?? fallbackType;
let record;
if (index === -1) {
record = {
dismissible: true,
...patch,
id: resolvedId,
type,
createdAt: Date.now()
};
toasts = [...toasts, record];
} else {
record = {
...toasts[index],
...patch,
id: resolvedId,
type
};
toasts = toasts.map((t, i) => i === index ? record : t);
}
recordHistory(record);
publish();
return resolvedId;
}
function dismiss(id) {
if (id === void 0) {
const swept = new Set(toasts);
for (const t of swept) {
if (!toasts.includes(t)) continue;
t.onDismiss?.(t);
}
toasts = toasts.filter((t) => !swept.has(t));
publish();
return;
}
const existing = toasts.find((t) => t.id === id);
if (!existing) return id;
existing.onDismiss?.(existing);
toasts = toasts.filter((t) => t !== existing);
publish();
return id;
}
function expire(id) {
const existing = toasts.find((t) => t.id === id);
if (!existing) return;
existing.onAutoClose?.(existing);
toasts = toasts.filter((t) => t !== existing);
publish();
}
function baseToast(message, data) {
return upsert(data?.id, {
...data,
title: message
}, "default");
}
function withType(type) {
return (message, data) => upsert(data?.id, {
...data,
title: message
}, type);
}
const toastImpl = baseToast;
toastImpl.success = withType("success");
toastImpl.error = withType("error");
toastImpl.info = withType("info");
toastImpl.warning = withType("warning");
toastImpl.loading = withType("loading");
toastImpl.message = withType("default");
toastImpl.custom = (jsx, data) => {
const id = data?.id ?? nextId();
return upsert(id, {
...data,
jsx: jsx(id)
}, "default");
};
function isHttpResponse(value) {
return typeof value === "object" && value !== null && "ok" in value && typeof value.ok === "boolean" && "status" in value && typeof value.status === "number";
}
function isPromiseExtendedResult(value) {
return typeof value === "object" && value !== null && !React.isValidElement(value);
}
function applyPromiseSettlement(id, resolved, fallbackTitle, description, type) {
if (isPromiseExtendedResult(resolved)) {
const { message, ...rest } = resolved;
upsert(id, {
description,
...rest,
title: message ?? fallbackTitle,
type
}, type);
return;
}
upsert(id, {
title: resolved ?? fallbackTitle,
description,
type
}, type);
}
function promiseImpl(promiseOrFn, data) {
const { loading, success, error, finally: onFinally, description, ...rest } = data;
const id = upsert(data.id, {
...rest,
description: typeof description === "function" ? void 0 : description,
title: loading,
type: "loading"
}, "loading");
const settle = typeof promiseOrFn === "function" ? promiseOrFn() : promiseOrFn;
let settled;
const chain = settle.then(async (result) => {
if (isHttpResponse(result) && !result.ok) {
settled = {
ok: false,
reason: result
};
const statusMessage = `HTTP error! status: ${result.status}`;
const resolvedDescription = typeof description === "function" ? await description(statusMessage) : description;
const resolved = typeof error === "function" ? await error(statusMessage) : error;
applyPromiseSettlement(id, resolved, "Error", resolvedDescription, "error");
return;
}
if (result instanceof Error) {
settled = {
ok: false,
reason: result
};
const resolvedDescription = typeof description === "function" ? await description(result) : description;
const resolved = typeof error === "function" ? await error(result) : error;
applyPromiseSettlement(id, resolved, "Error", resolvedDescription, "error");
return;
}
settled = {
ok: true,
value: result
};
const resolvedDescription = typeof description === "function" ? await description(result) : description;
const resolved = typeof success === "function" ? await success(result) : success;
applyPromiseSettlement(id, resolved, "Success", resolvedDescription, "success");
}).catch(async (err) => {
settled = {
ok: false,
reason: err
};
const resolvedDescription = typeof description === "function" ? await description(err) : description;
const resolved = typeof error === "function" ? await error(err) : error;
applyPromiseSettlement(id, resolved, "Error", resolvedDescription, "error");
}).finally(() => onFinally?.());
const unwrap = () => chain.then(() => {
if (!settled) throw new Error("scrollsheet toast.promise: chain settled with no outcome");
if (settled.ok) return settled.value;
throw settled.reason;
});
return Object.assign(id, { unwrap });
}
toastImpl.promise = promiseImpl;
toastImpl.dismiss = dismiss;
toastImpl.getToasts = () => toasts;
toastImpl.getHistory = () => history;
const toast = toastImpl;
function useSonner() {
return { toasts: React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) };
}
const useToasts = useSonner;
function selectToasterToasts(all, toasterId) {
return all.filter((t) => toasterId === void 0 ? t.toasterId === void 0 : t.toasterId === toasterId);
}
function selectPositionToasts(all, position, defaultPosition) {
return all.filter((t) => t.position === void 0 ? position === defaultPosition : t.position === position);
}
function selectToastWindow(toasts, visibleToasts) {
const newestFirst = [...toasts].reverse();
return {
visible: newestFirst.slice(0, visibleToasts),
hidden: newestFirst.slice(visibleToasts)
};
}
//#endregion
//#region packages/scrollsheet/src/toast/toast-styles.ts
const { injectDocument, injectShadowRoot } = createStyleInjector("", "data-sonner-toast-styles");
function injectToastStyles(nonce) {
injectDocument(nonce);
}
function injectToastStylesInto(root, nonce) {
injectShadowRoot(root, nonce);
}
//#endregion
//#region packages/scrollsheet/src/toast/toaster.tsx
function resolveVisibleToasts(count) {
if (count === void 0) return 3;
return Math.max(1, Math.floor(count));
}
function useMountedFlag() {
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => {
if (prefersReducedMotion()) {
setMounted(true);
return;
}
let raf2 = 0;
const raf1 = requestAnimationFrame(() => {
raf2 = requestAnimationFrame(() => setMounted(true));
});
return () => {
cancelAnimationFrame(raf1);
cancelAnimationFrame(raf2);
};
}, []);
return mounted;
}
function useIsDocumentHidden() {
const [hidden, setHidden] = React.useState(() => typeof document !== "undefined" && document.hidden);
React.useEffect(() => {
const onVisibilityChange = () => setHidden(document.hidden);
document.addEventListener("visibilitychange", onVisibilityChange);
return () => document.removeEventListener("visibilitychange", onVisibilityChange);
}, []);
return hidden;
}
const EMPTY_RECORDS = [];
const EMPTY_MAP = new Map();
function toIdMap(records) {
return new Map(records.map((r) => [r.id, r]));
}
function sameIds(a, b) {
if (a.size !== b.size) return false;
for (const id of a.keys()) if (!b.has(id)) return false;
return true;
}
function useToastExit(live, queued = EMPTY_RECORDS, exitMs = 220) {
const [exiting, setExiting] = React.useState(() => EMPTY_MAP);
const [prevLive, setPrevLive] = React.useState(() => toIdMap(live));
const timersRef = React.useRef(new Map());
const liveIds = new Set(live.map((r) => r.id));
let renderExiting = exiting;
if (!sameIds(prevLive, liveIds)) {
const queuedIds = new Set(queued.map((r) => r.id));
const justRemoved = [];
for (const [id, record] of prevLive) {
if (liveIds.has(id)) continue;
if (queuedIds.has(id)) continue;
justRemoved.push(record);
}
setPrevLive(toIdMap(live));
if (justRemoved.length > 0) {
const next = new Map(exiting);
for (const record of justRemoved) next.set(record.id, record);
renderExiting = next;
setExiting(next);
}
}
React.useEffect(() => {
const reduced = prefersReducedMotion();
for (const [id] of exiting) {
if (timersRef.current.has(id)) continue;
const timer = setTimeout(() => {
timersRef.current.delete(id);
setExiting((prev) => {
if (!prev.has(id)) return prev;
const next = new Map(prev);
next.delete(id);
return next;
});
}, reduced ? 0 : exitMs);
timersRef.current.set(id, timer);
}
for (const [id, timer] of timersRef.current) if (!exiting.has(id)) {
clearTimeout(timer);
timersRef.current.delete(id);
}
}, [exiting, exitMs]);
React.useEffect(() => {
const timers = timersRef.current;
return () => {
for (const timer of timers.values()) clearTimeout(timer);
timers.clear();
};
}, []);
const rows = live.map((record) => ({
record,
exiting: false
}));
for (const [id, record] of renderExiting) if (!liveIds.has(id)) rows.push({
record,
exiting: true
});
return rows;
}
//#endregion
//#region packages/scrollsheet/src/toast/shell/shell-selectors.ts
function splitPosition(position) {
const [y, x] = position.split("-");
return {
y,
x
};
}
function computePossiblePositions(defaultPosition, toasts) {
const seen = new Set([defaultPosition]);
for (const t of toasts) if (t.position) seen.add(t.position);
return [...seen];
}
function computeRowOffsets(newestFirst, heights, gap) {
let cumulativeHeight = 0;
return newestFirst.map((record, index) => {
const stackOffset = index * gap + cumulativeHeight;
cumulativeHeight += heights.get(record.id) ?? 0;
return {
id: record.id,
index,
toastsBefore: index,
stackOffset
};
});
}
function findNewestDismissible(toasts) {
for (let i = toasts.length - 1; i >= 0; i -= 1) {
const t = toasts[i];
if (t && t.dismissible !== false) return t;
}
}
const SWIPE_VELOCITY_THRESHOLD = .11;
function getDefaultSwipeDirections(position) {
const { y, x } = splitPosition(position);
const directions = [y];
if (x === "left" || x === "right") directions.push(x);
return directions;
}
function lockSwipeAxis(dx, dy) {
if (Math.abs(dx) <= 1 && Math.abs(dy) <= 1) return null;
return Math.abs(dx) > Math.abs(dy) ? "x" : "y";
}
function dampenSwipeDelta(delta) {
const dampened = delta * (1 / (1.5 + Math.abs(delta) / 20));
return Math.abs(dampened) < Math.abs(delta) ? dampened : delta;
}
function computeSwipeAxisAmount(axis, delta, directions) {
const negativeDir = axis === "x" ? "left" : "top";
const positiveDir = axis === "x" ? "right" : "bottom";
if (!(directions.includes(negativeDir) || directions.includes(positiveDir))) return 0;
return directions.includes(negativeDir) && delta < 0 || directions.includes(positiveDir) && delta > 0 ? delta : dampenSwipeDelta(delta);
}
function isSwipeReleaseAllowed(axis, amount, directions) {
if (axis === "x") return directions.includes(amount > 0 ? "right" : "left");
return directions.includes(amount > 0 ? "bottom" : "top");
}
function shouldDismissOnSwipeRelease(amount, velocity, thresholdPx = 45, velocityThreshold = SWIPE_VELOCITY_THRESHOLD) {
return Math.abs(amount) >= thresholdPx || velocity > velocityThreshold;
}
function resolveSwipeOutDirection(axis, amount) {
if (axis === "x") return amount > 0 ? "right" : "left";
return amount > 0 ? "down" : "up";
}
//#endregion
//#region packages/scrollsheet/src/toast/shell/use-toast-swipe.ts
const ZERO_AMOUNT = {
x: 0,
y: 0
};
function useToastSwipe(elRef, enabled, directions, onSwipeDismiss) {
const draggingRef = React.useRef(false);
const axisRef = React.useRef(null);
const startRef = React.useRef(ZERO_AMOUNT);
const amountRef = React.useRef(ZERO_AMOUNT);
const dragStartRef = React.useRef(0);
const onSwipeDismissRef = React.useRef(onSwipeDismiss);
onSwipeDismissRef.current = onSwipeDismiss;
const onPointerDown = React.useCallback((event) => {
if (!enabled || event.button !== 0) return;
if (event.target.tagName === "BUTTON") return;
const el = elRef.current;
if (!el) return;
draggingRef.current = true;
axisRef.current = null;
amountRef.current = ZERO_AMOUNT;
startRef.current = {
x: event.clientX,
y: event.clientY
};
dragStartRef.current = Date.now();
el.setPointerCapture(event.pointerId);
el.setAttribute("data-swiping", "true");
}, [enabled, elRef]);
const onPointerMove = React.useCallback((event) => {
if (!draggingRef.current) return;
const el = elRef.current;
if (!el) return;
if ((window.getSelection?.()?.toString().length ?? 0) > 0) return;
const start = startRef.current;
const dx = event.clientX - start.x;
const dy = event.clientY - start.y;
if (axisRef.current === null) axisRef.current = lockSwipeAxis(dx, dy);
const axis = axisRef.current;
if (axis === null) return;
const resolved = computeSwipeAxisAmount(axis, axis === "x" ? dx : dy, directions);
const amount = axis === "x" ? {
x: resolved,
y: 0
} : {
x: 0,
y: resolved
};
amountRef.current = amount;
if (resolved !== 0) el.setAttribute("data-swiped", "true");
el.style.setProperty("--scrollsheet-toast-swipe-x", `${amount.x}px`);
el.style.setProperty("--scrollsheet-toast-swipe-y", `${amount.y}px`);
}, [elRef, directions]);
const release = React.useCallback(() => {
if (!draggingRef.current) return;
draggingRef.current = false;
const el = elRef.current;
const axis = axisRef.current;
axisRef.current = null;
if (!el) return;
if (axis === null) {
el.setAttribute("data-swiping", "false");
return;
}
const amount = axis === "x" ? amountRef.current.x : amountRef.current.y;
const elapsed = Math.max(1, Date.now() - dragStartRef.current);
const velocity = Math.abs(amount) / elapsed;
if (isSwipeReleaseAllowed(axis, amount, directions) && shouldDismissOnSwipeRelease(amount, velocity)) {
const direction = resolveSwipeOutDirection(axis, amount);
el.setAttribute("data-swipe-direction", direction);
if (prefersReducedMotion()) {
el.setAttribute("data-swipe-out", "true");
onSwipeDismissRef.current();
return;
}
const onAnimEnd = () => {
el.removeEventListener("animationend", onAnimEnd);
onSwipeDismissRef.current();
};
el.addEventListener("animationend", onAnimEnd);
el.setAttribute("data-swipe-out", "true");
return;
}
el.setAttribute("data-swiping", "false");
el.setAttribute("data-swiped", "false");
el.style.setProperty("--scrollsheet-toast-swipe-x", "0px");
el.style.setProperty("--scrollsheet-toast-swipe-y", "0px");
}, [elRef, directions]);
return {
onPointerDown,
onPointerMove,
onPointerUp: React.useCallback((event) => {
if (elRef.current?.hasPointerCapture(event.pointerId)) elRef.current.releasePointerCapture(event.pointerId);
release();
}, [elRef, release]),
onPointerCancel: React.useCallback(() => release(), [release])
};
}
//#endregion
//#region packages/scrollsheet/src/toast/shell/toast-row.tsx
function ToastIcon({ type, icons, spinnerClassName }) {
if (type === "loading") {
if (icons?.loading) return jsx(Fragment, { children: icons.loading });
return jsx("span", { className: spinnerClassName });
}
switch (type) {
case "success": return jsx(Fragment, { children: icons?.success ?? "✓" });
case "error": return jsx(Fragment, { children: icons?.error ?? "✕" });
case "warning": return jsx(Fragment, { children: icons?.warning ?? "!" });
case "info": return jsx(Fragment, { children: icons?.info ?? "i" });
default: return null;
}
}
function ToastRow({ record, index, total, toastsBefore, stackOffset, height, frontHeight, visible, expanded, removed, yPosition, xPosition, closeButton, directions, onDismiss, observe, toasterClassNames, icons, toasterCloseButtonAriaLabel, toasterStyle }) {
const role = record.type === "error" ? "alert" : "status";
const mounted = useMountedFlag();
const elRef = React.useRef(null);
const dismissible = record.dismissible !== false;
const isFront = index === 0;
const setRefs = React.useCallback((el) => {
elRef.current = el;
observe(record.id, el);
}, [observe, record.id]);
const swipeHandlers = useToastSwipe(elRef, !removed && dismissible && record.type !== "loading", directions, () => onDismiss(record));
React.useEffect(() => {
const el = elRef.current;
if (el) el.inert = removed;
}, [removed]);
const style = {
"--scrollsheet-toast-toasts-before": toastsBefore,
"--scrollsheet-toast-stack-offset": `${stackOffset}px`,
"--scrollsheet-toast-front-height": frontHeight !== void 0 ? `${frontHeight}px` : "0px",
"--scrollsheet-toast-initial-height": height !== void 0 ? `${height}px` : "auto",
zIndex: Math.max(0, total - index),
...toasterStyle,
...record.style
};
const rootClassName = cn("scrollsheet-toast", "sonner-toast", record.className, toasterClassNames?.toast, record.classNames?.toast, toasterClassNames?.default, toasterClassNames?.[record.type], record.classNames?.[record.type]);
const motionAttrs = {
"data-mounted": mounted ? "" : void 0,
"data-removed": removed ? "" : void 0,
"data-visible": visible ? "true" : "false",
"data-front": isFront ? "true" : "false",
"data-expanded": expanded ? "true" : "false",
"data-y-position": yPosition,
"data-x-position": xPosition,
"data-index": index,
"data-dismissible": dismissible ? "true" : "false",
"data-swiping": "false",
"data-swiped": "false",
"aria-hidden": removed ? "true" : void 0
};
if (record.jsx !== void 0) return jsx("div", {
ref: setRefs,
className: rootClassName,
"data-scrollsheet-toast": "",
"data-sonner-toast": "",
"data-scrollsheet-custom": "",
"data-sonner-custom": "",
"data-testid": record.testId,
role,
style,
...motionAttrs,
...swipeHandlers,
children: record.jsx
});
const showCloseButton = closeButton && dismissible && record.type !== "loading";
const hasActions = Boolean(record.action || record.cancel || showCloseButton);
const closeButtonAriaLabel = record.closeButtonAriaLabel ?? toasterCloseButtonAriaLabel ?? "Close toast";
return jsxs("div", {
ref: setRefs,
className: rootClassName,
"data-scrollsheet-toast": "",
"data-sonner-toast": "",
"data-type": record.type,
"data-testid": record.testId,
role,
style,
...motionAttrs,
...swipeHandlers,
children: [
jsx("span", {
className: cn("scrollsheet-toast-icon", "sonner-toast-icon", toasterClassNames?.icon, record.classNames?.icon),
"data-type": record.type,
"aria-hidden": "true",
children: record.icon ?? jsx(ToastIcon, {
type: record.type,
icons,
spinnerClassName: cn("scrollsheet-toast-spinner", "sonner-toast-spinner", toasterClassNames?.loader, record.classNames?.loader)
})
}),
jsxs("div", {
className: cn("scrollsheet-toast-body", "sonner-toast-body", toasterClassNames?.content, record.classNames?.content),
children: [record.title !== void 0 && jsx("div", {
className: cn("scrollsheet-toast-title", "sonner-toast-title", toasterClassNames?.title, record.classNames?.title),
children: record.title
}), record.description !== void 0 && jsx("div", {
className: cn("scrollsheet-toast-description", "sonner-toast-description", toasterClassNames?.description, record.classNames?.description),
children: record.description
})]
}),
hasActions && jsxs("div", {
className: "scrollsheet-toast-actions sonner-toast-actions",
children: [
record.cancel && jsx("button", {
type: "button",
className: cn("scrollsheet-toast-cancel", "sonner-toast-cancel", toasterClassNames?.cancelButton, record.classNames?.cancelButton),
onClick: (event) => {
if (!dismissible) return;
record.cancel?.onClick(event);
onDismiss(record);
},
children: record.cancel.label
}),
record.action && jsx("button", {
type: "button",
className: cn("scrollsheet-toast-action", "sonner-toast-action", toasterClassNames?.actionButton, record.classNames?.actionButton),
onClick: (event) => {
record.action?.onClick(event);
if (event.defaultPrevented) return;
onDismiss(record);
},
children: record.action.label
}),
showCloseButton && jsx("button", {
type: "button",
className: cn("scrollsheet-toast-close", "sonner-toast-close", toasterClassNames?.closeButton, record.classNames?.closeButton),
"aria-label": closeButtonAriaLabel,
onClick: () => onDismiss(record),
children: icons?.close ?? "×"
})
]
})
]
});
}
//#endregion
//#region packages/scrollsheet/src/toast/shell/use-toast-heights.ts
function useToastHeights() {
const [heights, setHeights] = React.useState(() => new Map());
const observersRef = React.useRef(new Map());
const setHeight = React.useCallback((id, height) => {
setHeights((prev) => prev.get(id) === height ? prev : new Map(prev).set(id, height));
}, []);
const forget = React.useCallback((id) => {
observersRef.current.get(id)?.disconnect();
observersRef.current.delete(id);
setHeights((prev) => {
if (!prev.has(id)) return prev;
const next = new Map(prev);
next.delete(id);
return next;
});
}, []);
const observe = React.useCallback((id, el) => {
const observers = observersRef.current;
observers.get(id)?.disconnect();
observers.delete(id);
if (!el || typeof ResizeObserver === "undefined") return;
const ro = new ResizeObserver(() => setHeight(id, el.getBoundingClientRect().height));
ro.observe(el);
observers.set(id, ro);
setHeight(id, el.getBoundingClientRect().height);
}, [setHeight]);
React.useEffect(() => {
const observers = observersRef.current;
return () => {
for (const ro of observers.values()) ro.disconnect();
observers.clear();
};
}, []);
return {
heights,
observe,
forget
};
}
//#endregion
//#region packages/scrollsheet/src/toast/shell/toaster-shell.tsx
const DEFAULT_HOTKEY = ["altKey", "KeyT"];
function PositionGroup({ position, defaultPosition, toasts, visibleToasts, gap, expanded, closeButton, swipeDirections, toastOptions, icons, className, baseStyle, registerListEl, dismissRow, onHoverEnter, onHoverLeave, onInteractingChange }) {
const directions = React.useMemo(() => swipeDirections ?? getDefaultSwipeDirections(position), [swipeDirections, position]);
const positionToasts = React.useMemo(() => selectPositionToasts(toasts, position, defaultPosition), [
toasts,
position,
defaultPosition
]);
const newestFirst = React.useMemo(() => [...positionToasts].reverse(), [positionToasts]);
const window_ = React.useMemo(() => selectToastWindow(positionToasts, visibleToasts), [positionToasts, visibleToasts]);
const visibleIds = React.useMemo(() => new Set(window_.visible.map((t) => t.id)), [window_]);
const { heights, observe, forget } = useToastHeights();
const exitRows = useToastExit(newestFirst);
const exitingIds = React.useMemo(() => new Set(exitRows.filter((r) => r.exiting).map((r) => r.record.id)), [exitRows]);
React.useEffect(() => {
for (const id of exitingIds) forget(id);
}, [exitingIds, forget]);
const offsets = React.useMemo(() => computeRowOffsets(newestFirst, heights, gap), [
newestFirst,
heights,
gap
]);
const offsetById = React.useMemo(() => new Map(offsets.map((o) => [o.id, o])), [offsets]);
const lastOffsetRef = React.useRef(new Map());
for (const o of offsets) lastOffsetRef.current.set(o.id, o);
if (lastOffsetRef.current.size > offsets.length) {
const keep = new Set([...offsets.map((o) => o.id), ...exitingIds]);
for (const id of [...lastOffsetRef.current.keys()]) if (!keep.has(id)) lastOffsetRef.current.delete(id);
}
const frontHeight = heights.get(newestFirst[0]?.id ?? "");
const { y, x } = splitPosition(position);
const listRef = React.useCallback((el) => registerListEl(position, el), [registerListEl, position]);
if (exitRows.length === 0) return null;
return jsx("ol", {
ref: listRef,
"data-scrollsheet-toaster": "",
"data-sonner-toaster": "",
"data-scrollsheet-theme": "light",
"data-sonner-theme": "light",
"data-y-position": y,
"data-x-position": x,
tabIndex: -1,
className,
style: {
...baseStyle,
"--scrollsheet-toast-gap": `${gap}px`,
"--scrollsheet-toast-front-height": frontHeight !== void 0 ? `${frontHeight}px` : void 0
},
onMouseEnter: onHoverEnter,
onMouseMove: onHoverEnter,
onMouseLeave: onHoverLeave,
onPointerDown: (event) => {
if (event.target.dataset.dismissible === "false") return;
onInteractingChange(true);
},
onPointerUp: () => onInteractingChange(false),
children: exitRows.map(({ record, exiting }) => {
const offset = offsetById.get(record.id) ?? lastOffsetRef.current.get(record.id);
const index = offset?.index ?? newestFirst.length;
return jsx(ToastRow, {
record,
index,
total: newestFirst.length,
toastsBefore: offset?.toastsBefore ?? index,
stackOffset: offset?.stackOffset ?? 0,
height: heights.get(record.id),
frontHeight,
visible: visibleIds.has(record.id),
expanded,
removed: exiting,
yPosition: y,
xPosition: x,
closeButton,
directions,
onDismiss: dismissRow,
observe,
toasterClassNames: toastOptions?.classNames,
icons,
toasterCloseButtonAriaLabel: toastOptions?.closeButtonAriaLabel,
toasterStyle: toastOptions?.style
}, record.id);
})
});
}
function resolveShellPosition(position) {
return position ?? "bottom-right";
}
function ToasterShell({ id, toasterId, position, theme, richColors, expand: forceExpand, visibleToasts: visibleToastsProp, closeButton = false, duration: durationProp, gap = 14, offset, swipeDirections, toastOptions, icons, className, style, containerAriaLabel = "Notifications", hotkey = DEFAULT_HOTKEY, nonce }) {
const resolvedId = id ?? toasterId;
if (toasterId !== void 0) warnOnce("toaster-id-deprecated", "[scrollsheet Toaster] Toaster's \"toasterId\" prop is deprecated — use \"id\" instead.");
const { toasts: allToasts } = useSonner();
const toasts = React.useMemo(() => selectToasterToasts(allToasts, resolvedId), [allToasts, resolvedId]);
const defaultPosition = resolveShellPosition(position);
const possiblePositions = React.useMemo(() => computePossiblePositions(defaultPosition, toasts), [defaultPosition, toasts]);
const visibleToasts = resolveVisibleToasts(visibleToastsProp);
const defaultDuration = durationProp ?? toastOptions?.duration ?? 4e3;
if (theme !== void 0 && theme !== "light") warnOnce("shell-theme", `[scrollsheet Toaster] theme="${theme}" isn't implemented yet — v1 always renders the static light card real Sonner ships by default.`);
if (richColors) warnOnce("shell-rich-colors", "[scrollsheet Toaster] richColors isn't implemented yet — toasts render with their default icon color only.");
React.useEffect(() => {
injectToastStyles(nonce);
}, [nonce]);
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => setMounted(true), []);
const [hovered, setHovered] = React.useState(false);
const [hotkeyExpanded, setHotkeyExpanded] = React.useState(false);
const [interacting, setInteracting] = React.useState(false);
const expanded = Boolean(forceExpand) || hovered || hotkeyExpanded;
const isDocumentHidden = useIsDocumentHidden();
const handleHoverEnter = React.useCallback(() => setHovered(true), []);
const handleHoverLeave = React.useCallback(() => {
if (!interacting) setHovered(false);
}, [interacting]);
const dismissRow = React.useCallback((record) => {
toast.dismiss(record.id);
}, []);
const listElsRef = React.useRef(new Map());
const registerListEl = React.useCallback((pos, el) => {
if (el) listElsRef.current.set(pos, el);
else listElsRef.current.delete(pos);
}, []);
React.useEffect(() => {
if (hotkey.length === 0) return;
const handleKeyDown = (event) => {
if (hotkey.every((key) => event[key] || event.code === key)) {
setHotkeyExpanded(true);
const [firstList] = listElsRef.current.values();
firstList?.focus({ preventScroll: true });
}
if (event.code !== "Escape") return;
const active = document.activeElement;
const focusedEntry = [...listElsRef.current.entries()].find(([, el]) => active === el || el.contains(active));
if (!focusedEntry) return;
if (hotkeyExpanded) {
setHotkeyExpanded(false);
return;
}
const [focusedPosition] = focusedEntry;
const positionToasts = selectPositionToasts(toasts, focusedPosition, defaultPosition);
const front = positionToasts[positionToasts.length - 1];
if (front) dismissRow(front);
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [
hotkey,
hotkeyExpanded,
toasts,
defaultPosition,
dismissRow
]);
const timersRef = React.useRef(new Map());
React.useEffect(() => {
const timers = timersRef.current;
const liveIds = new Set(toasts.map((t) => t.id));
for (const [id, entry] of timers) {
if (liveIds.has(id)) continue;
if (entry.timer) clearTimeout(entry.timer);
timers.delete(id);
}
const paused = expanded || interacting || isDocumentHidden;
for (const t of toasts) {
if (t.type === "loading") {
const stale = timers.get(t.id);
if (stale) {
if (stale.timer) clearTimeout(stale.timer);
timers.delete(t.id);
}
continue;
}
const ms = t.duration ?? toastOptions?.duration ?? defaultDuration;
const existing = timers.get(t.id);
if (!existing || existing.record !== t) {
if (existing?.timer) clearTimeout(existing.timer);
const entry = {
timer: null,
startedAt: 0,
remaining: ms,
record: t
};
timers.set(t.id, entry);
if (Number.isFinite(ms) && !paused) {
entry.startedAt = Date.now();
entry.timer = setTimeout(() => {
timersRef.current.delete(t.id);
expire(t.id);
}, ms);
}
continue;
}
if (paused) {
if (existing.timer) {
clearTimeout(existing.timer);
const elapsed = Date.now() - existing.startedAt;
existing.remaining = Math.max(0, existing.remaining - elapsed);
existing.timer = null;
}
continue;
}
if (existing.timer || !Number.isFinite(existing.remaining)) continue;
existing.startedAt = Date.now();
existing.timer = setTimeout(() => {
timersRef.current.delete(t.id);
expire(t.id);
}, existing.remaining);
}
}, [
toasts,
expanded,
interacting,
isDocumentHidden,
defaultDuration,
toastOptions?.duration
]);
React.useEffect(() => {
const timers = timersRef.current;
return () => {
for (const entry of timers.values()) if (entry.timer) clearTimeout(entry.timer);
timers.clear();
};
}, []);
useCloseWatcher({
present: toasts.length > 0,
nonModal: true,
escapeDismissible: true,
onClose: () => {
const target = findNewestDismissible(toasts);
if (target) dismissRow(target);
}
});
if (!mounted || typeof document === "undefined") return null;
const offsetValue = offset !== void 0 ? typeof offset === "number" ? `${offset}px` : offset : void 0;
const baseStyle = {
...style,
"--scrollsheet-toast-offset": offsetValue
};
return createPortal(jsx("section", {
"aria-label": containerAriaLabel,
tabIndex: -1,
"aria-live": "polite",
"aria-relevant": "additions text",
"aria-atomic": "false",
suppressHydrationWarning: true,
"data-react-aria-top-layer": "",
children: possiblePositions.map((groupPosition) => jsx(PositionGroup, {
position: groupPosition,
defaultPosition,
toasts,
visibleToasts,
gap,
expanded,
closeButton,
swipeDirections,
toastOptions,
icons,
className,
baseStyle,
registerListEl,
dismissRow,
onHoverEnter: handleHoverEnter,
onHoverLeave: handleHoverLeave,
onInteractingChange: setInteracting
}, groupPosition))
}), document.body);
}
//#endregion
export { useSonner as a, toast as i, resolveVisibleToasts as n, useToasts as o, injectToastStylesInto as r, ToasterShell as t };
+1
-1

@@ -1,2 +0,2 @@

import { D as SheetTitleProps, E as SheetDescriptionProps, M as SheetTriggerProps, N as Trigger, O as Title, T as SheetCloseProps, _ as composeOpenChange, a as DrawerHandleProps, b as resolveFadeFromIndex, c as DrawerPortalProps, d as NestedRoot, f as Overlay, g as composeHandleClick, h as VaulSnapPoint, i as DrawerContentProps, l as DrawerRootProps, m as Root, n as Drawer, o as DrawerNestedRootProps, p as Portal, r as DrawerClose, s as DrawerOverlayProps, t as Content, u as Handle, v as hasMultipleSnapPoints, w as Description, y as resolveCloseThreshold } from "./index-CBawoyr4.mjs";
import { D as SheetTitleProps, E as SheetDescriptionProps, M as Trigger, O as Title, T as SheetCloseProps, _ as composeOpenChange, a as DrawerHandleProps, b as resolveFadeFromIndex, c as DrawerPortalProps, d as NestedRoot, f as Overlay, g as composeHandleClick, h as VaulSnapPoint, i as DrawerContentProps, j as SheetTriggerProps, l as DrawerRootProps, m as Root, n as Drawer, o as DrawerNestedRootProps, p as Portal, r as DrawerClose, s as DrawerOverlayProps, t as Content, u as Handle, v as hasMultipleSnapPoints, w as Description, y as resolveCloseThreshold } from "./index-D_yFDuZL.mjs";
export { DrawerClose as Close, Content, Description, Drawer, type SheetCloseProps as DrawerCloseProps, DrawerContentProps, type SheetDescriptionProps as DrawerDescriptionProps, DrawerHandleProps, DrawerNestedRootProps, DrawerOverlayProps, DrawerPortalProps, DrawerRootProps, type SheetTitleProps as DrawerTitleProps, type SheetTriggerProps as DrawerTriggerProps, Handle, NestedRoot, Overlay, Portal, Root, Title, Trigger, VaulSnapPoint, composeHandleClick, composeOpenChange, hasMultipleSnapPoints, resolveCloseThreshold, resolveFadeFromIndex };
"use client";
"use client";
import { a as NestedRoot, c as Root, d as hasMultipleSnapPoints, f as resolveCloseThreshold, g as Title, h as Description, i as Handle, l as composeHandleClick, n as Drawer, o as Overlay, p as resolveFadeFromIndex, r as DrawerClose, s as Portal, t as Content, u as composeOpenChange, x as Trigger } from "./drawer-HVSMFDtk.mjs";
import { a as NestedRoot, c as Root, d as hasMultipleSnapPoints, f as resolveCloseThreshold, g as Title, h as Description, i as Handle, l as composeHandleClick, n as Drawer, o as Overlay, p as resolveFadeFromIndex, r as DrawerClose, s as Portal, t as Content, u as composeOpenChange, x as Trigger } from "./drawer-CY9M15SC.mjs";
export { DrawerClose as Close, Content, Description, Drawer, Handle, NestedRoot, Overlay, Portal, Root, Title, Trigger, composeHandleClick, composeOpenChange, hasMultipleSnapPoints, resolveCloseThreshold, resolveFadeFromIndex };

@@ -1,230 +0,3 @@

import { A as SheetContentProps, C as Close, D as SheetTitleProps, E as SheetDescriptionProps, M as SheetTriggerProps, N as Trigger, O as Title, S as SheetHandleProps, T as SheetCloseProps, a as DrawerHandleProps, c as DrawerPortalProps, h as VaulSnapPoint, i as DrawerContentProps, j as DetentSpec, k as Content, l as DrawerRootProps, n as Drawer, o as DrawerNestedRootProps, s as DrawerOverlayProps, w as Description, x as Handle } from "./index-CBawoyr4.mjs";
import { a as SonnerPosition, c as ToastData, d as ToastPromiseData, f as ToastRecord, g as useToasts, h as useSonner, l as ToastIcons, m as toast, n as ToasterShell, o as ToastAction, p as ToastType, r as ToasterProps, s as ToastClassnames, t as injectToastStylesInto, u as ToastPosition } from "./index-DhRyhV1b.mjs";
import * as React from "react";
//#region packages/scrollsheet/src/motion/geometry.d.ts
/**
* Per-side axis geometry.
*
* The whole engine (content.tsx) is written once against an abstract
* "revealed px" concept: 0 = fully hidden, `maxDetent` = fully revealed. The
* DOM's scroll position is the mechanism, but which raw value corresponds to
* "revealed" depends on which canvas edge the panel is pinned to:
* bottom/right sit at the canvas's *far* end, so raw scroll IS revealed px
* directly; top/left sit at the *near* end (flush with the screen when fully
* open), so raw scroll is mirrored: revealed = maxDetent - rawScroll.
* `mapScroll` is the self-inverse conversion between the two spaces.
*/
type Side = "bottom" | "top" | "left" | "right";
//#endregion
//#region packages/scrollsheet/src/context.d.ts
/**
* onTravel's third argument. Reused and mutated in place every travel frame
* (see content.tsx's updateTravel) — read it synchronously, don't retain the
* reference across frames.
*/
interface TravelInfo {
/** [0, maxDetent] — the full resolved travel range in px, this frame. */
range: readonly [number, number];
/**
* 0-1 progress toward EACH configured detent, keyed by the detent's
* resolved height in px (not its spec — a spec like 'content' isn't a
* stable identity across resizes, but its resolved height this frame is
* what a consumer interpolates against).
*/
progressAtDetents: ReadonlyMap<number, number>;
}
//#endregion
//#region packages/scrollsheet/src/root.d.ts
/** Imperative escape hatch — see `actionsRef` on `<Sheet.Root>`. */
interface SheetActions {
open(): void;
close(): void;
/**
* Moves the sheet to `detent` through the same `setActiveDetent` path a
* controlled `activeDetent` change uses, so it animates identically. Warns
* in dev (once) if `detent` isn't one of this sheet's configured
* `detents` — only the panel's *rest position* resolves to the nearest
* configured detent in that case; `activeDetent`/`onActiveDetentChange`
* (and Handle's `aria-valuenow`/`aria-valuetext`) still reflect the
* literal value passed in here, not the resolved one.
*/
snapTo(detent: DetentSpec): void;
}
interface SheetRootProps {
children?: React.ReactNode;
/** Controlled open state. */
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
/**
* Fires once the open/close *transition* finishes (not just the state
* flip) — `true` when the sheet has fully settled open, `false` once it's
* fully closed and unmounted. Backed by the same phase timers that drive
* `data-scrollsheet-state`/`data-scrollsheet-side` (see Content) — no
* separate timer of its own.
*/
onOpenChangeComplete?: (open: boolean) => void;
/**
* Imperative escape hatch for cases a controlled `open`/`activeDetent`
* prop is awkward for (deep links, push notifications, a descendant
* component reaching up without prop-drilling): `ref.current?.close()` /
* `.open()` / `.snapTo(detent)`. Thin wrappers over the same `setOpen`/
* `setActiveDetent` the rest of the sheet uses — not a separate code path.
*/
actionsRef?: React.Ref<SheetActions>;
/**
* The stops the sheet can rest at, UISheetPresentationController-style.
* `'content'` (natural height), `'medium'` (50%), `'full'`, a 0–1 fraction,
* or `'320px'`.
* @default ['content']
*/
detents?: readonly DetentSpec[];
/** Controlled active detent (one of `detents`). */
activeDetent?: DetentSpec;
onActiveDetentChange?: (detent: DetentSpec) => void;
/**
* Master switch: when false, swipe/drag-to-dismiss, backdrop tap and Esc
* all stop closing the sheet. `escapeDismissible`/`backdropDismissible`
* below let you turn off just one of the pointer/keyboard paths while
* leaving the others (and swipe-to-dismiss, which only this prop gates)
* alone — both default to whatever `dismissible` is.
* @default true
*/
dismissible?: boolean;
/** Whether Esc closes the sheet. @default `dismissible` */
escapeDismissible?: boolean;
/** Whether tapping the backdrop/empty track closes the sheet. @default `dismissible` */
backdropDismissible?: boolean;
/** CSP nonce for the injected style tag. */
nonce?: string;
/**
* Blend the page's `<meta name="theme-color">` toward black as the sheet
* travels, so the browser chrome (iOS Safari tints both the status bar area
* and the bottom toolbar from this tag; Android tints the status bar) dims
* along with the backdrop. Every theme-color tag is dimmed from its own
* base color, so the usual media-scoped light/dark pair works and stays
* correct even if the OS scheme flips while the sheet is open. On a
* bottom-anchored attached sheet, the strip behind the bottom toolbar takes
* the panel's own color instead (via a fixed sentinel element), so the bar
* reads as part of the sheet, not the dimmed page. No-ops if no
* tag holds a parseable color, or on platforms that ignore theme-color.
* @default false
*/
themeColorDimming?: boolean;
/**
* Fires on every frame the sheet travels: revealed px, 0-1 progress
* (against the first detent, same number `--scrollsheet-progress` uses),
* and `info` — per-detent progress plus the resolved travel range. `info`
* is reused and mutated in place every frame (this is the highest-
* frequency callback in the library); read it synchronously, don't store
* the reference. Keep this handler cheap — it runs on every travel frame.
*/
onTravel?: (revealedPx: number, progress: number, info: TravelInfo) => void;
/** Which edge the sheet is anchored to. @default 'bottom' */
side?: Side;
/**
* When false, the page behind stays fully interactive and scrollable — no
* backdrop, no focus trap, no inert-ing the rest of the page. Renders on a
* `<div popover="manual">` (top layer, non-blocking) where supported,
* falling back to a non-modal `<dialog>` (`.show()`) otherwise.
* @default true
*/
modal?: boolean;
/**
* Applies an iOS-style card effect to the page element marked
* `data-scrollsheet-background`, driven by this sheet's own travel
* progress. `'scale'` scales/insets/rounds it; `'parallax'` only shifts it.
* No-ops if no element is marked.
*
* Left unset, a full-height bottom sheet in the mobile presentation gets
* `'scale'` — that is how the platform presents a sheet that covers the
* screen. Desktop drawers dock beside the page and never move it, and a
* detached (floating-card) sheet never claims the full screen, so neither
* defaults on. `'none'` opts out anywhere.
*/
backgroundEffect?: "scale" | "parallax" | "none";
/**
* Explicit target for backgroundEffect, instead of the default document-wide
* `[data-scrollsheet-background]` query. Pass the same ref you'd otherwise
* mark with the attribute. Skips the ownership check the implicit default
* applies (an explicit ref is unambiguous about which page element it
* means) — matches how an explicit `backgroundEffect` prop today already
* bypasses the implicit default's full-height/opener-containment check.
*/
backgroundRef?: React.RefObject<HTMLElement | null>;
/**
* Inner-content-overflow scrollbar style, for the panel's own scroll at
* max detent (or any content taller than the panel). `'overlay'`: a thin
* auto-hiding thumb (iOS/macOS-style). `'hidden'`: no thumb, no native
* scrollbar either. `'native'`: let the browser draw
* its own.
* @default 'overlay'
*/
scrollbar?: "overlay" | "hidden" | "native";
/**
* Scoped to exactly two things: the backdrop's opacity and
* `themeColorDimming`. Both stay fully transparent/undimmed while the
* sheet is at or below this detent's resolved height, then interpolate
* from there up to the next detent above it (or the tallest detent, if
* this is already the tallest). Unset (default): dims across the full
* range from closed to the first detent, today's behavior. Does *not*
* affect `onTravel`'s progress argument, `--scrollsheet-progress`,
* stacking recede, or `backgroundEffect` — those all track the sheet's
* full-range travel 1:1 regardless of this prop.
*/
largestUndimmedDetent?: DetentSpec;
/**
* Drag sessions (mouse, and touch on non-modal sheets) only start from
* `<Sheet.Handle>` — a pointerdown elsewhere on the panel no-ops. Modal
* touch (native scroll drives the gesture) is restricted via
* `touch-action` instead, except at the tallest detent, where the panel's
* own content scroll still needs it.
* @default false
*/
handleOnly?: boolean;
/**
* No drag sessions at all, handle included — detent changes are
* programmatic (`activeDetent`, `actionsRef`) or click/keyboard
* (`<Sheet.Handle>`) only. Esc and backdrop dismissal are unaffected.
* @default false
*/
disableDrag?: boolean;
/**
* A release never skips over an intermediate detent — the resolved target
* is clamped to the immediate neighbor of the detent the gesture started
* from (or, for a native-scroll settle, the last settled detent).
* @default false
*/
sequentialDetents?: boolean;
/**
* Fraction (0-1) of the first detent below which a release dismisses the
* sheet, replacing the built-in half-of-first-detent rule.
* @default 0.5
*/
closeThreshold?: number;
/**
* Bottom sheets only. When the software keyboard opens with a text field
* focused inside the sheet, promote to the tallest detent so the field
* has the whole remaining viewport to scroll within — a short peek detent
* can't show a field the keyboard would otherwise cover. Restores the
* previous detent on blur unless something else (a drag, a controlled
* change) moved the sheet in between. Gated on the keyboard actually
* appearing (a nonzero measured inset), never on focus alone, so a
* desktop click into an input doesn't expand anything. Promotion goes
* through the same `setActiveDetent` path a controlled change uses — a
* controlled sheet must apply `onActiveDetentChange` back to
* `activeDetent` or this prop has no visible effect.
* @default false
*/
keyboardExpands?: boolean;
/**
* Fires once a drag session resolves, with the real pointer event (the
* `buttons===0` recovery path may pass the triggering `pointermove`
* instead of a `pointerup`) and whether the sheet is staying open —
* `false` only when the release resolved to a dismiss.
*/
onRelease?: (event: PointerEvent, willRemainOpen: boolean) => void;
}
declare function Root({ children, open: openProp, defaultOpen, onOpenChange, onOpenChangeComplete, actionsRef, detents, activeDetent: activeDetentProp, onActiveDetentChange, dismissible, escapeDismissible, backdropDismissible, nonce, themeColorDimming, onTravel, side, modal, backgroundEffect, backgroundRef, scrollbar, largestUndimmedDetent, handleOnly, disableDrag, sequentialDetents, closeThreshold, keyboardExpands, onRelease }: SheetRootProps): React.JSX.Element;
//#endregion
import { A as SheetContentProps, C as Close, D as SheetTitleProps, E as SheetDescriptionProps, F as SheetRootProps, I as TravelInfo, L as Side, M as Trigger, N as Root, O as Title, P as SheetActions, R as DetentSpec, S as SheetHandleProps, T as SheetCloseProps, a as DrawerHandleProps, c as DrawerPortalProps, h as VaulSnapPoint, i as DrawerContentProps, j as SheetTriggerProps, k as Content, l as DrawerRootProps, n as Drawer, o as DrawerNestedRootProps, s as DrawerOverlayProps, w as Description, x as Handle } from "./index-D_yFDuZL.mjs";
import { a as SonnerPosition, c as ToastData, d as ToastPromiseData, f as ToastRecord, g as useToasts, h as useSonner, l as ToastIcons, m as toast, n as ToasterShell, o as ToastAction, p as ToastType, r as ToasterProps, s as ToastClassnames, t as injectToastStylesInto, u as ToastPosition } from "./index-FslrKtLB.mjs";
//#region packages/scrollsheet/src/motion/spring.d.ts

@@ -231,0 +4,0 @@ /**

"use client";
import { S as Root, _ as Handle, b as spring, g as Title, h as Description, m as Close, n as Drawer, v as Content, x as Trigger, y as injectStylesInto } from "./drawer-HVSMFDtk.mjs";
import { S as Root, _ as Handle, b as spring, g as Title, h as Description, m as Close, n as Drawer, v as Content, x as Trigger, y as injectStylesInto } from "./drawer-CY9M15SC.mjs";
import { o as hasDialogSupport } from "./cn-CActSFFr.mjs";
import { a as useSonner, i as toast, o as useToasts, r as injectToastStylesInto, t as ToasterShell } from "./toast-BByAtKPq.mjs";
import { a as useSonner, i as toast, o as useToasts, r as injectToastStylesInto, t as ToasterShell } from "./toast-DMDz8Njb.mjs";
//#region packages/scrollsheet/src/index.ts

@@ -6,0 +6,0 @@ function isSupported() {

@@ -1,2 +0,2 @@

import { a as SonnerPosition, c as ToastData, d as ToastPromiseData, f as ToastRecord, g as useToasts, h as useSonner, i as resolveVisibleToasts, l as ToastIcons, m as toast, n as ToasterShell, o as ToastAction, p as ToastType, r as ToasterProps, s as ToastClassnames, t as injectToastStylesInto, u as ToastPosition } from "./index-DhRyhV1b.mjs";
import { a as SonnerPosition, c as ToastData, d as ToastPromiseData, f as ToastRecord, g as useToasts, h as useSonner, i as resolveVisibleToasts, l as ToastIcons, m as toast, n as ToasterShell, o as ToastAction, p as ToastType, r as ToasterProps, s as ToastClassnames, t as injectToastStylesInto, u as ToastPosition } from "./index-FslrKtLB.mjs";
export { type SonnerPosition, type ToastAction, type ToastClassnames, type ToastData, type ToastIcons, type ToastPosition, type ToastPromiseData, type ToastRecord, type ToastType, ToasterShell as Toaster, type ToasterProps, injectToastStylesInto, resolveVisibleToasts, toast, useSonner, useToasts };
"use client";
"use client";
import { a as useSonner, i as toast, n as resolveVisibleToasts, o as useToasts, r as injectToastStylesInto, t as ToasterShell } from "./toast-BByAtKPq.mjs";
import { a as useSonner, i as toast, n as resolveVisibleToasts, o as useToasts, r as injectToastStylesInto, t as ToasterShell } from "./toast-DMDz8Njb.mjs";
export { ToasterShell as Toaster, injectToastStylesInto, resolveVisibleToasts, toast, useSonner, useToasts };

@@ -1,2 +0,2 @@

import { D as SheetTitleProps, E as SheetDescriptionProps, M as SheetTriggerProps, N as Trigger, O as Title, T as SheetCloseProps, _ as composeOpenChange, a as DrawerHandleProps, b as resolveFadeFromIndex, c as DrawerPortalProps, d as NestedRoot, f as Overlay, g as composeHandleClick, h as VaulSnapPoint, i as DrawerContentProps, l as DrawerRootProps, m as Root, n as Drawer, o as DrawerNestedRootProps, p as Portal, r as DrawerClose, s as DrawerOverlayProps, t as Content, u as Handle, v as hasMultipleSnapPoints, w as Description, y as resolveCloseThreshold } from "./index-CBawoyr4.mjs";
import { D as SheetTitleProps, E as SheetDescriptionProps, M as Trigger, O as Title, T as SheetCloseProps, _ as composeOpenChange, a as DrawerHandleProps, b as resolveFadeFromIndex, c as DrawerPortalProps, d as NestedRoot, f as Overlay, g as composeHandleClick, h as VaulSnapPoint, i as DrawerContentProps, j as SheetTriggerProps, l as DrawerRootProps, m as Root, n as Drawer, o as DrawerNestedRootProps, p as Portal, r as DrawerClose, s as DrawerOverlayProps, t as Content, u as Handle, v as hasMultipleSnapPoints, w as Description, y as resolveCloseThreshold } from "./index-DdBnpMLy.mjs";
export { DrawerClose as Close, Content, Description, Drawer, type SheetCloseProps as DrawerCloseProps, DrawerContentProps, type SheetDescriptionProps as DrawerDescriptionProps, DrawerHandleProps, DrawerNestedRootProps, DrawerOverlayProps, DrawerPortalProps, DrawerRootProps, type SheetTitleProps as DrawerTitleProps, type SheetTriggerProps as DrawerTriggerProps, Handle, NestedRoot, Overlay, Portal, Root, Title, Trigger, VaulSnapPoint, composeHandleClick, composeOpenChange, hasMultipleSnapPoints, resolveCloseThreshold, resolveFadeFromIndex };
"use client";
"use client";
import { a as NestedRoot, b as Trigger, c as Root, d as hasMultipleSnapPoints, f as resolveCloseThreshold, g as Title, h as Description, i as Handle, l as composeHandleClick, n as Drawer, o as Overlay, p as resolveFadeFromIndex, r as DrawerClose, s as Portal, t as Content, u as composeOpenChange } from "./drawer-BXqx0bTN.mjs";
import { a as NestedRoot, b as Trigger, c as Root, d as hasMultipleSnapPoints, f as resolveCloseThreshold, g as Title, h as Description, i as Handle, l as composeHandleClick, n as Drawer, o as Overlay, p as resolveFadeFromIndex, r as DrawerClose, s as Portal, t as Content, u as composeOpenChange } from "./drawer-OLscKclW.mjs";
export { DrawerClose as Close, Content, Description, Drawer, Handle, NestedRoot, Overlay, Portal, Root, Title, Trigger, composeHandleClick, composeOpenChange, hasMultipleSnapPoints, resolveCloseThreshold, resolveFadeFromIndex };

@@ -1,216 +0,5 @@

import { A as SheetContentProps, C as Close, D as SheetTitleProps, E as SheetDescriptionProps, M as SheetTriggerProps, N as Trigger, O as Title, S as SheetHandleProps, T as SheetCloseProps, a as DrawerHandleProps, c as DrawerPortalProps, h as VaulSnapPoint, i as DrawerContentProps, j as DetentSpec, k as Content, l as DrawerRootProps, n as Drawer, o as DrawerNestedRootProps, s as DrawerOverlayProps, w as Description, x as Handle } from "./index-CBawoyr4.mjs";
import { a as SpringConfig, c as spring, l as Side, o as SpringCurve } from "./index-f5LGCgfI.mjs";
import { a as SonnerPosition, c as ToastData, d as ToastPromiseData, f as ToastRecord, g as useToasts, h as useSonner, l as ToastIcons, m as toast, n as ToasterShell, o as ToastAction, p as ToastType, r as ToasterProps, s as ToastClassnames, t as injectToastStylesInto, u as ToastPosition } from "./index-DhRyhV1b.mjs";
import * as React from "react";
//#region packages/scrollsheet/src/context.d.ts
/**
* onTravel's third argument. Reused and mutated in place every travel frame
* (see content.tsx's updateTravel) — read it synchronously, don't retain the
* reference across frames.
*/
interface TravelInfo {
/** [0, maxDetent] — the full resolved travel range in px, this frame. */
range: readonly [number, number];
/**
* 0-1 progress toward EACH configured detent, keyed by the detent's
* resolved height in px (not its spec — a spec like 'content' isn't a
* stable identity across resizes, but its resolved height this frame is
* what a consumer interpolates against).
*/
progressAtDetents: ReadonlyMap<number, number>;
}
//#endregion
//#region packages/scrollsheet/src/root.d.ts
/** Imperative escape hatch — see `actionsRef` on `<Sheet.Root>`. */
interface SheetActions {
open(): void;
close(): void;
/**
* Moves the sheet to `detent` through the same `setActiveDetent` path a
* controlled `activeDetent` change uses, so it animates identically. Warns
* in dev (once) if `detent` isn't one of this sheet's configured
* `detents` — only the panel's *rest position* resolves to the nearest
* configured detent in that case; `activeDetent`/`onActiveDetentChange`
* (and Handle's `aria-valuenow`/`aria-valuetext`) still reflect the
* literal value passed in here, not the resolved one.
*/
snapTo(detent: DetentSpec): void;
}
interface SheetRootProps {
children?: React.ReactNode;
/** Controlled open state. */
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
/**
* Fires once the open/close *transition* finishes (not just the state
* flip) — `true` when the sheet has fully settled open, `false` once it's
* fully closed and unmounted. Backed by the same phase timers that drive
* `data-scrollsheet-state`/`data-scrollsheet-side` (see Content) — no
* separate timer of its own.
*/
onOpenChangeComplete?: (open: boolean) => void;
/**
* Imperative escape hatch for cases a controlled `open`/`activeDetent`
* prop is awkward for (deep links, push notifications, a descendant
* component reaching up without prop-drilling): `ref.current?.close()` /
* `.open()` / `.snapTo(detent)`. Thin wrappers over the same `setOpen`/
* `setActiveDetent` the rest of the sheet uses — not a separate code path.
*/
actionsRef?: React.Ref<SheetActions>;
/**
* The stops the sheet can rest at, UISheetPresentationController-style.
* `'content'` (natural height), `'medium'` (50%), `'full'`, a 0–1 fraction,
* or `'320px'`.
* @default ['content']
*/
detents?: readonly DetentSpec[];
/** Controlled active detent (one of `detents`). */
activeDetent?: DetentSpec;
onActiveDetentChange?: (detent: DetentSpec) => void;
/**
* Master switch: when false, swipe/drag-to-dismiss, backdrop tap and Esc
* all stop closing the sheet. `escapeDismissible`/`backdropDismissible`
* below let you turn off just one of the pointer/keyboard paths while
* leaving the others (and swipe-to-dismiss, which only this prop gates)
* alone — both default to whatever `dismissible` is.
* @default true
*/
dismissible?: boolean;
/** Whether Esc closes the sheet. @default `dismissible` */
escapeDismissible?: boolean;
/** Whether tapping the backdrop/empty track closes the sheet. @default `dismissible` */
backdropDismissible?: boolean;
/** CSP nonce for the injected style tag. */
nonce?: string;
/**
* Blend the page's `<meta name="theme-color">` toward black as the sheet
* travels, so the browser chrome (iOS Safari tints both the status bar area
* and the bottom toolbar from this tag; Android tints the status bar) dims
* along with the backdrop. Every theme-color tag is dimmed from its own
* base color, so the usual media-scoped light/dark pair works and stays
* correct even if the OS scheme flips while the sheet is open. On a
* bottom-anchored attached sheet, the strip behind the bottom toolbar takes
* the panel's own color instead (via a fixed sentinel element), so the bar
* reads as part of the sheet, not the dimmed page. No-ops if no
* tag holds a parseable color, or on platforms that ignore theme-color.
* @default false
*/
themeColorDimming?: boolean;
/**
* Fires on every frame the sheet travels: revealed px, 0-1 progress
* (against the first detent, same number `--scrollsheet-progress` uses),
* and `info` — per-detent progress plus the resolved travel range. `info`
* is reused and mutated in place every frame (this is the highest-
* frequency callback in the library); read it synchronously, don't store
* the reference. Keep this handler cheap — it runs on every travel frame.
*/
onTravel?: (revealedPx: number, progress: number, info: TravelInfo) => void;
/** Which edge the sheet is anchored to. @default 'bottom' */
side?: Side;
/**
* When false, the page behind stays fully interactive and scrollable — no
* backdrop, no focus trap, no inert-ing the rest of the page. Renders on a
* `<div popover="manual">` (top layer, non-blocking) where supported,
* falling back to a non-modal `<dialog>` (`.show()`) otherwise.
* @default true
*/
modal?: boolean;
/**
* Applies an iOS-style card effect to the page element marked
* `data-scrollsheet-background`, driven by this sheet's own travel
* progress. `'scale'` scales/insets/rounds it; `'parallax'` only shifts it.
* No-ops if no element is marked.
*
* Left unset, a full-height bottom sheet in the mobile presentation gets
* `'scale'` — that is how the platform presents a sheet that covers the
* screen. Desktop drawers dock beside the page and never move it, and a
* detached (floating-card) sheet never claims the full screen, so neither
* defaults on. `'none'` opts out anywhere.
*/
backgroundEffect?: "scale" | "parallax" | "none";
/**
* Explicit target for backgroundEffect, instead of the default document-wide
* `[data-scrollsheet-background]` query. Pass the same ref you'd otherwise
* mark with the attribute. Skips the ownership check the implicit default
* applies (an explicit ref is unambiguous about which page element it
* means) — matches how an explicit `backgroundEffect` prop today already
* bypasses the implicit default's full-height/opener-containment check.
*/
backgroundRef?: React.RefObject<HTMLElement | null>;
/**
* Inner-content-overflow scrollbar style, for the panel's own scroll at
* max detent (or any content taller than the panel). `'overlay'`: a thin
* auto-hiding thumb (iOS/macOS-style). `'hidden'`: no thumb, no native
* scrollbar either. `'native'`: let the browser draw
* its own.
* @default 'overlay'
*/
scrollbar?: "overlay" | "hidden" | "native";
/**
* Scoped to exactly two things: the backdrop's opacity and
* `themeColorDimming`. Both stay fully transparent/undimmed while the
* sheet is at or below this detent's resolved height, then interpolate
* from there up to the next detent above it (or the tallest detent, if
* this is already the tallest). Unset (default): dims across the full
* range from closed to the first detent, today's behavior. Does *not*
* affect `onTravel`'s progress argument, `--scrollsheet-progress`,
* stacking recede, or `backgroundEffect` — those all track the sheet's
* full-range travel 1:1 regardless of this prop.
*/
largestUndimmedDetent?: DetentSpec;
/**
* Drag sessions (mouse, and touch on non-modal sheets) only start from
* `<Sheet.Handle>` — a pointerdown elsewhere on the panel no-ops. Modal
* touch (native scroll drives the gesture) is restricted via
* `touch-action` instead, except at the tallest detent, where the panel's
* own content scroll still needs it.
* @default false
*/
handleOnly?: boolean;
/**
* No drag sessions at all, handle included — detent changes are
* programmatic (`activeDetent`, `actionsRef`) or click/keyboard
* (`<Sheet.Handle>`) only. Esc and backdrop dismissal are unaffected.
* @default false
*/
disableDrag?: boolean;
/**
* A release never skips over an intermediate detent — the resolved target
* is clamped to the immediate neighbor of the detent the gesture started
* from (or, for a native-scroll settle, the last settled detent).
* @default false
*/
sequentialDetents?: boolean;
/**
* Fraction (0-1) of the first detent below which a release dismisses the
* sheet, replacing the built-in half-of-first-detent rule.
* @default 0.5
*/
closeThreshold?: number;
/**
* Bottom sheets only. When the software keyboard opens with a text field
* focused inside the sheet, promote to the tallest detent so the field
* has the whole remaining viewport to scroll within — a short peek detent
* can't show a field the keyboard would otherwise cover. Restores the
* previous detent on blur unless something else (a drag, a controlled
* change) moved the sheet in between. Gated on the keyboard actually
* appearing (a nonzero measured inset), never on focus alone, so a
* desktop click into an input doesn't expand anything. Promotion goes
* through the same `setActiveDetent` path a controlled change uses — a
* controlled sheet must apply `onActiveDetentChange` back to
* `activeDetent` or this prop has no visible effect.
* @default false
*/
keyboardExpands?: boolean;
/**
* Fires once a drag session resolves, with the real pointer event (the
* `buttons===0` recovery path may pass the triggering `pointermove`
* instead of a `pointerup`) and whether the sheet is staying open —
* `false` only when the release resolved to a dismiss.
*/
onRelease?: (event: PointerEvent, willRemainOpen: boolean) => void;
}
declare function Root({ children, open: openProp, defaultOpen, onOpenChange, onOpenChangeComplete, actionsRef, detents, activeDetent: activeDetentProp, onActiveDetentChange, dismissible, escapeDismissible, backdropDismissible, nonce, themeColorDimming, onTravel, side, modal, backgroundEffect, backgroundRef, scrollbar, largestUndimmedDetent, handleOnly, disableDrag, sequentialDetents, closeThreshold, keyboardExpands, onRelease }: SheetRootProps): React.JSX.Element;
//#endregion
import { A as SheetContentProps, C as Close, D as SheetTitleProps, E as SheetDescriptionProps, F as SheetRootProps, I as TravelInfo, L as DetentSpec, M as Trigger, N as Root, O as Title, P as SheetActions, S as SheetHandleProps, T as SheetCloseProps, a as DrawerHandleProps, c as DrawerPortalProps, h as VaulSnapPoint, i as DrawerContentProps, j as SheetTriggerProps, k as Content, l as DrawerRootProps, n as Drawer, o as DrawerNestedRootProps, s as DrawerOverlayProps, w as Description, x as Handle } from "./index-DdBnpMLy.mjs";
import { t as Side } from "./geometry-C78uYrYJ.mjs";
import { a as SpringConfig, c as spring, o as SpringCurve } from "./index-Bn2Kfep4.mjs";
import { a as SonnerPosition, c as ToastData, d as ToastPromiseData, f as ToastRecord, g as useToasts, h as useSonner, l as ToastIcons, m as toast, n as ToasterShell, o as ToastAction, p as ToastType, r as ToasterProps, s as ToastClassnames, t as injectToastStylesInto, u as ToastPosition } from "./index-FslrKtLB.mjs";
//#region packages/scrollsheet/src/internal/styles.d.ts

@@ -217,0 +6,0 @@ /**

"use client";
import { _ as Handle, b as Trigger, g as Title, h as Description, m as Close, n as Drawer, v as Content, x as Root, y as injectStylesInto } from "./drawer-BXqx0bTN.mjs";
import { _ as Handle, b as Trigger, g as Title, h as Description, m as Close, n as Drawer, v as Content, x as Root, y as injectStylesInto } from "./drawer-OLscKclW.mjs";
import { o as hasDialogSupport } from "./cn-CActSFFr.mjs";
import { a as spring } from "./animate-D5TGKlub.mjs";
import "./motion.mjs";
import { a as useSonner, i as toast, o as useToasts, r as injectToastStylesInto, t as ToasterShell } from "./toast-DgIhy5Sq.mjs";
import { a as useSonner, i as toast, o as useToasts, r as injectToastStylesInto, t as ToasterShell } from "./toast-CODyGYAz.mjs";
//#region packages/scrollsheet/src/index.ts

@@ -8,0 +8,0 @@ function isSupported() {

@@ -1,2 +0,3 @@

import { a as SpringConfig, c as spring, d as geometryFor, f as mapScroll, i as animate, l as Side, n as animateScrollTo, o as SpringCurve, p as recedeTransform, r as AnimateHandle, s as sampleSpringAt, t as ScrollAnimation, u as SideGeometry } from "./index-f5LGCgfI.mjs";
import { a as recedeTransform, i as mapScroll, n as SideGeometry, r as geometryFor, t as Side } from "./geometry-C78uYrYJ.mjs";
import { a as SpringConfig, c as spring, i as animate, n as animateScrollTo, o as SpringCurve, r as AnimateHandle, s as sampleSpringAt, t as ScrollAnimation } from "./index-Bn2Kfep4.mjs";
export { type AnimateHandle, type ScrollAnimation, type Side, type SideGeometry, type SpringConfig, type SpringCurve, animate, animateScrollTo, geometryFor, mapScroll, recedeTransform, sampleSpringAt, spring };

@@ -1,2 +0,2 @@

import { a as SonnerPosition, c as ToastData, d as ToastPromiseData, f as ToastRecord, g as useToasts, h as useSonner, i as resolveVisibleToasts, l as ToastIcons, m as toast, n as ToasterShell, o as ToastAction, p as ToastType, r as ToasterProps, s as ToastClassnames, t as injectToastStylesInto, u as ToastPosition } from "./index-DhRyhV1b.mjs";
import { a as SonnerPosition, c as ToastData, d as ToastPromiseData, f as ToastRecord, g as useToasts, h as useSonner, i as resolveVisibleToasts, l as ToastIcons, m as toast, n as ToasterShell, o as ToastAction, p as ToastType, r as ToasterProps, s as ToastClassnames, t as injectToastStylesInto, u as ToastPosition } from "./index-FslrKtLB.mjs";
export { type SonnerPosition, type ToastAction, type ToastClassnames, type ToastData, type ToastIcons, type ToastPosition, type ToastPromiseData, type ToastRecord, type ToastType, ToasterShell as Toaster, type ToasterProps, injectToastStylesInto, resolveVisibleToasts, toast, useSonner, useToasts };
"use client";
"use client";
import { a as useSonner, i as toast, n as resolveVisibleToasts, o as useToasts, r as injectToastStylesInto, t as ToasterShell } from "./toast-DgIhy5Sq.mjs";
import { a as useSonner, i as toast, n as resolveVisibleToasts, o as useToasts, r as injectToastStylesInto, t as ToasterShell } from "./toast-CODyGYAz.mjs";
export { ToasterShell as Toaster, injectToastStylesInto, resolveVisibleToasts, toast, useSonner, useToasts };
{
"name": "scrollsheet",
"version": "1.0.0-beta.2",
"version": "1.0.0-beta.3",
"publishConfig": {

@@ -5,0 +5,0 @@ "tag": "beta"

@@ -5,3 +5,3 @@ # scrollsheet

One primitive for bottom sheets, drawers, modal dialogs, side panels, and toasts. A real `<dialog>` in the top layer. The browser's own scroll engine for gestures. Spring physics compiled to CSS `linear()`. <!--size:index.gzip:1-->18.5<!--/size--> kB gzipped, <!--size:index.brotli:1-->16.4<!--/size--> kB brotli, plus a mandatory 3.8 kB stylesheet: 22.3 kB combined. React 18+.
One primitive for bottom sheets, drawers, modal dialogs, side panels, and toasts. A real `<dialog>` in the top layer. The browser's own scroll engine for gestures. Spring physics compiled to CSS `linear()`. <!--size:index.gzip:1-->18.6<!--/size--> kB gzipped, <!--size:index.brotli:1-->16.5<!--/size--> kB brotli, plus a mandatory 3.8 kB stylesheet: 22.3 kB combined. React 18+.

@@ -25,4 +25,2 @@ ## Why another drawer

Currently `1.0.0-beta.1` on the `beta` dist-tag: the API is settled and the test matrix is green; the label comes off after the real-device verification pass. ESM only.
## Use

@@ -49,2 +47,4 @@

Design systems and other libraries that re-bundle their CSS: if your pipeline statically compiles custom properties away (postcss-css-variables and similar), it destroys the runtime `--scrollsheet-*` variables the stylesheet's geometry runs on, and sheets break in subtle ways. Import from `scrollsheet/auto` there instead; the injected stylesheet never enters your build.
### Detents

@@ -82,3 +82,3 @@

- **Motion core (experimental).** `scrollsheet/motion` is the React-free layer the sheet runs on: closed-form spring solver, interruptible WAAPI wrapper, scroll tween. 1.6 kB gzipped standalone.
- **Zero-config entry.** `scrollsheet/auto` embeds the stylesheet and injects it on first open: no CSS import needed, <!--size:auto.gzip:1-->22.5<!--/size--> kB gzip for `Sheet` against the default entry's <!--size:index.gzip:1-->18.5<!--/size-->.
- **Zero-config entry.** `scrollsheet/auto` embeds the stylesheet and injects it on first open: no CSS import needed, <!--size:auto.gzip:1-->22.6<!--/size--> kB gzip for `Sheet` against the default entry's <!--size:index.gzip:1-->18.6<!--/size-->.

@@ -85,0 +85,0 @@ Focus containment comes from the platform's `showModal()`, not a JS focus trap. On open, focus lands on the panel so mobile keyboards don't pop unasked; use native `autofocus` to override.

Sorry, the diff of this file is too big to display

import * as React from "react";
//#region packages/scrollsheet/src/trigger.d.ts
interface SheetTriggerProps extends React.ComponentProps<"button"> {
/**
* Render the child element instead of a `<button>`, merging the trigger's
* props (aria, onClick) into it — for custom button components or links.
*/
asChild?: boolean;
}
declare const Trigger: React.ForwardRefExoticComponent<Omit<SheetTriggerProps, "ref"> & React.RefAttributes<HTMLButtonElement>>;
//#endregion
//#region packages/scrollsheet/src/internal/detents.d.ts
/**
* Detents — the stops a sheet can rest at, in the spirit of
* UISheetPresentationController's `detents`.
*
* A detent resolves to the visible height of the sheet in px.
* Accepted forms:
* - 'full' → viewport height minus top inset
* - 'medium' → 50% of viewport
* - 'content' → natural content height (measured, capped at full)
* - number in (0, 1] → fraction of viewport
* - `${number}px` → absolute pixels
*/
type DetentSpec = "full" | "medium" | "content" | number | `${number}px`;
//#endregion
//#region packages/scrollsheet/src/content.d.ts
interface SheetContentProps extends React.ComponentProps<"div"> {
/** Accessible name when no <Sheet.Title> is rendered. */
"aria-label"?: string;
/**
* Stretches [data-scrollsheet-body] to fill the panel (flex column, the
* body itself flex:1/min-height:0) instead of sizing to its natural content
* height. Promotes the docs "Full-height content" recipe into the library —
* write your own inner-scroll child (flex:1, overflow-y:auto) with no CSS
* of your own required on the wrapper.
* @default false
*/
fill?: boolean;
/**
* Render the child element instead of scrollsheet's own panel `<div>`,
* merging the panel's props (role, tabIndex, ref, `data-scrollsheet-*`)
* into it — matches Radix `Dialog.Content asChild`: the child replaces the
* panel outright rather than nesting inside it. The child must be a single
* non-Fragment element; whatever it was given as its own children is
* hoisted into the body wrapper below (detent measurement and stacking
* still need a real DOM descendant there regardless of which element ends
* up as the panel).
* If `children` isn't a single non-Fragment element (missing, a string,
* multiple elements, a `<>...</>` Fragment…), `asChild` is ignored for
* that render — the default panel `<div>` is used instead, with a
* one-time dev warning.
* @default false
*/
asChild?: boolean;
}
declare const Content$1: React.ForwardRefExoticComponent<Omit<SheetContentProps, "ref"> & React.RefAttributes<HTMLDivElement>>;
//#endregion
//#region packages/scrollsheet/src/misc.d.ts
interface SheetTitleProps extends React.ComponentProps<"h2"> {
/** Render the child element instead of an `<h2>`, merging props into it. */
asChild?: boolean;
}
declare const Title: React.ForwardRefExoticComponent<Omit<SheetTitleProps, "ref"> & React.RefAttributes<HTMLHeadingElement>>;
interface SheetDescriptionProps extends React.ComponentProps<"p"> {
/** Render the child element instead of a `<p>`, merging props into it. */
asChild?: boolean;
}
declare const Description: React.ForwardRefExoticComponent<Omit<SheetDescriptionProps, "ref"> & React.RefAttributes<HTMLParagraphElement>>;
interface SheetCloseProps extends React.ComponentProps<"button"> {
/** Render the child element instead of a `<button>`, merging props into it. */
asChild?: boolean;
}
declare const Close: React.ForwardRefExoticComponent<Omit<SheetCloseProps, "ref"> & React.RefAttributes<HTMLButtonElement>>;
//#endregion
//#region packages/scrollsheet/src/handle.d.ts
interface SheetHandleProps extends React.ComponentProps<"button"> {
/** Render the child element instead of a `<button>`, merging props into it. */
asChild?: boolean;
/**
* Where the pill sits. `'inside'` (default) flows at the top of the
* sheet's content. `'floating'` overlays the content — absolutely
* positioned over the panel's top edge, for full-bleed content (maps,
* photos) a flow pill would push down. `'outside'` floats in the backdrop
* above the sheet's top edge (bottom sheets only; other sides render as
* `'inside'`) — it rides the canvas layer outside the panel's clip and
* drags, clicks, and keys exactly like the others.
* @default 'inside'
*/
variant?: "inside" | "floating" | "outside";
}
/**
* The grabber pill. Click cycles detents; ArrowUp/ArrowDown (or Left/Right
* for side sheets) move between them, Home/End jump to the first/last — so
* multi-detent sheets are fully keyboard-operable. With two or more detents
* it exposes itself as a slider (role, value min/max/now, value text) so
* screen readers announce the current stop, not just "button".
*
* The handle is optional. It's purely a click/keyboard affordance layered on
* top of the drag engine, which listens on the whole panel, not the handle —
* omit `<Sheet.Handle>` and the whole panel still drags and dismisses, in
* every mode (modal and non-modal) and every side (bottom/top/left/right).
* There's no boolean prop for this; omission is the API.
*/
declare const Handle$1: React.ForwardRefExoticComponent<Omit<SheetHandleProps, "ref"> & React.RefAttributes<HTMLButtonElement>>;
//#endregion
//#region packages/scrollsheet/src/drawer/index.d.ts
/** vaul's snap point shape: a 0–1 fraction, an absolute `'###px'` string, or `'fit-content'`. */
type VaulSnapPoint = number | string;
/**
* Resolves vaul's `fadeFromIndex` against `snapPoints` into a
* `largestUndimmedDetent` spec, mirroring real vaul's own default (its
* `src/index.tsx`: `fadeFromIndex = snapPoints && snapPoints.length - 1`) —
* omitted with `snapPoints` set defaults to the *topmost* snap point index
* (no dim until the last snap point), not "no undimmed range at all"
* (scrollsheet's own default when `largestUndimmedDetent` is never touched).
* No `snapPoints`, or an out-of-range explicit index, resolves to
* `undefined` (today's full-dim-range behavior) — pulled out as its own pure
* function so this index math is unit-testable independent of rendering.
*/
declare function resolveFadeFromIndex(snapPoints: readonly VaulSnapPoint[] | undefined, fadeFromIndex: number | undefined): DetentSpec | undefined;
/**
* Resolves vaul's `closeThreshold` against whether `snapPoints` is set,
* CONVERTING between opposite conventions: vaul counts the fraction dragged
* AWAY (its 0.25 default = dismiss after a quarter-height drag), scrollsheet
* counts the fraction still VISIBLE (`isBelowCloseThreshold`: dismiss when
* `revealed < firstDetent * closeThreshold`, so higher = easier). A raw
* passthrough inverts the migrated feel — vaul's 0.25 became "drag 75% to
* dismiss", harder than scrollsheet's own 0.5 default instead of easier —
* so the mapping is `1 - value`, and an omitted prop resolves to vaul's own
* `CLOSE_THRESHOLD` default of 0.25 (=> 0.75 here), never `Sheet.Root`'s
* unrelated 0.5.
* `snapPoints` set makes `closeThreshold` dead code in real vaul (its
* `onRelease` returns through the snap-points branch before ever reading
* it), matched here by always resolving to `undefined` in that case.
* Pulled out as its own pure function, mirroring `resolveFadeFromIndex`,
* so the conversion math is unit-testable independent of rendering.
*/
declare function resolveCloseThreshold(snapPoints: readonly VaulSnapPoint[] | undefined, closeThreshold: number | undefined): number | undefined;
/**
* Composes vaul's `onClose` on top of `onOpenChange`: `onClose` fires
* whenever the transition target is `false`, then `onOpenChange` always
* fires. Real vaul's `closeDrawer()` calls `onClose` from every dismiss
* path (swipe past threshold, Esc, backdrop, imperative); scrollsheet's own
* `onOpenChange` is already the single funnel every one of *its* dismiss
* paths goes through (`useControllableState`'s `onChange`, which fires
* exactly once per real open/false transition), so layering `onClose` on
* top of that wiring reproduces "every dismiss path" without a second one.
* Returns `undefined` when neither callback is given, matching the
* conditional-wrapper convention `onRelease`'s cast uses below. Pulled out
* as its own pure function so the composition is unit-testable without a
* live DOM/click event.
*/
declare function composeOpenChange(onOpenChange: ((open: boolean) => void) | undefined, onClose: (() => void) | undefined): ((open: boolean) => void) | undefined;
interface DrawerRootProps {
children?: React.ReactNode;
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
/**
* Fires whenever the drawer closes — every dismiss path (swipe past
* threshold, Esc, backdrop tap, imperative `close()`) funnels through the
* same `onOpenChange` wiring this is layered on top of, matching real
* vaul's `closeDrawer()`, which calls `onClose` on every one of those
* paths too.
*/
onClose?: () => void;
/** @default true */
dismissible?: boolean;
/** Fractions (0–1), `'###px'` strings, or `'fit-content'` — translated to scrollsheet `detents`. */
snapPoints?: readonly VaulSnapPoint[];
/** Optionally-controlled active snap point; `null` means "no explicit snap point". */
activeSnapPoint?: VaulSnapPoint | null;
setActiveSnapPoint?: (snapPoint: VaulSnapPoint | null) => void;
/** Fires when the open/close transition actually completes — wired to `onOpenChangeComplete`, exact rather than vaul's timer. */
onAnimationEnd?: (open: boolean) => void;
/** Ignored — the native `<dialog>` manages focus itself. */
autoFocus?: boolean;
/** Ignored — nesting is automatic: a `<Drawer.Content>` rendered inside another registers itself. */
nested?: boolean;
/** Maps to scrollsheet `side` — all four edges. */
direction?: "top" | "bottom" | "left" | "right";
/** Maps to scrollsheet `modal` — `false` renders non-modal (Popover top layer, page stays interactive). */
modal?: boolean;
/**
* Maps to `backgroundEffect="scale"`; vaul's own `[data-vaul-drawer-wrapper]`
* is picked up as the target. Left false (vaul's default), this maps to
* `'none'`, not unset — vaul never scales the page unless asked, so the
* scrollsheet default for full-height sheets must not leak in here.
*/
shouldScaleBackground?: boolean;
/**
* Maps to scrollsheet `closeThreshold` — but only when `snapPoints` is
* unset. In real vaul, `closeThreshold` is dead code once `snapPoints`
* exist (its `onRelease` returns through the snap-points branch before
* ever reading it); this compat layer matches that rather than giving the
* prop unconditional live effect. Passing both together warns once in dev
* — use the native `Sheet.Root closeThreshold` directly if you want it to
* combine with detents.
*
* Omitted (with `snapPoints` unset) resolves to vaul's own default of
* `0.25` (`CLOSE_THRESHOLD` in vaul's `src/constants.ts`) — not
* scrollsheet's own `0.5` default, which is twice the drag distance.
*/
closeThreshold?: number;
/**
* Maps to `largestUndimmedDetent`, resolved against `snapPoints` at this
* index. Omitted (with `snapPoints` set) defaults to `snapPoints.length -
* 1` — vaul's own default, meaning no dim until the topmost snap point —
* not "dim across the full range" (scrollsheet's own default when
* `largestUndimmedDetent` is never touched at all).
*/
fadeFromIndex?: number;
/** Maps to `sequentialDetents`. */
snapToSequentialPoint?: boolean;
/** Maps to `handleOnly` directly. */
handleOnly?: boolean;
/**
* Maps to `onRelease`. Typed as vaul's own `React.PointerEvent<HTMLDivElement>`
* (rather than scrollsheet's wider native-`PointerEvent` type) so a
* handler already typed against vaul's declaration still assigns cleanly —
* see the internal cast in `Root` for why this is safe at runtime.
*/
onRelease?: (event: React.PointerEvent<HTMLDivElement>, open: boolean) => void;
setBackgroundColorOnScale?: boolean;
noBodyStyles?: boolean;
disablePreventScroll?: boolean;
preventScrollRestoration?: boolean;
repositionInputs?: boolean;
scrollLockTimeout?: number;
onDrag?: (event: React.PointerEvent, percentageDragged: number) => void;
container?: HTMLElement | null;
/**
* Ignored — real vaul's `fixed` swaps its own keyboard-avoidance strategy
* (resize instead of translate); scrollsheet's `useKeyboardViewport` has
* no equivalent toggle.
*/
fixed?: boolean;
}
declare function Root(props: DrawerRootProps): React.JSX.Element;
/**
* Nested drawers just work: a `<Drawer.Content>` rendered inside another
* `<Drawer.Content>` automatically registers with the parent's stacking
* context (the parent recedes while the child is open), the way iOS stacks
* sheets. `NestedRoot` is an alias of `Root` kept for drop-in compatibility
* with vaul's API surface — there's nothing distinct for it to do.
*/
declare const NestedRoot: typeof Root;
type DrawerNestedRootProps = DrawerRootProps;
interface DrawerPortalProps {
children?: React.ReactNode;
/** Ignored — the native `<dialog>` always renders into the browser's top layer. */
container?: HTMLElement | null;
}
declare function Portal({ children, container }: DrawerPortalProps): React.JSX.Element;
type DrawerOverlayProps = React.ComponentProps<"div">;
/**
* scrollsheet draws its own backdrop (`.scrollsheet-backdrop`, themeable via
* the `--scrollsheet-backdrop` CSS variable) that tracks scroll progress, so
* there's nothing for a separate overlay element to render. This accepts
* vaul's `<Drawer.Overlay className .../>` so existing JSX doesn't crash —
* restyle the backdrop through the CSS variable instead.
*/
declare function Overlay(_props: DrawerOverlayProps): null;
/**
* Whether translated `snapPoints` produced more than one distinct detent —
* feeds Content's `data-vaul-snap-points` attribute. vaul's own
* CSS-selector convention treats a single snap point the same as none
* (scrollsheet's own single-detent default also resolves to `false` here).
* Pulled out as its own pure function, mirroring `resolveFadeFromIndex` /
* `resolveCloseThreshold`, so it's unit-testable independent of rendering
* (`<Drawer.Content>`'s actual DOM output is behind a client-only mount
* gate — see content.tsx — so SSR can't observe the attribute directly).
*/
declare function hasMultipleSnapPoints(detents: readonly DetentSpec[]): boolean;
interface DrawerContentProps extends SheetContentProps {
onPointerDownOutside?: (event: CustomEvent) => void;
onOpenAutoFocus?: (event: Event) => void;
onEscapeKeyDown?: (event: KeyboardEvent) => void;
onCloseAutoFocus?: (event: Event) => void;
onInteractOutside?: (event: CustomEvent) => void;
onFocusOutside?: (event: CustomEvent) => void;
forceMount?: boolean;
}
declare const Content: React.ForwardRefExoticComponent<Omit<DrawerContentProps, "ref"> & React.RefAttributes<HTMLDivElement>>;
/**
* Composes the click handler Handle passes to `SheetHandle`: the caller's
* own `onClick` always fires first, then — when `preventCycle` is set —
* `event.preventDefault()`, the same signal `SheetHandle`'s own
* click-to-cycle logic already checks (`if (event.defaultPrevented) return`
* in handle.tsx) to skip advancing to the next detent. Mirrors real vaul's
* `preventCycle`, read inside its own `handleCycleSnapPoints` guard. Pulled
* out as its own pure function so the composition is unit-testable without
* a live DOM/click event.
*/
declare function composeHandleClick(preventCycle: boolean | undefined, onClick: ((event: React.MouseEvent<HTMLButtonElement>) => void) | undefined): (event: React.MouseEvent<HTMLButtonElement>) => void;
interface DrawerHandleProps extends SheetHandleProps {
/**
* Suppresses the click-to-cycle-detents behavior — the handle still
* renders, drags, and is keyboard-operable as usual, but a click no
* longer advances to the next detent. Mirrors real vaul's own
* `preventCycle`, which guards the same click path (vaul's handle has no
* keyboard cycling to suppress).
*/
preventCycle?: boolean;
}
declare const Handle: React.ForwardRefExoticComponent<Omit<DrawerHandleProps, "ref"> & React.RefAttributes<HTMLButtonElement>>;
/**
* Sheet.Close's self-closing form renders a styled ✕ default; vaul's own
* `<Drawer.Close />` renders an empty unstyled button. A migrating user
* must not get a surprise icon button from a version swap, so the compat
* Close pins children to null (defined, so the styled-default branch never
* arms) unless the caller passed some.
*/
declare const DrawerClose: React.ForwardRefExoticComponent<Omit<SheetCloseProps, "ref"> & React.RefAttributes<HTMLButtonElement>>;
/** Namespace-style access matching vaul's `<Drawer.Root>…</Drawer.Root>` shape. */
declare const Drawer: {
Root: typeof Root;
NestedRoot: typeof Root;
Trigger: React.ForwardRefExoticComponent<Omit<SheetTriggerProps, "ref"> & React.RefAttributes<HTMLButtonElement>>;
Portal: typeof Portal;
Overlay: typeof Overlay;
Content: React.ForwardRefExoticComponent<Omit<DrawerContentProps, "ref"> & React.RefAttributes<HTMLDivElement>>;
Close: React.ForwardRefExoticComponent<Omit<SheetCloseProps, "ref"> & React.RefAttributes<HTMLButtonElement>>;
Title: React.ForwardRefExoticComponent<Omit<SheetTitleProps, "ref"> & React.RefAttributes<HTMLHeadingElement>>;
Description: React.ForwardRefExoticComponent<Omit<SheetDescriptionProps, "ref"> & React.RefAttributes<HTMLParagraphElement>>;
Handle: React.ForwardRefExoticComponent<Omit<DrawerHandleProps, "ref"> & React.RefAttributes<HTMLButtonElement>>;
};
//#endregion
export { SheetContentProps as A, Close as C, SheetTitleProps as D, SheetDescriptionProps as E, SheetTriggerProps as M, Trigger as N, Title as O, SheetHandleProps as S, SheetCloseProps as T, composeOpenChange as _, DrawerHandleProps as a, resolveFadeFromIndex as b, DrawerPortalProps as c, NestedRoot as d, Overlay as f, composeHandleClick as g, VaulSnapPoint as h, DrawerContentProps as i, DetentSpec as j, Content$1 as k, DrawerRootProps as l, Root as m, Drawer as n, DrawerNestedRootProps as o, Portal as p, DrawerClose as r, DrawerOverlayProps as s, Content as t, Handle as u, hasMultipleSnapPoints as v, Description as w, Handle$1 as x, resolveCloseThreshold as y };
import * as React from "react";
//#region packages/scrollsheet/src/toast/state.d.ts
/** Drives the row's icon/role and the `data-type` attribute the stylesheet reads. */
type ToastType = "default" | "success" | "error" | "warning" | "info" | "loading";
/**
* The six-value position union (matches real Sonner's own set). Every value
* routes for real at this layer (see `selectPositionToasts` below) — how
* many of the six a given `<Toaster>` actually renders is that component's
* own call (`toaster.tsx`'s `resolveToasterPosition`), not something this
* module gates.
*/
type ToastPosition = "top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right";
/** Compat alias for the pre-neutral-naming name — new code should use `ToastPosition`. */
type SonnerPosition = ToastPosition;
/**
* Real Sonner's own swipe-dismiss edge union (types.ts:152). A Toaster-level
* `swipeDirections` override (see `ToasterProps` in `../toaster`) understands
* these four values regardless of which position they're applied to — lives
* here, not in the shell folder, so both the Sheet-backed `toaster.tsx` and
* the new shell can reference the type without either depending on the
* other's own directory.
*/
type SwipeDirection = "top" | "right" | "bottom" | "left";
interface ToastAction {
label: React.ReactNode;
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
}
/** Per-slot extra class names, mirrors real Sonner's own `ToastClassnames` field-for-field. */
interface ToastClassnames {
toast?: string;
title?: string;
description?: string;
loader?: string;
closeButton?: string;
cancelButton?: string;
actionButton?: string;
success?: string;
error?: string;
info?: string;
warning?: string;
loading?: string;
default?: string;
content?: string;
icon?: string;
}
/** Per-type icon overrides, mirrors real Sonner's own `ToastIcons`. */
interface ToastIcons {
success?: React.ReactNode;
info?: React.ReactNode;
warning?: React.ReactNode;
error?: React.ReactNode;
loading?: React.ReactNode;
close?: React.ReactNode;
}
/** The `data` argument to `toast()` / `.success()` / etc — mirrors Sonner's own `ExternalToast`. */
interface ToastData {
/** Update-in-place: reusing an id already in the queue merges onto that entry instead of pushing a new one. */
id?: number | string;
description?: React.ReactNode;
/** Milliseconds before auto-dismiss, or `Infinity` to disable it. @default 4000 (Toaster's own `duration`, or `toastOptions.duration`) */
duration?: number;
/** @default true */
dismissible?: boolean;
icon?: React.ReactNode;
action?: ToastAction;
cancel?: ToastAction;
className?: string;
classNames?: ToastClassnames;
onDismiss?: (toast: ToastRecord) => void;
onAutoClose?: (toast: ToastRecord) => void;
/** Routes this toast to the `<Toaster toasterId>` with the matching id instead of the default, un-keyed toaster. */
toasterId?: string;
/** Which position's `<ol>` group this toast renders in — omitted routes to the owning `<Toaster>`'s own `position` prop instead (see `selectPositionToasts`). Valid regardless of which positions that Toaster actually renders. */
position?: ToastPosition;
/** Rendered as `data-testid` on the row — mirrors real Sonner's own `testId`. */
testId?: string;
/** Overrides the close button's accessible name for this toast only. @default "Close toast" (or the Toaster's own `toastOptions.closeButtonAriaLabel`) */
closeButtonAriaLabel?: string;
}
/** A live queue entry — what `useSonner()` and each row render from. */
interface ToastRecord extends ToastData {
id: number | string;
type: ToastType;
title?: React.ReactNode;
/** Set by `toast.custom()` — its presence means "render this node verbatim, skip the card chrome" (see ToastRow). */
jsx?: React.ReactNode;
createdAt: number;
}
/**
* A `success`/`error` handler may resolve to this instead of a plain node —
* `message` becomes the title, every other field (description, action,
* icon, ...) applies to the settled toast. Mirrors real Sonner's own
* `PromiseIExtendedResult` (state.ts's `isExtendedResult` check).
*/
type ToastPromiseExtendedResult = Omit<ToastData, "id"> & {
message?: React.ReactNode;
};
interface ToastPromiseData<T> extends Omit<ToastData, "id" | "description"> {
id?: number | string;
loading?: React.ReactNode;
success?: React.ReactNode | ((data: T) => React.ReactNode | ToastPromiseExtendedResult | Promise<React.ReactNode | ToastPromiseExtendedResult>);
error?: React.ReactNode | ((error: unknown) => React.ReactNode | ToastPromiseExtendedResult | Promise<React.ReactNode | ToastPromiseExtendedResult>);
/**
* A static value carries through to the settled toast unchanged; a
* function is called with the settle value (the resolved data, or the
* error/HTTP-status string on the error branches) — matches real Sonner's
* own per-branch description resolution (it never wipes a static one).
*/
description?: React.ReactNode | ((value: unknown) => React.ReactNode | Promise<React.ReactNode>);
finally?: () => void;
}
type ToastFn = ((message: React.ReactNode, data?: ToastData) => number | string) & {
success: (message: React.ReactNode, data?: ToastData) => number | string;
error: (message: React.ReactNode, data?: ToastData) => number | string;
info: (message: React.ReactNode, data?: ToastData) => number | string;
warning: (message: React.ReactNode, data?: ToastData) => number | string;
loading: (message: React.ReactNode, data?: ToastData) => number | string;
/** Alias for the base call, with no type — matches real Sonner's `toast.message()`. */
message: (message: React.ReactNode, data?: ToastData) => number | string;
/**
* `jsx` is a render function receiving the resolved id (matching real
* Sonner), so a custom toast can call `toast.dismiss(id)` on itself. The
* resulting node is stored on the record's `jsx` field, which ToastRow
* renders verbatim, skipping the card chrome (icon/title/description)
* entirely.
*/
custom: (jsx: (id: number | string) => React.ReactNode, data?: ToastData) => number | string;
promise: <T>(promiseOrFn: Promise<T> | (() => Promise<T>), data: ToastPromiseData<T>) => (number | string) & {
unwrap: () => Promise<T>;
};
dismiss: (id?: number | string) => number | string | undefined;
/** Every live (not yet dismissed) toast across every toaster — mirrors real Sonner's own `toast.getToasts()`. */
getToasts: () => readonly ToastRecord[];
/** Every toast ever created, oldest-first, capped at 100 — mirrors real Sonner's own `toast.getHistory()`. */
getHistory: () => readonly ToastRecord[];
};
declare const toast: ToastFn;
/**
* Subscribes to the same store `toast()` writes to. Real Sonner's own
* `useSonner()` takes no arguments and has no visible per-toaster filtering
* in its public surface — matched here unfiltered, across every toaster.
* Not verified byte-for-byte against Sonner's source; matches its observed
* behavior.
*/
declare function useSonner(): {
toasts: ToastRecord[];
};
/** Neutral-named alias for `useSonner` — same implementation, the name fresh docs teach. */
declare const useToasts: typeof useSonner;
//#endregion
//#region packages/scrollsheet/src/toast/toaster.d.ts
/** Pure, exported for tests. @default 3, matching real Sonner's own default. */
declare function resolveVisibleToasts(count: number | undefined): number;
interface ToasterProps {
/** Routes this Toaster to only the toasts created with a matching `toasterId` — omitted (the common case) renders the default, un-keyed queue. */
id?: string;
/** @deprecated Use `id` instead — matches real Sonner's own `Toaster` prop name. */
toasterId?: string;
/** Which corner (or edge-center) this Toaster's own default `<ol>` renders in. All six positions are real. @default 'bottom-right' */
position?: ToastPosition;
/**
* v1 always renders the static light card real Sonner ships by default.
* 'dark'/'system' aren't implemented yet (a v1.1 follow-up) and warn once
* rather than silently no-op — forcing a real visual mismatch would be
* worse than an honest warning.
*/
theme?: "light" | "dark" | "system";
/** Not implemented yet (v1.1) — warns once if set. */
richColors?: boolean;
/** Force the row list open (instead of the collapsed front-card-plus-ghosts) even without hover/focus. */
expand?: boolean;
/**
* Maximum toasts visible (interactive, full opacity) at once per position
* group. Beyond this, older toasts become hidden — `data-visible="false"`,
* still a real DOM node, still timing — rather than evicted; nothing is
* ever dismissed by overflow, and `dismissible: false` earns no special
* protection (it overflows into hidden exactly like any other toast).
* @default 3 (matches real Sonner's own visibleToasts default)
*/
visibleToasts?: number;
/** @default false */
closeButton?: boolean;
/** Milliseconds before auto-dismiss; a per-toast `duration` wins over this. @default 4000 */
duration?: number;
/** Gap between stacked rows, px. @default 14 */
gap?: number;
/** Distance from the viewport edge, px (or any CSS length). @default 24 (16 at the <600px full-bleed breakpoint) */
offset?: number | string;
/**
* Which edge(s) each toast may be swiped away from, overriding the
* per-position default (a corner position allows both its own y and x
* edges; a *-center position only its own y edge). One setting for the
* whole Toaster, applied uniformly to every position group it renders —
* matches real Sonner's own single Toaster-wide `swipeDirections` prop
* (it has no per-toast override).
*/
swipeDirections?: readonly SwipeDirection[];
/** Defaults merged underneath each individual `toast()` call's own options. */
toastOptions?: Omit<ToastData, "id" | "toasterId">;
/** Per-type icon overrides; a per-toast `icon` always wins over these. */
icons?: ToastIcons;
className?: string;
style?: React.CSSProperties;
/** Accessible name for the notifications region. @default "Notifications" */
containerAriaLabel?: string;
/**
* Global keyboard shortcut that expands the stack and moves focus into it
* — every field must be truthy on the event (a modifier like `altKey`, or
* a `KeyboardEvent.code` match), same contract as real Sonner's own
* `hotkey` prop. Escape while focus is inside the region collapses it
* back. Pass `[]` to disable.
* @default ['altKey', 'KeyT']
*/
hotkey?: readonly string[];
/** CSP nonce for the injected style tag. */
nonce?: string;
}
//#endregion
//#region packages/scrollsheet/src/toast/shell/toaster-shell.d.ts
declare function ToasterShell({ id, toasterId, position, theme, richColors, expand: forceExpand, visibleToasts: visibleToastsProp, closeButton, duration: durationProp, gap, offset, swipeDirections, toastOptions, icons, className, style, containerAriaLabel, hotkey, nonce }: ToasterProps): React.ReactPortal | null;
//#endregion
//#region packages/scrollsheet/src/toast/toast-styles.d.ts
/**
* Injects the toast stylesheet into a shadow root via adoptedStyleSheets
* instead of document.head — mirrors src/internal/styles.ts's
* injectStylesInto. Call once per shadow root that hosts a <Toaster>,
* before it first renders. A sibling export rather than folding into
* injectStylesInto: the toast layer is its own chunk (dist/toast.mjs) that
* never loads for a core-only consumer, so its shadow-root injector has to
* be reachable without pulling core's own chunk (and vice versa) — one
* shared call would break that tree-shaking boundary.
*
* Falls back silently to a <style> element appended to the shadow root
* itself on engines without constructable stylesheets (Safari <16.4).
* `nonce` only applies on that fallback path: it sets the injected
* `<style>` element's `nonce` attribute so a CSP `style-src` policy with
* `'nonce-...'` allows it. The `adoptedStyleSheets` path constructs a
* `CSSStyleSheet` and adopts it directly — it is never parsed as an inline
* style element, so CSP has no inline-style check to gate there and `nonce`
* is accepted (for a stable signature across both paths) but unused.
*/
declare function injectToastStylesInto(root: ShadowRoot, nonce?: string): void;
//#endregion
export { SonnerPosition as a, ToastData as c, ToastPromiseData as d, ToastRecord as f, useToasts as g, useSonner as h, resolveVisibleToasts as i, ToastIcons as l, toast as m, ToasterShell as n, ToastAction as o, ToastType as p, ToasterProps as r, ToastClassnames as s, injectToastStylesInto as t, ToastPosition as u };
"use client";
import { c as prefersReducedMotion, h as warnOnce, n as useCloseWatcher, r as createStyleInjector, t as cn } from "./cn-CActSFFr.mjs";
import * as React from "react";
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
import { createPortal } from "react-dom";
//#region packages/scrollsheet/src/toast/state.ts
let toasts = [];
const listeners = new Set();
let uid = 0;
const MAX_HISTORY_SIZE = 100;
let history = [];
function trimHistory() {
let toRemove = history.length - MAX_HISTORY_SIZE;
if (toRemove <= 0) return;
const liveIds = new Set(toasts.map((t) => t.id));
history = history.filter((record) => {
if (toRemove > 0 && !liveIds.has(record.id)) {
toRemove -= 1;
return false;
}
return true;
});
}
function recordHistory(record) {
const index = history.findIndex((t) => t.id === record.id);
if (index === -1) {
history = [...history, record];
trimHistory();
return;
}
history = history.map((t, i) => i === index ? record : t);
}
function nextId() {
uid += 1;
return uid;
}
function publish() {
for (const listener of listeners) listener();
}
function subscribe(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
}
function getSnapshot() {
return toasts;
}
const EMPTY_TOASTS = [];
function getServerSnapshot() {
return EMPTY_TOASTS;
}
function upsert(id, patch, fallbackType) {
const resolvedId = id ?? nextId();
const index = toasts.findIndex((t) => t.id === resolvedId);
const type = patch.type ?? fallbackType;
let record;
if (index === -1) {
record = {
dismissible: true,
...patch,
id: resolvedId,
type,
createdAt: Date.now()
};
toasts = [...toasts, record];
} else {
record = {
...toasts[index],
...patch,
id: resolvedId,
type
};
toasts = toasts.map((t, i) => i === index ? record : t);
}
recordHistory(record);
publish();
return resolvedId;
}
function dismiss(id) {
if (id === void 0) {
const swept = new Set(toasts);
for (const t of swept) {
if (!toasts.includes(t)) continue;
t.onDismiss?.(t);
}
toasts = toasts.filter((t) => !swept.has(t));
publish();
return;
}
const existing = toasts.find((t) => t.id === id);
if (!existing) return id;
existing.onDismiss?.(existing);
toasts = toasts.filter((t) => t !== existing);
publish();
return id;
}
function expire(id) {
const existing = toasts.find((t) => t.id === id);
if (!existing) return;
existing.onAutoClose?.(existing);
toasts = toasts.filter((t) => t !== existing);
publish();
}
function baseToast(message, data) {
return upsert(data?.id, {
...data,
title: message
}, "default");
}
function withType(type) {
return (message, data) => upsert(data?.id, {
...data,
title: message
}, type);
}
const toastImpl = baseToast;
toastImpl.success = withType("success");
toastImpl.error = withType("error");
toastImpl.info = withType("info");
toastImpl.warning = withType("warning");
toastImpl.loading = withType("loading");
toastImpl.message = withType("default");
toastImpl.custom = (jsx, data) => {
const id = data?.id ?? nextId();
return upsert(id, {
...data,
jsx: jsx(id)
}, "default");
};
function isHttpResponse(value) {
return typeof value === "object" && value !== null && "ok" in value && typeof value.ok === "boolean" && "status" in value && typeof value.status === "number";
}
function isPromiseExtendedResult(value) {
return typeof value === "object" && value !== null && !React.isValidElement(value);
}
function applyPromiseSettlement(id, resolved, fallbackTitle, description, type) {
if (isPromiseExtendedResult(resolved)) {
const { message, ...rest } = resolved;
upsert(id, {
description,
...rest,
title: message ?? fallbackTitle,
type
}, type);
return;
}
upsert(id, {
title: resolved ?? fallbackTitle,
description,
type
}, type);
}
function promiseImpl(promiseOrFn, data) {
const { loading, success, error, finally: onFinally, description, ...rest } = data;
const id = upsert(data.id, {
...rest,
description: typeof description === "function" ? void 0 : description,
title: loading,
type: "loading"
}, "loading");
const settle = typeof promiseOrFn === "function" ? promiseOrFn() : promiseOrFn;
let settled;
const chain = settle.then(async (result) => {
if (isHttpResponse(result) && !result.ok) {
settled = {
ok: false,
reason: result
};
const statusMessage = `HTTP error! status: ${result.status}`;
const resolvedDescription = typeof description === "function" ? await description(statusMessage) : description;
const resolved = typeof error === "function" ? await error(statusMessage) : error;
applyPromiseSettlement(id, resolved, "Error", resolvedDescription, "error");
return;
}
if (result instanceof Error) {
settled = {
ok: false,
reason: result
};
const resolvedDescription = typeof description === "function" ? await description(result) : description;
const resolved = typeof error === "function" ? await error(result) : error;
applyPromiseSettlement(id, resolved, "Error", resolvedDescription, "error");
return;
}
settled = {
ok: true,
value: result
};
const resolvedDescription = typeof description === "function" ? await description(result) : description;
const resolved = typeof success === "function" ? await success(result) : success;
applyPromiseSettlement(id, resolved, "Success", resolvedDescription, "success");
}).catch(async (err) => {
settled = {
ok: false,
reason: err
};
const resolvedDescription = typeof description === "function" ? await description(err) : description;
const resolved = typeof error === "function" ? await error(err) : error;
applyPromiseSettlement(id, resolved, "Error", resolvedDescription, "error");
}).finally(() => onFinally?.());
const unwrap = () => chain.then(() => {
if (!settled) throw new Error("scrollsheet toast.promise: chain settled with no outcome");
if (settled.ok) return settled.value;
throw settled.reason;
});
return Object.assign(id, { unwrap });
}
toastImpl.promise = promiseImpl;
toastImpl.dismiss = dismiss;
toastImpl.getToasts = () => toasts;
toastImpl.getHistory = () => history;
const toast = toastImpl;
function useSonner() {
return { toasts: React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) };
}
const useToasts = useSonner;
function selectToasterToasts(all, toasterId) {
return all.filter((t) => toasterId === void 0 ? t.toasterId === void 0 : t.toasterId === toasterId);
}
function selectPositionToasts(all, position, defaultPosition) {
return all.filter((t) => t.position === void 0 ? position === defaultPosition : t.position === position);
}
function selectToastWindow(toasts, visibleToasts) {
const newestFirst = [...toasts].reverse();
return {
visible: newestFirst.slice(0, visibleToasts),
hidden: newestFirst.slice(visibleToasts)
};
}
//#endregion
//#region packages/scrollsheet/src/toast/toast-styles.ts
const { injectDocument, injectShadowRoot } = createStyleInjector(`.scrollsheet-toast{border-radius:var(--scrollsheet-toast-radius,14px);color:#18181b;background:#fff;border:1px solid #00000014;align-items:flex-start;gap:10px;padding:16px;font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,sans-serif;font-size:13px;display:flex;position:relative;box-shadow:0 4px 12px #0000001a}.scrollsheet-toast[data-scrollsheet-custom]{box-shadow:none;background:0 0;border:none;padding:0}.scrollsheet-toast-icon{color:#fff;background:#6b7280;border-radius:50%;flex:none;place-items:center;width:18px;height:18px;margin-top:1px;font-size:11px;line-height:1;display:grid}.scrollsheet-toast-icon[data-type=success]{background:#22c55e}.scrollsheet-toast-icon[data-type=error]{background:#ef4444}.scrollsheet-toast-icon[data-type=warning]{background:#f59e0b}.scrollsheet-toast-icon[data-type=info]{background:#3b82f6}.scrollsheet-toast-icon[data-type=loading]{background:0 0}.scrollsheet-toast-spinner{border:2px solid #00000026;border-top-color:#0000008c;border-radius:50%;width:14px;height:14px;animation:.6s linear infinite scrollsheet-toast-spin}@keyframes scrollsheet-toast-spin{to{transform:rotate(360deg)}}@media (prefers-reduced-motion:reduce){.scrollsheet-toast-spinner{animation-duration:1.6s}}.scrollsheet-toast-body{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.scrollsheet-toast-title{font-size:13px;font-weight:500;line-height:1.35}.scrollsheet-toast-description{color:#0009;font-size:13px;line-height:1.4}.scrollsheet-toast-actions{flex:none;align-items:center;gap:6px;display:flex}.scrollsheet-toast-action,.scrollsheet-toast-cancel{cursor:pointer;border:none;border-radius:4px;height:24px;padding:0 8px;font-size:12px;font-weight:500}.scrollsheet-toast-action{color:#fff;background:#18181b}.scrollsheet-toast-cancel{color:#18181b;background:#0000000f}.scrollsheet-toast-close{color:#00000080;cursor:pointer;background:0 0;border:none;border-radius:50%;flex:none;place-items:center;width:20px;height:20px;padding:0;font-size:14px;line-height:1;display:grid}.scrollsheet-toast-close:hover{background:#0000000f}[data-scrollsheet-toaster]{z-index:2147483647;width:min(356px, calc(100vw - 2 * var(--scrollsheet-toast-offset,var(--sonner-offset,24px))));outline:none;margin:0;padding:0;list-style:none;transition:transform .4s;position:fixed}[data-scrollsheet-toaster][data-x-position=right]{right:var(--scrollsheet-toast-offset,var(--sonner-offset,24px))}[data-scrollsheet-toaster][data-x-position=left]{left:var(--scrollsheet-toast-offset,var(--sonner-offset,24px))}[data-scrollsheet-toaster][data-x-position=center]{left:50%;transform:translate(-50%)}[data-scrollsheet-toaster][data-y-position=top]{top:var(--scrollsheet-toast-offset,var(--sonner-offset,24px))}[data-scrollsheet-toaster][data-y-position=bottom]{bottom:var(--scrollsheet-toast-offset,var(--sonner-offset,24px))}.scrollsheet-toast{--scrollsheet-toast-lift:-1;--scrollsheet-toast-y:translateY(100%);opacity:0;transform:var(--scrollsheet-toast-y);touch-action:none;box-sizing:border-box;overflow-wrap:anywhere;transition:transform .25s,opacity .25s,height .25s;position:absolute;left:0;right:0}.scrollsheet-toast[data-y-position=top]{--scrollsheet-toast-lift:1;--scrollsheet-toast-y:translateY(-100%);top:0}.scrollsheet-toast[data-y-position=bottom]{--scrollsheet-toast-lift:-1;--scrollsheet-toast-y:translateY(100%);bottom:0}.scrollsheet-toast[data-mounted]{--scrollsheet-toast-y:translateY(0);opacity:1}.scrollsheet-toast[data-expanded=false][data-front=false]{--scrollsheet-toast-y:translateY(calc(var(--scrollsheet-toast-lift) * var(--scrollsheet-toast-gap,var(--sonner-gap,14px)) * var(--scrollsheet-toast-toasts-before,var(--sonner-toasts-before,0)))) scale(calc(1 - var(--scrollsheet-toast-toasts-before,var(--sonner-toasts-before,0)) * .05));height:var(--scrollsheet-toast-front-height,var(--sonner-front-height,auto));overflow:hidden}.scrollsheet-toast[data-expanded=false][data-front=false]>*{opacity:0}.scrollsheet-toast[data-mounted][data-expanded=true]{--scrollsheet-toast-y:translateY(calc(var(--scrollsheet-toast-lift) * var(--scrollsheet-toast-stack-offset,var(--sonner-stack-offset,0px))));height:var(--scrollsheet-toast-initial-height,var(--sonner-initial-height,auto))}.scrollsheet-toast[data-visible=false]{opacity:0;pointer-events:none}.scrollsheet-toast[data-removed]{pointer-events:none}.scrollsheet-toast[data-removed][data-front=true]{--scrollsheet-toast-y:translateY(calc(var(--scrollsheet-toast-lift) * -100%));opacity:0}.scrollsheet-toast[data-removed][data-front=false][data-expanded=true]{--scrollsheet-toast-y:translateY(calc(var(--scrollsheet-toast-lift) * var(--scrollsheet-toast-stack-offset,var(--sonner-stack-offset,0px)) + var(--scrollsheet-toast-lift) * -100%));opacity:0}.scrollsheet-toast[data-removed][data-front=false][data-expanded=false]{--scrollsheet-toast-y:translateY(40%);opacity:0}.scrollsheet-toast[data-swiping=true]{transform:var(--scrollsheet-toast-y) translateY(var(--scrollsheet-toast-swipe-y,var(--sonner-swipe-y,0px))) translateX(var(--scrollsheet-toast-swipe-x,var(--sonner-swipe-x,0px)));transition:none}.scrollsheet-toast[data-swiped=true]{-webkit-user-select:none;user-select:none}.scrollsheet-toast[data-swipe-out=true]{animation:.2s ease-out forwards scrollsheet-toast-swipe-out}.scrollsheet-toast[data-swipe-direction=left]{--scrollsheet-toast-swipe-out-x:-1}.scrollsheet-toast[data-swipe-direction=right]{--scrollsheet-toast-swipe-out-x:1}.scrollsheet-toast[data-swipe-direction=up]{--scrollsheet-toast-swipe-out-y:-1}.scrollsheet-toast[data-swipe-direction=down]{--scrollsheet-toast-swipe-out-y:1}@keyframes scrollsheet-toast-swipe-out{0%{transform:var(--scrollsheet-toast-y) translateY(var(--scrollsheet-toast-swipe-y,var(--sonner-swipe-y,0px))) translateX(var(--scrollsheet-toast-swipe-x,var(--sonner-swipe-x,0px)));opacity:1}to{transform:var(--scrollsheet-toast-y) translateY(calc(var(--scrollsheet-toast-swipe-y,var(--sonner-swipe-y,0px)) + var(--scrollsheet-toast-swipe-out-y,var(--sonner-swipe-out-y,0)) * 100%)) translateX(calc(var(--scrollsheet-toast-swipe-x,var(--sonner-swipe-x,0px)) + var(--scrollsheet-toast-swipe-out-x,var(--sonner-swipe-out-x,0)) * 100%));opacity:0}}@media (prefers-reduced-motion:reduce){[data-scrollsheet-toaster],.scrollsheet-toast{transition:none!important;animation:none!important}}@media (max-width:600px){[data-scrollsheet-toaster]{right:var(--scrollsheet-toast-mobile-offset,var(--sonner-mobile-offset,16px));left:var(--scrollsheet-toast-mobile-offset,var(--sonner-mobile-offset,16px));width:100%}[data-scrollsheet-toaster] .scrollsheet-toast{width:calc(100% - var(--scrollsheet-toast-mobile-offset,var(--sonner-mobile-offset,16px)) * 2);left:0;right:0}[data-scrollsheet-toaster][data-x-position=left]{left:var(--scrollsheet-toast-mobile-offset,var(--sonner-mobile-offset,16px))}[data-scrollsheet-toaster][data-y-position=bottom]{bottom:var(--scrollsheet-toast-mobile-offset,var(--sonner-mobile-offset,16px))}[data-scrollsheet-toaster][data-y-position=top]{top:var(--scrollsheet-toast-mobile-offset,var(--sonner-mobile-offset,16px))}[data-scrollsheet-toaster][data-x-position=center]{left:var(--scrollsheet-toast-mobile-offset,var(--sonner-mobile-offset,16px));right:var(--scrollsheet-toast-mobile-offset,var(--sonner-mobile-offset,16px));transform:none}}`, "data-sonner-toast-styles");
function injectToastStyles(nonce) {
injectDocument(nonce);
}
function injectToastStylesInto(root, nonce) {
injectShadowRoot(root, nonce);
}
//#endregion
//#region packages/scrollsheet/src/toast/toaster.tsx
function resolveVisibleToasts(count) {
if (count === void 0) return 3;
return Math.max(1, Math.floor(count));
}
function useMountedFlag() {
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => {
if (prefersReducedMotion()) {
setMounted(true);
return;
}
let raf2 = 0;
const raf1 = requestAnimationFrame(() => {
raf2 = requestAnimationFrame(() => setMounted(true));
});
return () => {
cancelAnimationFrame(raf1);
cancelAnimationFrame(raf2);
};
}, []);
return mounted;
}
function useIsDocumentHidden() {
const [hidden, setHidden] = React.useState(() => typeof document !== "undefined" && document.hidden);
React.useEffect(() => {
const onVisibilityChange = () => setHidden(document.hidden);
document.addEventListener("visibilitychange", onVisibilityChange);
return () => document.removeEventListener("visibilitychange", onVisibilityChange);
}, []);
return hidden;
}
const EMPTY_RECORDS = [];
const EMPTY_MAP = new Map();
function toIdMap(records) {
return new Map(records.map((r) => [r.id, r]));
}
function sameIds(a, b) {
if (a.size !== b.size) return false;
for (const id of a.keys()) if (!b.has(id)) return false;
return true;
}
function useToastExit(live, queued = EMPTY_RECORDS, exitMs = 220) {
const [exiting, setExiting] = React.useState(() => EMPTY_MAP);
const [prevLive, setPrevLive] = React.useState(() => toIdMap(live));
const timersRef = React.useRef(new Map());
const liveIds = new Set(live.map((r) => r.id));
let renderExiting = exiting;
if (!sameIds(prevLive, liveIds)) {
const queuedIds = new Set(queued.map((r) => r.id));
const justRemoved = [];
for (const [id, record] of prevLive) {
if (liveIds.has(id)) continue;
if (queuedIds.has(id)) continue;
justRemoved.push(record);
}
setPrevLive(toIdMap(live));
if (justRemoved.length > 0) {
const next = new Map(exiting);
for (const record of justRemoved) next.set(record.id, record);
renderExiting = next;
setExiting(next);
}
}
React.useEffect(() => {
const reduced = prefersReducedMotion();
for (const [id] of exiting) {
if (timersRef.current.has(id)) continue;
const timer = setTimeout(() => {
timersRef.current.delete(id);
setExiting((prev) => {
if (!prev.has(id)) return prev;
const next = new Map(prev);
next.delete(id);
return next;
});
}, reduced ? 0 : exitMs);
timersRef.current.set(id, timer);
}
for (const [id, timer] of timersRef.current) if (!exiting.has(id)) {
clearTimeout(timer);
timersRef.current.delete(id);
}
}, [exiting, exitMs]);
React.useEffect(() => {
const timers = timersRef.current;
return () => {
for (const timer of timers.values()) clearTimeout(timer);
timers.clear();
};
}, []);
const rows = live.map((record) => ({
record,
exiting: false
}));
for (const [id, record] of renderExiting) if (!liveIds.has(id)) rows.push({
record,
exiting: true
});
return rows;
}
//#endregion
//#region packages/scrollsheet/src/toast/shell/shell-selectors.ts
function splitPosition(position) {
const [y, x] = position.split("-");
return {
y,
x
};
}
function computePossiblePositions(defaultPosition, toasts) {
const seen = new Set([defaultPosition]);
for (const t of toasts) if (t.position) seen.add(t.position);
return [...seen];
}
function computeRowOffsets(newestFirst, heights, gap) {
let cumulativeHeight = 0;
return newestFirst.map((record, index) => {
const stackOffset = index * gap + cumulativeHeight;
cumulativeHeight += heights.get(record.id) ?? 0;
return {
id: record.id,
index,
toastsBefore: index,
stackOffset
};
});
}
function findNewestDismissible(toasts) {
for (let i = toasts.length - 1; i >= 0; i -= 1) {
const t = toasts[i];
if (t && t.dismissible !== false) return t;
}
}
const SWIPE_VELOCITY_THRESHOLD = .11;
function getDefaultSwipeDirections(position) {
const { y, x } = splitPosition(position);
const directions = [y];
if (x === "left" || x === "right") directions.push(x);
return directions;
}
function lockSwipeAxis(dx, dy) {
if (Math.abs(dx) <= 1 && Math.abs(dy) <= 1) return null;
return Math.abs(dx) > Math.abs(dy) ? "x" : "y";
}
function dampenSwipeDelta(delta) {
const dampened = delta * (1 / (1.5 + Math.abs(delta) / 20));
return Math.abs(dampened) < Math.abs(delta) ? dampened : delta;
}
function computeSwipeAxisAmount(axis, delta, directions) {
const negativeDir = axis === "x" ? "left" : "top";
const positiveDir = axis === "x" ? "right" : "bottom";
if (!(directions.includes(negativeDir) || directions.includes(positiveDir))) return 0;
return directions.includes(negativeDir) && delta < 0 || directions.includes(positiveDir) && delta > 0 ? delta : dampenSwipeDelta(delta);
}
function isSwipeReleaseAllowed(axis, amount, directions) {
if (axis === "x") return directions.includes(amount > 0 ? "right" : "left");
return directions.includes(amount > 0 ? "bottom" : "top");
}
function shouldDismissOnSwipeRelease(amount, velocity, thresholdPx = 45, velocityThreshold = SWIPE_VELOCITY_THRESHOLD) {
return Math.abs(amount) >= thresholdPx || velocity > velocityThreshold;
}
function resolveSwipeOutDirection(axis, amount) {
if (axis === "x") return amount > 0 ? "right" : "left";
return amount > 0 ? "down" : "up";
}
//#endregion
//#region packages/scrollsheet/src/toast/shell/use-toast-swipe.ts
const ZERO_AMOUNT = {
x: 0,
y: 0
};
function useToastSwipe(elRef, enabled, directions, onSwipeDismiss) {
const draggingRef = React.useRef(false);
const axisRef = React.useRef(null);
const startRef = React.useRef(ZERO_AMOUNT);
const amountRef = React.useRef(ZERO_AMOUNT);
const dragStartRef = React.useRef(0);
const onSwipeDismissRef = React.useRef(onSwipeDismiss);
onSwipeDismissRef.current = onSwipeDismiss;
const onPointerDown = React.useCallback((event) => {
if (!enabled || event.button !== 0) return;
if (event.target.tagName === "BUTTON") return;
const el = elRef.current;
if (!el) return;
draggingRef.current = true;
axisRef.current = null;
amountRef.current = ZERO_AMOUNT;
startRef.current = {
x: event.clientX,
y: event.clientY
};
dragStartRef.current = Date.now();
el.setPointerCapture(event.pointerId);
el.setAttribute("data-swiping", "true");
}, [enabled, elRef]);
const onPointerMove = React.useCallback((event) => {
if (!draggingRef.current) return;
const el = elRef.current;
if (!el) return;
if ((window.getSelection?.()?.toString().length ?? 0) > 0) return;
const start = startRef.current;
const dx = event.clientX - start.x;
const dy = event.clientY - start.y;
if (axisRef.current === null) axisRef.current = lockSwipeAxis(dx, dy);
const axis = axisRef.current;
if (axis === null) return;
const resolved = computeSwipeAxisAmount(axis, axis === "x" ? dx : dy, directions);
const amount = axis === "x" ? {
x: resolved,
y: 0
} : {
x: 0,
y: resolved
};
amountRef.current = amount;
if (resolved !== 0) el.setAttribute("data-swiped", "true");
el.style.setProperty("--scrollsheet-toast-swipe-x", `${amount.x}px`);
el.style.setProperty("--scrollsheet-toast-swipe-y", `${amount.y}px`);
}, [elRef, directions]);
const release = React.useCallback(() => {
if (!draggingRef.current) return;
draggingRef.current = false;
const el = elRef.current;
const axis = axisRef.current;
axisRef.current = null;
if (!el) return;
if (axis === null) {
el.setAttribute("data-swiping", "false");
return;
}
const amount = axis === "x" ? amountRef.current.x : amountRef.current.y;
const elapsed = Math.max(1, Date.now() - dragStartRef.current);
const velocity = Math.abs(amount) / elapsed;
if (isSwipeReleaseAllowed(axis, amount, directions) && shouldDismissOnSwipeRelease(amount, velocity)) {
const direction = resolveSwipeOutDirection(axis, amount);
el.setAttribute("data-swipe-direction", direction);
if (prefersReducedMotion()) {
el.setAttribute("data-swipe-out", "true");
onSwipeDismissRef.current();
return;
}
const onAnimEnd = () => {
el.removeEventListener("animationend", onAnimEnd);
onSwipeDismissRef.current();
};
el.addEventListener("animationend", onAnimEnd);
el.setAttribute("data-swipe-out", "true");
return;
}
el.setAttribute("data-swiping", "false");
el.setAttribute("data-swiped", "false");
el.style.setProperty("--scrollsheet-toast-swipe-x", "0px");
el.style.setProperty("--scrollsheet-toast-swipe-y", "0px");
}, [elRef, directions]);
return {
onPointerDown,
onPointerMove,
onPointerUp: React.useCallback((event) => {
if (elRef.current?.hasPointerCapture(event.pointerId)) elRef.current.releasePointerCapture(event.pointerId);
release();
}, [elRef, release]),
onPointerCancel: React.useCallback(() => release(), [release])
};
}
//#endregion
//#region packages/scrollsheet/src/toast/shell/toast-row.tsx
function ToastIcon({ type, icons, spinnerClassName }) {
if (type === "loading") {
if (icons?.loading) return jsx(Fragment, { children: icons.loading });
return jsx("span", { className: spinnerClassName });
}
switch (type) {
case "success": return jsx(Fragment, { children: icons?.success ?? "✓" });
case "error": return jsx(Fragment, { children: icons?.error ?? "✕" });
case "warning": return jsx(Fragment, { children: icons?.warning ?? "!" });
case "info": return jsx(Fragment, { children: icons?.info ?? "i" });
default: return null;
}
}
function ToastRow({ record, index, total, toastsBefore, stackOffset, height, frontHeight, visible, expanded, removed, yPosition, xPosition, closeButton, directions, onDismiss, observe, toasterClassNames, icons, toasterCloseButtonAriaLabel }) {
const role = record.type === "error" ? "alert" : "status";
const mounted = useMountedFlag();
const elRef = React.useRef(null);
const dismissible = record.dismissible !== false;
const isFront = index === 0;
const setRefs = React.useCallback((el) => {
elRef.current = el;
observe(record.id, el);
}, [observe, record.id]);
const swipeHandlers = useToastSwipe(elRef, !removed && dismissible && record.type !== "loading", directions, () => onDismiss(record));
React.useEffect(() => {
const el = elRef.current;
if (el) el.inert = removed;
}, [removed]);
const style = {
"--scrollsheet-toast-toasts-before": toastsBefore,
"--scrollsheet-toast-stack-offset": `${stackOffset}px`,
"--scrollsheet-toast-front-height": frontHeight !== void 0 ? `${frontHeight}px` : "0px",
"--scrollsheet-toast-initial-height": height !== void 0 ? `${height}px` : "auto",
zIndex: Math.max(0, total - index)
};
const rootClassName = cn("scrollsheet-toast", "sonner-toast", record.className, toasterClassNames?.toast, record.classNames?.toast, toasterClassNames?.default, toasterClassNames?.[record.type], record.classNames?.[record.type]);
const motionAttrs = {
"data-mounted": mounted ? "" : void 0,
"data-removed": removed ? "" : void 0,
"data-visible": visible ? "true" : "false",
"data-front": isFront ? "true" : "false",
"data-expanded": expanded ? "true" : "false",
"data-y-position": yPosition,
"data-x-position": xPosition,
"data-index": index,
"data-dismissible": dismissible ? "true" : "false",
"data-swiping": "false",
"data-swiped": "false",
"aria-hidden": removed ? "true" : void 0
};
if (record.jsx !== void 0) return jsx("div", {
ref: setRefs,
className: rootClassName,
"data-scrollsheet-toast": "",
"data-sonner-toast": "",
"data-scrollsheet-custom": "",
"data-sonner-custom": "",
"data-testid": record.testId,
role,
style,
...motionAttrs,
...swipeHandlers,
children: record.jsx
});
const showCloseButton = closeButton && dismissible && record.type !== "loading";
const hasActions = Boolean(record.action || record.cancel || showCloseButton);
const closeButtonAriaLabel = record.closeButtonAriaLabel ?? toasterCloseButtonAriaLabel ?? "Close toast";
return jsxs("div", {
ref: setRefs,
className: rootClassName,
"data-scrollsheet-toast": "",
"data-sonner-toast": "",
"data-type": record.type,
"data-testid": record.testId,
role,
style,
...motionAttrs,
...swipeHandlers,
children: [
jsx("span", {
className: cn("scrollsheet-toast-icon", "sonner-toast-icon", toasterClassNames?.icon, record.classNames?.icon),
"data-type": record.type,
"aria-hidden": "true",
children: record.icon ?? jsx(ToastIcon, {
type: record.type,
icons,
spinnerClassName: cn("scrollsheet-toast-spinner", "sonner-toast-spinner", toasterClassNames?.loader, record.classNames?.loader)
})
}),
jsxs("div", {
className: cn("scrollsheet-toast-body", "sonner-toast-body", toasterClassNames?.content, record.classNames?.content),
children: [record.title !== void 0 && jsx("div", {
className: cn("scrollsheet-toast-title", "sonner-toast-title", toasterClassNames?.title, record.classNames?.title),
children: record.title
}), record.description !== void 0 && jsx("div", {
className: cn("scrollsheet-toast-description", "sonner-toast-description", toasterClassNames?.description, record.classNames?.description),
children: record.description
})]
}),
hasActions && jsxs("div", {
className: "scrollsheet-toast-actions sonner-toast-actions",
children: [
record.cancel && jsx("button", {
type: "button",
className: cn("scrollsheet-toast-cancel", "sonner-toast-cancel", toasterClassNames?.cancelButton, record.classNames?.cancelButton),
onClick: (event) => {
if (!dismissible) return;
record.cancel?.onClick(event);
onDismiss(record);
},
children: record.cancel.label
}),
record.action && jsx("button", {
type: "button",
className: cn("scrollsheet-toast-action", "sonner-toast-action", toasterClassNames?.actionButton, record.classNames?.actionButton),
onClick: (event) => {
record.action?.onClick(event);
if (event.defaultPrevented) return;
onDismiss(record);
},
children: record.action.label
}),
showCloseButton && jsx("button", {
type: "button",
className: cn("scrollsheet-toast-close", "sonner-toast-close", toasterClassNames?.closeButton, record.classNames?.closeButton),
"aria-label": closeButtonAriaLabel,
onClick: () => onDismiss(record),
children: icons?.close ?? "×"
})
]
})
]
});
}
//#endregion
//#region packages/scrollsheet/src/toast/shell/use-toast-heights.ts
function useToastHeights() {
const [heights, setHeights] = React.useState(() => new Map());
const observersRef = React.useRef(new Map());
const setHeight = React.useCallback((id, height) => {
setHeights((prev) => prev.get(id) === height ? prev : new Map(prev).set(id, height));
}, []);
const forget = React.useCallback((id) => {
observersRef.current.get(id)?.disconnect();
observersRef.current.delete(id);
setHeights((prev) => {
if (!prev.has(id)) return prev;
const next = new Map(prev);
next.delete(id);
return next;
});
}, []);
const observe = React.useCallback((id, el) => {
const observers = observersRef.current;
observers.get(id)?.disconnect();
observers.delete(id);
if (!el || typeof ResizeObserver === "undefined") return;
const ro = new ResizeObserver(() => setHeight(id, el.getBoundingClientRect().height));
ro.observe(el);
observers.set(id, ro);
setHeight(id, el.getBoundingClientRect().height);
}, [setHeight]);
React.useEffect(() => {
const observers = observersRef.current;
return () => {
for (const ro of observers.values()) ro.disconnect();
observers.clear();
};
}, []);
return {
heights,
observe,
forget
};
}
//#endregion
//#region packages/scrollsheet/src/toast/shell/toaster-shell.tsx
const DEFAULT_HOTKEY = ["altKey", "KeyT"];
function PositionGroup({ position, defaultPosition, toasts, visibleToasts, gap, expanded, closeButton, swipeDirections, toastOptions, icons, className, baseStyle, registerListEl, dismissRow, onHoverEnter, onHoverLeave, onInteractingChange }) {
const directions = React.useMemo(() => swipeDirections ?? getDefaultSwipeDirections(position), [swipeDirections, position]);
const positionToasts = React.useMemo(() => selectPositionToasts(toasts, position, defaultPosition), [
toasts,
position,
defaultPosition
]);
const newestFirst = React.useMemo(() => [...positionToasts].reverse(), [positionToasts]);
const window_ = React.useMemo(() => selectToastWindow(positionToasts, visibleToasts), [positionToasts, visibleToasts]);
const visibleIds = React.useMemo(() => new Set(window_.visible.map((t) => t.id)), [window_]);
const { heights, observe, forget } = useToastHeights();
const exitRows = useToastExit(newestFirst);
const exitingIds = React.useMemo(() => new Set(exitRows.filter((r) => r.exiting).map((r) => r.record.id)), [exitRows]);
React.useEffect(() => {
for (const id of exitingIds) forget(id);
}, [exitingIds, forget]);
const offsets = React.useMemo(() => computeRowOffsets(newestFirst, heights, gap), [
newestFirst,
heights,
gap
]);
const offsetById = React.useMemo(() => new Map(offsets.map((o) => [o.id, o])), [offsets]);
const lastOffsetRef = React.useRef(new Map());
for (const o of offsets) lastOffsetRef.current.set(o.id, o);
if (lastOffsetRef.current.size > offsets.length) {
const keep = new Set([...offsets.map((o) => o.id), ...exitingIds]);
for (const id of [...lastOffsetRef.current.keys()]) if (!keep.has(id)) lastOffsetRef.current.delete(id);
}
const frontHeight = heights.get(newestFirst[0]?.id ?? "");
const { y, x } = splitPosition(position);
const listRef = React.useCallback((el) => registerListEl(position, el), [registerListEl, position]);
if (exitRows.length === 0) return null;
return jsx("ol", {
ref: listRef,
"data-scrollsheet-toaster": "",
"data-sonner-toaster": "",
"data-scrollsheet-theme": "light",
"data-sonner-theme": "light",
"data-y-position": y,
"data-x-position": x,
tabIndex: -1,
className,
style: {
...baseStyle,
"--scrollsheet-toast-gap": `${gap}px`,
"--scrollsheet-toast-front-height": frontHeight !== void 0 ? `${frontHeight}px` : void 0
},
onMouseEnter: onHoverEnter,
onMouseMove: onHoverEnter,
onMouseLeave: onHoverLeave,
onPointerDown: (event) => {
if (event.target.dataset.dismissible === "false") return;
onInteractingChange(true);
},
onPointerUp: () => onInteractingChange(false),
children: exitRows.map(({ record, exiting }) => {
const offset = offsetById.get(record.id) ?? lastOffsetRef.current.get(record.id);
const index = offset?.index ?? newestFirst.length;
return jsx(ToastRow, {
record,
index,
total: newestFirst.length,
toastsBefore: offset?.toastsBefore ?? index,
stackOffset: offset?.stackOffset ?? 0,
height: heights.get(record.id),
frontHeight,
visible: visibleIds.has(record.id),
expanded,
removed: exiting,
yPosition: y,
xPosition: x,
closeButton,
directions,
onDismiss: dismissRow,
observe,
toasterClassNames: toastOptions?.classNames,
icons,
toasterCloseButtonAriaLabel: toastOptions?.closeButtonAriaLabel
}, record.id);
})
});
}
function resolveShellPosition(position) {
return position ?? "bottom-right";
}
function ToasterShell({ id, toasterId, position, theme, richColors, expand: forceExpand, visibleToasts: visibleToastsProp, closeButton = false, duration: durationProp, gap = 14, offset, swipeDirections, toastOptions, icons, className, style, containerAriaLabel = "Notifications", hotkey = DEFAULT_HOTKEY, nonce }) {
const resolvedId = id ?? toasterId;
if (toasterId !== void 0) warnOnce("toaster-id-deprecated", "[scrollsheet Toaster] Toaster's \"toasterId\" prop is deprecated — use \"id\" instead.");
const { toasts: allToasts } = useSonner();
const toasts = React.useMemo(() => selectToasterToasts(allToasts, resolvedId), [allToasts, resolvedId]);
const defaultPosition = resolveShellPosition(position);
const possiblePositions = React.useMemo(() => computePossiblePositions(defaultPosition, toasts), [defaultPosition, toasts]);
const visibleToasts = resolveVisibleToasts(visibleToastsProp);
const defaultDuration = durationProp ?? toastOptions?.duration ?? 4e3;
if (theme !== void 0 && theme !== "light") warnOnce("shell-theme", `[scrollsheet Toaster] theme="${theme}" isn't implemented yet — v1 always renders the static light card real Sonner ships by default.`);
if (richColors) warnOnce("shell-rich-colors", "[scrollsheet Toaster] richColors isn't implemented yet — toasts render with their default icon color only.");
React.useEffect(() => {
injectToastStyles(nonce);
}, [nonce]);
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => setMounted(true), []);
const [hovered, setHovered] = React.useState(false);
const [hotkeyExpanded, setHotkeyExpanded] = React.useState(false);
const [interacting, setInteracting] = React.useState(false);
const expanded = Boolean(forceExpand) || hovered || hotkeyExpanded;
const isDocumentHidden = useIsDocumentHidden();
const handleHoverEnter = React.useCallback(() => setHovered(true), []);
const handleHoverLeave = React.useCallback(() => {
if (!interacting) setHovered(false);
}, [interacting]);
const dismissRow = React.useCallback((record) => {
toast.dismiss(record.id);
}, []);
const listElsRef = React.useRef(new Map());
const registerListEl = React.useCallback((pos, el) => {
if (el) listElsRef.current.set(pos, el);
else listElsRef.current.delete(pos);
}, []);
React.useEffect(() => {
if (hotkey.length === 0) return;
const handleKeyDown = (event) => {
if (hotkey.every((key) => event[key] || event.code === key)) {
setHotkeyExpanded(true);
const [firstList] = listElsRef.current.values();
firstList?.focus({ preventScroll: true });
}
if (event.code !== "Escape") return;
const active = document.activeElement;
const focusedEntry = [...listElsRef.current.entries()].find(([, el]) => active === el || el.contains(active));
if (!focusedEntry) return;
if (hotkeyExpanded) {
setHotkeyExpanded(false);
return;
}
const [focusedPosition] = focusedEntry;
const positionToasts = selectPositionToasts(toasts, focusedPosition, defaultPosition);
const front = positionToasts[positionToasts.length - 1];
if (front) dismissRow(front);
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [
hotkey,
hotkeyExpanded,
toasts,
defaultPosition,
dismissRow
]);
const timersRef = React.useRef(new Map());
React.useEffect(() => {
const timers = timersRef.current;
const liveIds = new Set(toasts.map((t) => t.id));
for (const [id, entry] of timers) {
if (liveIds.has(id)) continue;
if (entry.timer) clearTimeout(entry.timer);
timers.delete(id);
}
const paused = expanded || interacting || isDocumentHidden;
for (const t of toasts) {
if (t.type === "loading") {
const stale = timers.get(t.id);
if (stale) {
if (stale.timer) clearTimeout(stale.timer);
timers.delete(t.id);
}
continue;
}
const ms = t.duration ?? toastOptions?.duration ?? defaultDuration;
const existing = timers.get(t.id);
if (!existing || existing.record !== t) {
if (existing?.timer) clearTimeout(existing.timer);
const entry = {
timer: null,
startedAt: 0,
remaining: ms,
record: t
};
timers.set(t.id, entry);
if (Number.isFinite(ms) && !paused) {
entry.startedAt = Date.now();
entry.timer = setTimeout(() => {
timersRef.current.delete(t.id);
expire(t.id);
}, ms);
}
continue;
}
if (paused) {
if (existing.timer) {
clearTimeout(existing.timer);
const elapsed = Date.now() - existing.startedAt;
existing.remaining = Math.max(0, existing.remaining - elapsed);
existing.timer = null;
}
continue;
}
if (existing.timer || !Number.isFinite(existing.remaining)) continue;
existing.startedAt = Date.now();
existing.timer = setTimeout(() => {
timersRef.current.delete(t.id);
expire(t.id);
}, existing.remaining);
}
}, [
toasts,
expanded,
interacting,
isDocumentHidden,
defaultDuration,
toastOptions?.duration
]);
React.useEffect(() => {
const timers = timersRef.current;
return () => {
for (const entry of timers.values()) if (entry.timer) clearTimeout(entry.timer);
timers.clear();
};
}, []);
useCloseWatcher({
present: toasts.length > 0,
nonModal: true,
escapeDismissible: true,
onClose: () => {
const target = findNewestDismissible(toasts);
if (target) dismissRow(target);
}
});
if (!mounted || typeof document === "undefined") return null;
const offsetValue = offset !== void 0 ? typeof offset === "number" ? `${offset}px` : offset : void 0;
const baseStyle = {
...style,
"--scrollsheet-toast-offset": offsetValue
};
return createPortal(jsx("section", {
"aria-label": containerAriaLabel,
tabIndex: -1,
"aria-live": "polite",
"aria-relevant": "additions text",
"aria-atomic": "false",
suppressHydrationWarning: true,
"data-react-aria-top-layer": "",
children: possiblePositions.map((groupPosition) => jsx(PositionGroup, {
position: groupPosition,
defaultPosition,
toasts,
visibleToasts,
gap,
expanded,
closeButton,
swipeDirections,
toastOptions,
icons,
className,
baseStyle,
registerListEl,
dismissRow,
onHoverEnter: handleHoverEnter,
onHoverLeave: handleHoverLeave,
onInteractingChange: setInteracting
}, groupPosition))
}), document.body);
}
//#endregion
export { useSonner as a, toast as i, resolveVisibleToasts as n, useToasts as o, injectToastStylesInto as r, ToasterShell as t };

Sorry, the diff of this file is too big to display

import * as React from "react";
//#region packages/scrollsheet/src/trigger.d.ts
interface SheetTriggerProps extends React.ComponentProps<"button"> {
/**
* Render the child element instead of a `<button>`, merging the trigger's
* props (aria, onClick) into it — for custom button components or links.
*/
asChild?: boolean;
}
declare const Trigger: React.ForwardRefExoticComponent<Omit<SheetTriggerProps, "ref"> & React.RefAttributes<HTMLButtonElement>>;
//#endregion
//#region packages/scrollsheet/src/internal/detents.d.ts
/**
* Detents — the stops a sheet can rest at, in the spirit of
* UISheetPresentationController's `detents`.
*
* A detent resolves to the visible height of the sheet in px.
* Accepted forms:
* - 'full' → viewport height minus top inset
* - 'medium' → 50% of viewport
* - 'content' → natural content height (measured, capped at full)
* - number in (0, 1] → fraction of viewport
* - `${number}px` → absolute pixels
*/
type DetentSpec = "full" | "medium" | "content" | number | `${number}px`;
//#endregion
//#region packages/scrollsheet/src/content.d.ts
interface SheetContentProps extends React.ComponentProps<"div"> {
/** Accessible name when no <Sheet.Title> is rendered. */
"aria-label"?: string;
/**
* Stretches [data-scrollsheet-body] to fill the panel (flex column, the
* body itself flex:1/min-height:0) instead of sizing to its natural content
* height. Promotes the docs "Full-height content" recipe into the library —
* write your own inner-scroll child (flex:1, overflow-y:auto) with no CSS
* of your own required on the wrapper.
* @default false
*/
fill?: boolean;
/**
* Render the child element instead of scrollsheet's own panel `<div>`,
* merging the panel's props (role, tabIndex, ref, `data-scrollsheet-*`)
* into it — matches Radix `Dialog.Content asChild`: the child replaces the
* panel outright rather than nesting inside it. The child must be a single
* non-Fragment element; whatever it was given as its own children is
* hoisted into the body wrapper below (detent measurement and stacking
* still need a real DOM descendant there regardless of which element ends
* up as the panel).
* If `children` isn't a single non-Fragment element (missing, a string,
* multiple elements, a `<>...</>` Fragment…), `asChild` is ignored for
* that render — the default panel `<div>` is used instead, with a
* one-time dev warning.
* @default false
*/
asChild?: boolean;
}
declare const Content$1: React.ForwardRefExoticComponent<Omit<SheetContentProps, "ref"> & React.RefAttributes<HTMLDivElement>>;
//#endregion
//#region packages/scrollsheet/src/misc.d.ts
interface SheetTitleProps extends React.ComponentProps<"h2"> {
/** Render the child element instead of an `<h2>`, merging props into it. */
asChild?: boolean;
}
declare const Title: React.ForwardRefExoticComponent<Omit<SheetTitleProps, "ref"> & React.RefAttributes<HTMLHeadingElement>>;
interface SheetDescriptionProps extends React.ComponentProps<"p"> {
/** Render the child element instead of a `<p>`, merging props into it. */
asChild?: boolean;
}
declare const Description: React.ForwardRefExoticComponent<Omit<SheetDescriptionProps, "ref"> & React.RefAttributes<HTMLParagraphElement>>;
interface SheetCloseProps extends React.ComponentProps<"button"> {
/** Render the child element instead of a `<button>`, merging props into it. */
asChild?: boolean;
}
declare const Close: React.ForwardRefExoticComponent<Omit<SheetCloseProps, "ref"> & React.RefAttributes<HTMLButtonElement>>;
//#endregion
//#region packages/scrollsheet/src/handle.d.ts
interface SheetHandleProps extends React.ComponentProps<"button"> {
/** Render the child element instead of a `<button>`, merging props into it. */
asChild?: boolean;
/**
* Where the pill sits. `'inside'` (default) flows at the top of the
* sheet's content. `'floating'` overlays the content — absolutely
* positioned over the panel's top edge, for full-bleed content (maps,
* photos) a flow pill would push down. `'outside'` floats in the backdrop
* above the sheet's top edge (bottom sheets only; other sides render as
* `'inside'`) — it rides the canvas layer outside the panel's clip and
* drags, clicks, and keys exactly like the others.
* @default 'inside'
*/
variant?: "inside" | "floating" | "outside";
}
/**
* The grabber pill. Click cycles detents; ArrowUp/ArrowDown (or Left/Right
* for side sheets) move between them, Home/End jump to the first/last — so
* multi-detent sheets are fully keyboard-operable. With two or more detents
* it exposes itself as a slider (role, value min/max/now, value text) so
* screen readers announce the current stop, not just "button".
*
* The handle is optional. It's purely a click/keyboard affordance layered on
* top of the drag engine, which listens on the whole panel, not the handle —
* omit `<Sheet.Handle>` and the whole panel still drags and dismisses, in
* every mode (modal and non-modal) and every side (bottom/top/left/right).
* There's no boolean prop for this; omission is the API.
*/
declare const Handle$1: React.ForwardRefExoticComponent<Omit<SheetHandleProps, "ref"> & React.RefAttributes<HTMLButtonElement>>;
//#endregion
//#region packages/scrollsheet/src/drawer/index.d.ts
/** vaul's snap point shape: a 0–1 fraction, an absolute `'###px'` string, or `'fit-content'`. */
type VaulSnapPoint = number | string;
/**
* Resolves vaul's `fadeFromIndex` against `snapPoints` into a
* `largestUndimmedDetent` spec, mirroring real vaul's own default (its
* `src/index.tsx`: `fadeFromIndex = snapPoints && snapPoints.length - 1`) —
* omitted with `snapPoints` set defaults to the *topmost* snap point index
* (no dim until the last snap point), not "no undimmed range at all"
* (scrollsheet's own default when `largestUndimmedDetent` is never touched).
* No `snapPoints`, or an out-of-range explicit index, resolves to
* `undefined` (today's full-dim-range behavior) — pulled out as its own pure
* function so this index math is unit-testable independent of rendering.
*/
declare function resolveFadeFromIndex(snapPoints: readonly VaulSnapPoint[] | undefined, fadeFromIndex: number | undefined): DetentSpec | undefined;
/**
* Resolves vaul's `closeThreshold` against whether `snapPoints` is set,
* CONVERTING between opposite conventions: vaul counts the fraction dragged
* AWAY (its 0.25 default = dismiss after a quarter-height drag), scrollsheet
* counts the fraction still VISIBLE (`isBelowCloseThreshold`: dismiss when
* `revealed < firstDetent * closeThreshold`, so higher = easier). A raw
* passthrough inverts the migrated feel — vaul's 0.25 became "drag 75% to
* dismiss", harder than scrollsheet's own 0.5 default instead of easier —
* so the mapping is `1 - value`, and an omitted prop resolves to vaul's own
* `CLOSE_THRESHOLD` default of 0.25 (=> 0.75 here), never `Sheet.Root`'s
* unrelated 0.5.
* `snapPoints` set makes `closeThreshold` dead code in real vaul (its
* `onRelease` returns through the snap-points branch before ever reading
* it), matched here by always resolving to `undefined` in that case.
* Pulled out as its own pure function, mirroring `resolveFadeFromIndex`,
* so the conversion math is unit-testable independent of rendering.
*/
declare function resolveCloseThreshold(snapPoints: readonly VaulSnapPoint[] | undefined, closeThreshold: number | undefined): number | undefined;
/**
* Composes vaul's `onClose` on top of `onOpenChange`: `onClose` fires
* whenever the transition target is `false`, then `onOpenChange` always
* fires. Real vaul's `closeDrawer()` calls `onClose` from every dismiss
* path (swipe past threshold, Esc, backdrop, imperative); scrollsheet's own
* `onOpenChange` is already the single funnel every one of *its* dismiss
* paths goes through (`useControllableState`'s `onChange`, which fires
* exactly once per real open/false transition), so layering `onClose` on
* top of that wiring reproduces "every dismiss path" without a second one.
* Returns `undefined` when neither callback is given, matching the
* conditional-wrapper convention `onRelease`'s cast uses below. Pulled out
* as its own pure function so the composition is unit-testable without a
* live DOM/click event.
*/
declare function composeOpenChange(onOpenChange: ((open: boolean) => void) | undefined, onClose: (() => void) | undefined): ((open: boolean) => void) | undefined;
interface DrawerRootProps {
children?: React.ReactNode;
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
/**
* Fires whenever the drawer closes — every dismiss path (swipe past
* threshold, Esc, backdrop tap, imperative `close()`) funnels through the
* same `onOpenChange` wiring this is layered on top of, matching real
* vaul's `closeDrawer()`, which calls `onClose` on every one of those
* paths too.
*/
onClose?: () => void;
/** @default true */
dismissible?: boolean;
/** Fractions (0–1), `'###px'` strings, or `'fit-content'` — translated to scrollsheet `detents`. */
snapPoints?: readonly VaulSnapPoint[];
/** Optionally-controlled active snap point; `null` means "no explicit snap point". */
activeSnapPoint?: VaulSnapPoint | null;
setActiveSnapPoint?: (snapPoint: VaulSnapPoint | null) => void;
/** Fires when the open/close transition actually completes — wired to `onOpenChangeComplete`, exact rather than vaul's timer. */
onAnimationEnd?: (open: boolean) => void;
/** Ignored — the native `<dialog>` manages focus itself. */
autoFocus?: boolean;
/** Ignored — nesting is automatic: a `<Drawer.Content>` rendered inside another registers itself. */
nested?: boolean;
/** Maps to scrollsheet `side` — all four edges. */
direction?: "top" | "bottom" | "left" | "right";
/** Maps to scrollsheet `modal` — `false` renders non-modal (Popover top layer, page stays interactive). */
modal?: boolean;
/**
* Maps to `backgroundEffect="scale"`; vaul's own `[data-vaul-drawer-wrapper]`
* is picked up as the target. Left false (vaul's default), this maps to
* `'none'`, not unset — vaul never scales the page unless asked, so the
* scrollsheet default for full-height sheets must not leak in here.
*/
shouldScaleBackground?: boolean;
/**
* Maps to scrollsheet `closeThreshold` — but only when `snapPoints` is
* unset. In real vaul, `closeThreshold` is dead code once `snapPoints`
* exist (its `onRelease` returns through the snap-points branch before
* ever reading it); this compat layer matches that rather than giving the
* prop unconditional live effect. Passing both together warns once in dev
* — use the native `Sheet.Root closeThreshold` directly if you want it to
* combine with detents.
*
* Omitted (with `snapPoints` unset) resolves to vaul's own default of
* `0.25` (`CLOSE_THRESHOLD` in vaul's `src/constants.ts`) — not
* scrollsheet's own `0.5` default, which is twice the drag distance.
*/
closeThreshold?: number;
/**
* Maps to `largestUndimmedDetent`, resolved against `snapPoints` at this
* index. Omitted (with `snapPoints` set) defaults to `snapPoints.length -
* 1` — vaul's own default, meaning no dim until the topmost snap point —
* not "dim across the full range" (scrollsheet's own default when
* `largestUndimmedDetent` is never touched at all).
*/
fadeFromIndex?: number;
/** Maps to `sequentialDetents`. */
snapToSequentialPoint?: boolean;
/** Maps to `handleOnly` directly. */
handleOnly?: boolean;
/**
* Maps to `onRelease`. Typed as vaul's own `React.PointerEvent<HTMLDivElement>`
* (rather than scrollsheet's wider native-`PointerEvent` type) so a
* handler already typed against vaul's declaration still assigns cleanly —
* see the internal cast in `Root` for why this is safe at runtime.
*/
onRelease?: (event: React.PointerEvent<HTMLDivElement>, open: boolean) => void;
setBackgroundColorOnScale?: boolean;
noBodyStyles?: boolean;
disablePreventScroll?: boolean;
preventScrollRestoration?: boolean;
repositionInputs?: boolean;
scrollLockTimeout?: number;
onDrag?: (event: React.PointerEvent, percentageDragged: number) => void;
container?: HTMLElement | null;
/**
* Ignored — real vaul's `fixed` swaps its own keyboard-avoidance strategy
* (resize instead of translate); scrollsheet's `useKeyboardViewport` has
* no equivalent toggle.
*/
fixed?: boolean;
}
declare function Root(props: DrawerRootProps): React.JSX.Element;
/**
* Nested drawers just work: a `<Drawer.Content>` rendered inside another
* `<Drawer.Content>` automatically registers with the parent's stacking
* context (the parent recedes while the child is open), the way iOS stacks
* sheets. `NestedRoot` is an alias of `Root` kept for drop-in compatibility
* with vaul's API surface — there's nothing distinct for it to do.
*/
declare const NestedRoot: typeof Root;
type DrawerNestedRootProps = DrawerRootProps;
interface DrawerPortalProps {
children?: React.ReactNode;
/** Ignored — the native `<dialog>` always renders into the browser's top layer. */
container?: HTMLElement | null;
}
declare function Portal({ children, container }: DrawerPortalProps): React.JSX.Element;
type DrawerOverlayProps = React.ComponentProps<"div">;
/**
* scrollsheet draws its own backdrop (`.scrollsheet-backdrop`, themeable via
* the `--scrollsheet-backdrop` CSS variable) that tracks scroll progress, so
* there's nothing for a separate overlay element to render. This accepts
* vaul's `<Drawer.Overlay className .../>` so existing JSX doesn't crash —
* restyle the backdrop through the CSS variable instead.
*/
declare function Overlay(_props: DrawerOverlayProps): null;
/**
* Whether translated `snapPoints` produced more than one distinct detent —
* feeds Content's `data-vaul-snap-points` attribute. vaul's own
* CSS-selector convention treats a single snap point the same as none
* (scrollsheet's own single-detent default also resolves to `false` here).
* Pulled out as its own pure function, mirroring `resolveFadeFromIndex` /
* `resolveCloseThreshold`, so it's unit-testable independent of rendering
* (`<Drawer.Content>`'s actual DOM output is behind a client-only mount
* gate — see content.tsx — so SSR can't observe the attribute directly).
*/
declare function hasMultipleSnapPoints(detents: readonly DetentSpec[]): boolean;
interface DrawerContentProps extends SheetContentProps {
onPointerDownOutside?: (event: CustomEvent) => void;
onOpenAutoFocus?: (event: Event) => void;
onEscapeKeyDown?: (event: KeyboardEvent) => void;
onCloseAutoFocus?: (event: Event) => void;
onInteractOutside?: (event: CustomEvent) => void;
onFocusOutside?: (event: CustomEvent) => void;
forceMount?: boolean;
}
declare const Content: React.ForwardRefExoticComponent<Omit<DrawerContentProps, "ref"> & React.RefAttributes<HTMLDivElement>>;
/**
* Composes the click handler Handle passes to `SheetHandle`: the caller's
* own `onClick` always fires first, then — when `preventCycle` is set —
* `event.preventDefault()`, the same signal `SheetHandle`'s own
* click-to-cycle logic already checks (`if (event.defaultPrevented) return`
* in handle.tsx) to skip advancing to the next detent. Mirrors real vaul's
* `preventCycle`, read inside its own `handleCycleSnapPoints` guard. Pulled
* out as its own pure function so the composition is unit-testable without
* a live DOM/click event.
*/
declare function composeHandleClick(preventCycle: boolean | undefined, onClick: ((event: React.MouseEvent<HTMLButtonElement>) => void) | undefined): (event: React.MouseEvent<HTMLButtonElement>) => void;
interface DrawerHandleProps extends SheetHandleProps {
/**
* Suppresses the click-to-cycle-detents behavior — the handle still
* renders, drags, and is keyboard-operable as usual, but a click no
* longer advances to the next detent. Mirrors real vaul's own
* `preventCycle`, which guards the same click path (vaul's handle has no
* keyboard cycling to suppress).
*/
preventCycle?: boolean;
}
declare const Handle: React.ForwardRefExoticComponent<Omit<DrawerHandleProps, "ref"> & React.RefAttributes<HTMLButtonElement>>;
/**
* Sheet.Close's self-closing form renders a styled ✕ default; vaul's own
* `<Drawer.Close />` renders an empty unstyled button. A migrating user
* must not get a surprise icon button from a version swap, so the compat
* Close pins children to null (defined, so the styled-default branch never
* arms) unless the caller passed some.
*/
declare const DrawerClose: React.ForwardRefExoticComponent<Omit<SheetCloseProps, "ref"> & React.RefAttributes<HTMLButtonElement>>;
/** Namespace-style access matching vaul's `<Drawer.Root>…</Drawer.Root>` shape. */
declare const Drawer: {
Root: typeof Root;
NestedRoot: typeof Root;
Trigger: React.ForwardRefExoticComponent<Omit<SheetTriggerProps, "ref"> & React.RefAttributes<HTMLButtonElement>>;
Portal: typeof Portal;
Overlay: typeof Overlay;
Content: React.ForwardRefExoticComponent<Omit<DrawerContentProps, "ref"> & React.RefAttributes<HTMLDivElement>>;
Close: React.ForwardRefExoticComponent<Omit<SheetCloseProps, "ref"> & React.RefAttributes<HTMLButtonElement>>;
Title: React.ForwardRefExoticComponent<Omit<SheetTitleProps, "ref"> & React.RefAttributes<HTMLHeadingElement>>;
Description: React.ForwardRefExoticComponent<Omit<SheetDescriptionProps, "ref"> & React.RefAttributes<HTMLParagraphElement>>;
Handle: React.ForwardRefExoticComponent<Omit<DrawerHandleProps, "ref"> & React.RefAttributes<HTMLButtonElement>>;
};
//#endregion
export { SheetContentProps as A, Close as C, SheetTitleProps as D, SheetDescriptionProps as E, SheetTriggerProps as M, Trigger as N, Title as O, SheetHandleProps as S, SheetCloseProps as T, composeOpenChange as _, DrawerHandleProps as a, resolveFadeFromIndex as b, DrawerPortalProps as c, NestedRoot as d, Overlay as f, composeHandleClick as g, VaulSnapPoint as h, DrawerContentProps as i, DetentSpec as j, Content$1 as k, DrawerRootProps as l, Root as m, Drawer as n, DrawerNestedRootProps as o, Portal as p, DrawerClose as r, DrawerOverlayProps as s, Content as t, Handle as u, hasMultipleSnapPoints as v, Description as w, Handle$1 as x, resolveCloseThreshold as y };
import * as React from "react";
//#region packages/scrollsheet/src/toast/state.d.ts
/** Drives the row's icon/role and the `data-type` attribute the stylesheet reads. */
type ToastType = "default" | "success" | "error" | "warning" | "info" | "loading";
/**
* The six-value position union (matches real Sonner's own set). Every value
* routes for real at this layer (see `selectPositionToasts` below) — how
* many of the six a given `<Toaster>` actually renders is that component's
* own call (`toaster.tsx`'s `resolveToasterPosition`), not something this
* module gates.
*/
type ToastPosition = "top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right";
/** Compat alias for the pre-neutral-naming name — new code should use `ToastPosition`. */
type SonnerPosition = ToastPosition;
/**
* Real Sonner's own swipe-dismiss edge union (types.ts:152). A Toaster-level
* `swipeDirections` override (see `ToasterProps` in `../toaster`) understands
* these four values regardless of which position they're applied to — lives
* here, not in the shell folder, so both the Sheet-backed `toaster.tsx` and
* the new shell can reference the type without either depending on the
* other's own directory.
*/
type SwipeDirection = "top" | "right" | "bottom" | "left";
interface ToastAction {
label: React.ReactNode;
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
}
/** Per-slot extra class names, mirrors real Sonner's own `ToastClassnames` field-for-field. */
interface ToastClassnames {
toast?: string;
title?: string;
description?: string;
loader?: string;
closeButton?: string;
cancelButton?: string;
actionButton?: string;
success?: string;
error?: string;
info?: string;
warning?: string;
loading?: string;
default?: string;
content?: string;
icon?: string;
}
/** Per-type icon overrides, mirrors real Sonner's own `ToastIcons`. */
interface ToastIcons {
success?: React.ReactNode;
info?: React.ReactNode;
warning?: React.ReactNode;
error?: React.ReactNode;
loading?: React.ReactNode;
close?: React.ReactNode;
}
/** The `data` argument to `toast()` / `.success()` / etc — mirrors Sonner's own `ExternalToast`. */
interface ToastData {
/** Update-in-place: reusing an id already in the queue merges onto that entry instead of pushing a new one. */
id?: number | string;
description?: React.ReactNode;
/** Milliseconds before auto-dismiss, or `Infinity` to disable it. @default 4000 (Toaster's own `duration`, or `toastOptions.duration`) */
duration?: number;
/** @default true */
dismissible?: boolean;
icon?: React.ReactNode;
action?: ToastAction;
cancel?: ToastAction;
className?: string;
classNames?: ToastClassnames;
onDismiss?: (toast: ToastRecord) => void;
onAutoClose?: (toast: ToastRecord) => void;
/** Routes this toast to the `<Toaster toasterId>` with the matching id instead of the default, un-keyed toaster. */
toasterId?: string;
/** Which position's `<ol>` group this toast renders in — omitted routes to the owning `<Toaster>`'s own `position` prop instead (see `selectPositionToasts`). Valid regardless of which positions that Toaster actually renders. */
position?: ToastPosition;
/** Rendered as `data-testid` on the row — mirrors real Sonner's own `testId`. */
testId?: string;
/** Overrides the close button's accessible name for this toast only. @default "Close toast" (or the Toaster's own `toastOptions.closeButtonAriaLabel`) */
closeButtonAriaLabel?: string;
}
/** A live queue entry — what `useSonner()` and each row render from. */
interface ToastRecord extends ToastData {
id: number | string;
type: ToastType;
title?: React.ReactNode;
/** Set by `toast.custom()` — its presence means "render this node verbatim, skip the card chrome" (see ToastRow). */
jsx?: React.ReactNode;
createdAt: number;
}
/**
* A `success`/`error` handler may resolve to this instead of a plain node —
* `message` becomes the title, every other field (description, action,
* icon, ...) applies to the settled toast. Mirrors real Sonner's own
* `PromiseIExtendedResult` (state.ts's `isExtendedResult` check).
*/
type ToastPromiseExtendedResult = Omit<ToastData, "id"> & {
message?: React.ReactNode;
};
interface ToastPromiseData<T> extends Omit<ToastData, "id" | "description"> {
id?: number | string;
loading?: React.ReactNode;
success?: React.ReactNode | ((data: T) => React.ReactNode | ToastPromiseExtendedResult | Promise<React.ReactNode | ToastPromiseExtendedResult>);
error?: React.ReactNode | ((error: unknown) => React.ReactNode | ToastPromiseExtendedResult | Promise<React.ReactNode | ToastPromiseExtendedResult>);
/**
* A static value carries through to the settled toast unchanged; a
* function is called with the settle value (the resolved data, or the
* error/HTTP-status string on the error branches) — matches real Sonner's
* own per-branch description resolution (it never wipes a static one).
*/
description?: React.ReactNode | ((value: unknown) => React.ReactNode | Promise<React.ReactNode>);
finally?: () => void;
}
type ToastFn = ((message: React.ReactNode, data?: ToastData) => number | string) & {
success: (message: React.ReactNode, data?: ToastData) => number | string;
error: (message: React.ReactNode, data?: ToastData) => number | string;
info: (message: React.ReactNode, data?: ToastData) => number | string;
warning: (message: React.ReactNode, data?: ToastData) => number | string;
loading: (message: React.ReactNode, data?: ToastData) => number | string;
/** Alias for the base call, with no type — matches real Sonner's `toast.message()`. */
message: (message: React.ReactNode, data?: ToastData) => number | string;
/**
* `jsx` is a render function receiving the resolved id (matching real
* Sonner), so a custom toast can call `toast.dismiss(id)` on itself. The
* resulting node is stored on the record's `jsx` field, which ToastRow
* renders verbatim, skipping the card chrome (icon/title/description)
* entirely.
*/
custom: (jsx: (id: number | string) => React.ReactNode, data?: ToastData) => number | string;
promise: <T>(promiseOrFn: Promise<T> | (() => Promise<T>), data: ToastPromiseData<T>) => (number | string) & {
unwrap: () => Promise<T>;
};
dismiss: (id?: number | string) => number | string | undefined;
/** Every live (not yet dismissed) toast across every toaster — mirrors real Sonner's own `toast.getToasts()`. */
getToasts: () => readonly ToastRecord[];
/** Every toast ever created, oldest-first, capped at 100 — mirrors real Sonner's own `toast.getHistory()`. */
getHistory: () => readonly ToastRecord[];
};
declare const toast: ToastFn;
/**
* Subscribes to the same store `toast()` writes to. Real Sonner's own
* `useSonner()` takes no arguments and has no visible per-toaster filtering
* in its public surface — matched here unfiltered, across every toaster.
* Not verified byte-for-byte against Sonner's source; matches its observed
* behavior.
*/
declare function useSonner(): {
toasts: ToastRecord[];
};
/** Neutral-named alias for `useSonner` — same implementation, the name fresh docs teach. */
declare const useToasts: typeof useSonner;
//#endregion
//#region packages/scrollsheet/src/toast/toaster.d.ts
/** Pure, exported for tests. @default 3, matching real Sonner's own default. */
declare function resolveVisibleToasts(count: number | undefined): number;
interface ToasterProps {
/** Routes this Toaster to only the toasts created with a matching `toasterId` — omitted (the common case) renders the default, un-keyed queue. */
id?: string;
/** @deprecated Use `id` instead — matches real Sonner's own `Toaster` prop name. */
toasterId?: string;
/** Which corner (or edge-center) this Toaster's own default `<ol>` renders in. All six positions are real. @default 'bottom-right' */
position?: ToastPosition;
/**
* v1 always renders the static light card real Sonner ships by default.
* 'dark'/'system' aren't implemented yet (a v1.1 follow-up) and warn once
* rather than silently no-op — forcing a real visual mismatch would be
* worse than an honest warning.
*/
theme?: "light" | "dark" | "system";
/** Not implemented yet (v1.1) — warns once if set. */
richColors?: boolean;
/** Force the row list open (instead of the collapsed front-card-plus-ghosts) even without hover/focus. */
expand?: boolean;
/**
* Maximum toasts visible (interactive, full opacity) at once per position
* group. Beyond this, older toasts become hidden — `data-visible="false"`,
* still a real DOM node, still timing — rather than evicted; nothing is
* ever dismissed by overflow, and `dismissible: false` earns no special
* protection (it overflows into hidden exactly like any other toast).
* @default 3 (matches real Sonner's own visibleToasts default)
*/
visibleToasts?: number;
/** @default false */
closeButton?: boolean;
/** Milliseconds before auto-dismiss; a per-toast `duration` wins over this. @default 4000 */
duration?: number;
/** Gap between stacked rows, px. @default 14 */
gap?: number;
/** Distance from the viewport edge, px (or any CSS length). @default 24 (16 at the <600px full-bleed breakpoint) */
offset?: number | string;
/**
* Which edge(s) each toast may be swiped away from, overriding the
* per-position default (a corner position allows both its own y and x
* edges; a *-center position only its own y edge). One setting for the
* whole Toaster, applied uniformly to every position group it renders —
* matches real Sonner's own single Toaster-wide `swipeDirections` prop
* (it has no per-toast override).
*/
swipeDirections?: readonly SwipeDirection[];
/** Defaults merged underneath each individual `toast()` call's own options. */
toastOptions?: Omit<ToastData, "id" | "toasterId">;
/** Per-type icon overrides; a per-toast `icon` always wins over these. */
icons?: ToastIcons;
className?: string;
style?: React.CSSProperties;
/** Accessible name for the notifications region. @default "Notifications" */
containerAriaLabel?: string;
/**
* Global keyboard shortcut that expands the stack and moves focus into it
* — every field must be truthy on the event (a modifier like `altKey`, or
* a `KeyboardEvent.code` match), same contract as real Sonner's own
* `hotkey` prop. Escape while focus is inside the region collapses it
* back. Pass `[]` to disable.
* @default ['altKey', 'KeyT']
*/
hotkey?: readonly string[];
/** CSP nonce for the injected style tag. */
nonce?: string;
}
//#endregion
//#region packages/scrollsheet/src/toast/shell/toaster-shell.d.ts
declare function ToasterShell({ id, toasterId, position, theme, richColors, expand: forceExpand, visibleToasts: visibleToastsProp, closeButton, duration: durationProp, gap, offset, swipeDirections, toastOptions, icons, className, style, containerAriaLabel, hotkey, nonce }: ToasterProps): React.ReactPortal | null;
//#endregion
//#region packages/scrollsheet/src/toast/toast-styles.d.ts
/**
* Injects the toast stylesheet into a shadow root via adoptedStyleSheets
* instead of document.head — mirrors src/internal/styles.ts's
* injectStylesInto. Call once per shadow root that hosts a <Toaster>,
* before it first renders. A sibling export rather than folding into
* injectStylesInto: the toast layer is its own chunk (dist/toast.mjs) that
* never loads for a core-only consumer, so its shadow-root injector has to
* be reachable without pulling core's own chunk (and vice versa) — one
* shared call would break that tree-shaking boundary.
*
* Falls back silently to a <style> element appended to the shadow root
* itself on engines without constructable stylesheets (Safari <16.4).
* `nonce` only applies on that fallback path: it sets the injected
* `<style>` element's `nonce` attribute so a CSP `style-src` policy with
* `'nonce-...'` allows it. The `adoptedStyleSheets` path constructs a
* `CSSStyleSheet` and adopts it directly — it is never parsed as an inline
* style element, so CSP has no inline-style check to gate there and `nonce`
* is accepted (for a stable signature across both paths) but unused.
*/
declare function injectToastStylesInto(root: ShadowRoot, nonce?: string): void;
//#endregion
export { SonnerPosition as a, ToastData as c, ToastPromiseData as d, ToastRecord as f, useToasts as g, useSonner as h, resolveVisibleToasts as i, ToastIcons as l, toast as m, ToasterShell as n, ToastAction as o, ToastType as p, ToasterProps as r, ToastClassnames as s, injectToastStylesInto as t, ToastPosition as u };
//#region packages/scrollsheet/src/motion/geometry.d.ts
/**
* Per-side axis geometry.
*
* The whole engine (content.tsx) is written once against an abstract
* "revealed px" concept: 0 = fully hidden, `maxDetent` = fully revealed. The
* DOM's scroll position is the mechanism, but which raw value corresponds to
* "revealed" depends on which canvas edge the panel is pinned to:
* bottom/right sit at the canvas's *far* end, so raw scroll IS revealed px
* directly; top/left sit at the *near* end (flush with the screen when fully
* open), so raw scroll is mirrored: revealed = maxDetent - rawScroll.
* `mapScroll` is the self-inverse conversion between the two spaces.
*/
type Side = "bottom" | "top" | "left" | "right";
interface SideGeometry {
side: Side;
axis: "x" | "y";
/** DOM scroll property driving reveal for this side. */
scrollProp: "scrollTop" | "scrollLeft";
/** Track (viewport) size property along the scroll axis. */
clientSizeProp: "clientHeight" | "clientWidth";
/** Content natural-size property along the scroll axis (for 'content' detents). */
offsetSizeProp: "offsetHeight" | "offsetWidth";
/** +1: raw scroll equals revealed px. -1: raw scroll is mirrored (maxDetent - revealed). */
sign: 1 | -1;
/** transform-origin for the "receded" stacked-parent scale — the panel's free/growing edge. */
recedeOrigin: string;
}
declare function geometryFor(side: Side): SideGeometry;
/**
* Convert between raw scroll position and "revealed px" — self-inverse for a
* given `maxDetent`, so the same call does either direction:
* `mapScroll(rawScrollValue, maxDetent, sign)` → revealed, and
* `mapScroll(revealedValue, maxDetent, sign)` → raw scroll target.
*/
declare function mapScroll(value: number, maxDetent: number, sign: 1 | -1): number;
/**
* translate3d(...) for the receded-parent transform, scale plus a slight
* shift along the panel's free axis (toward wherever "deeper" is for this
* side) — deliberately no filter/brightness (composites badly on iOS); see
* the ::before dim overlay in styles.ts for the darkening instead.
*/
declare function recedeTransform(geometry: SideGeometry, progress: number, scale: number): string;
//#endregion
//#region packages/scrollsheet/src/motion/spring.d.ts
/**
* Spring physics → CSS `linear()` easing.
*
* Simulates a damped spring and samples it into a `linear()` timing function,
* so transitions get real spring motion with zero JS on the animation path.
* The output runs on the compositor; the simulation runs once (and is cached).
*
* The per-regime closed-form solutions in `createSpringSolver` are adapted from
* motion-dom's spring generator (MIT license) —
* https://github.com/motiondivision/motion, packages/motion-dom/src/animation/generators/spring.ts —
* re-derived for this library's fixed 0 → 1 progress convention instead of motion's
* generic origin/target pair.
*/
interface SpringConfig {
/** Stiffness (N/m). Higher = snappier. */
stiffness?: number;
/** Damping coefficient. Higher = less oscillation. */
damping?: number;
/** Mass. Higher = more inertia. */
mass?: number;
/** Initial velocity in units of total-distance per second (from a gesture handoff). */
velocity?: number;
/** Rest threshold as a fraction of distance. */
restDelta?: number;
}
interface SpringCurve {
/** CSS timing function: `linear(0, 0.29 4.3%, …, 1)` */
easing: string;
/** Duration in ms that pairs with the easing. */
durationMs: number;
}
/**
* Simulate the spring in closed form, then sample down to `linear()` keypoints.
* Throws if the config cannot settle inside the search horizon (for example
* damping 0) instead of silently truncating the curve.
*/
declare function spring(config?: SpringConfig): SpringCurve;
/**
* Sample the same closed-form solution at a single elapsed time — progress and
* velocity (progress-units per second) at `atMs`. This is what lets an
* in-flight WAAPI enter/exit leg be interrupted mid-travel: the browser is
* playing a `linear()` curve sampled from this exact solution, so
* re-evaluating it at the elapsed wall-clock time recovers the current
* position and velocity without parsing any computed style (see
* internal/animate.ts). O(1): a direct analytic evaluation, never a
* re-simulation from t=0.
*/
declare function sampleSpringAt(config: SpringConfig, atMs: number): {
value: number;
velocity: number;
};
//#endregion
//#region packages/scrollsheet/src/motion/animate.d.ts
interface AnimateHandle {
/** Resolves on natural completion only — never on stop()/cancel(), never rejects. */
readonly finished: Promise<void>;
/** True once the leg is no longer driving the element (finished, stopped, or canceled). */
readonly settled: boolean;
/** The value the leg is heading toward. */
readonly to: number;
/** Paired duration of the generated curve, for backstop timers. */
readonly durationMs: number;
/**
* Freeze the element at its current position (commitStyles + cancel) and
* return the JS-computed value + velocity (value-units per second) there —
* the handoff for a retargeted or gesture-grabbed leg.
*/
stop(): {
value: number;
velocity: number;
};
/** Drop the animation without committing anything. */
cancel(): void;
}
/**
* Animate `el`'s `prop` from `from` to `to` (numeric, in whatever unit space
* `toCss` maps out of) along a spring's `linear()` curve. `config` must be
* the same spring the curve was generated from — `stop()` re-simulates it.
*
* A zero/absent duration, a missing `el.animate` (no WAAPI), or a throwing
* `animate()` call (e.g. an easing the engine can't parse) all degrade to an
* immediately-finished no-op handle: the caller's CSS resting state for the
* leg's end is what shows, an instant jump instead of a crash.
*/
declare function animate(el: HTMLElement, prop: string, from: number, to: number, toCss: (value: number) => string, curve: SpringCurve, config: SpringConfig): AnimateHandle;
//#endregion
//#region packages/scrollsheet/src/motion/scroll-animator.d.ts
/**
* Programmatic detent travel.
*
* Native smooth `scrollTo` fights concurrent touch input on iOS Safari
* (WebKit #238497), so programmatic moves run a small rAF tween instead —
* and any real user input (pointerdown / wheel / touchstart) cancels it
* immediately so the finger always wins.
*/
interface ScrollAnimation {
cancel(): void;
finished: Promise<boolean>;
}
declare function animateScrollTo(el: HTMLElement, target: number, durationMs: number, axis?: "x" | "y",
/**
* Opt-in only — see the doc comment below. Every caller except
* content-morph omits this, since their scroll targets are already
* registered snap stops; content-morph's target can be a position the
* container's current snap-stop set doesn't know about yet.
*/
suspendSnap?: boolean): ScrollAnimation;
//#endregion
export { SpringConfig as a, spring as c, geometryFor as d, mapScroll as f, animate as i, Side as l, animateScrollTo as n, SpringCurve as o, recedeTransform as p, AnimateHandle as r, sampleSpringAt as s, ScrollAnimation as t, SideGeometry as u };
"use client";
import { c as prefersReducedMotion, h as warnOnce, n as useCloseWatcher, r as createStyleInjector, t as cn } from "./cn-CActSFFr.mjs";
import * as React from "react";
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
import { createPortal } from "react-dom";
//#region packages/scrollsheet/src/toast/state.ts
let toasts = [];
const listeners = new Set();
let uid = 0;
const MAX_HISTORY_SIZE = 100;
let history = [];
function trimHistory() {
let toRemove = history.length - MAX_HISTORY_SIZE;
if (toRemove <= 0) return;
const liveIds = new Set(toasts.map((t) => t.id));
history = history.filter((record) => {
if (toRemove > 0 && !liveIds.has(record.id)) {
toRemove -= 1;
return false;
}
return true;
});
}
function recordHistory(record) {
const index = history.findIndex((t) => t.id === record.id);
if (index === -1) {
history = [...history, record];
trimHistory();
return;
}
history = history.map((t, i) => i === index ? record : t);
}
function nextId() {
uid += 1;
return uid;
}
function publish() {
for (const listener of listeners) listener();
}
function subscribe(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
}
function getSnapshot() {
return toasts;
}
const EMPTY_TOASTS = [];
function getServerSnapshot() {
return EMPTY_TOASTS;
}
function upsert(id, patch, fallbackType) {
const resolvedId = id ?? nextId();
const index = toasts.findIndex((t) => t.id === resolvedId);
const type = patch.type ?? fallbackType;
let record;
if (index === -1) {
record = {
dismissible: true,
...patch,
id: resolvedId,
type,
createdAt: Date.now()
};
toasts = [...toasts, record];
} else {
record = {
...toasts[index],
...patch,
id: resolvedId,
type
};
toasts = toasts.map((t, i) => i === index ? record : t);
}
recordHistory(record);
publish();
return resolvedId;
}
function dismiss(id) {
if (id === void 0) {
const swept = new Set(toasts);
for (const t of swept) {
if (!toasts.includes(t)) continue;
t.onDismiss?.(t);
}
toasts = toasts.filter((t) => !swept.has(t));
publish();
return;
}
const existing = toasts.find((t) => t.id === id);
if (!existing) return id;
existing.onDismiss?.(existing);
toasts = toasts.filter((t) => t !== existing);
publish();
return id;
}
function expire(id) {
const existing = toasts.find((t) => t.id === id);
if (!existing) return;
existing.onAutoClose?.(existing);
toasts = toasts.filter((t) => t !== existing);
publish();
}
function baseToast(message, data) {
return upsert(data?.id, {
...data,
title: message
}, "default");
}
function withType(type) {
return (message, data) => upsert(data?.id, {
...data,
title: message
}, type);
}
const toastImpl = baseToast;
toastImpl.success = withType("success");
toastImpl.error = withType("error");
toastImpl.info = withType("info");
toastImpl.warning = withType("warning");
toastImpl.loading = withType("loading");
toastImpl.message = withType("default");
toastImpl.custom = (jsx, data) => {
const id = data?.id ?? nextId();
return upsert(id, {
...data,
jsx: jsx(id)
}, "default");
};
function isHttpResponse(value) {
return typeof value === "object" && value !== null && "ok" in value && typeof value.ok === "boolean" && "status" in value && typeof value.status === "number";
}
function isPromiseExtendedResult(value) {
return typeof value === "object" && value !== null && !React.isValidElement(value);
}
function applyPromiseSettlement(id, resolved, fallbackTitle, description, type) {
if (isPromiseExtendedResult(resolved)) {
const { message, ...rest } = resolved;
upsert(id, {
description,
...rest,
title: message ?? fallbackTitle,
type
}, type);
return;
}
upsert(id, {
title: resolved ?? fallbackTitle,
description,
type
}, type);
}
function promiseImpl(promiseOrFn, data) {
const { loading, success, error, finally: onFinally, description, ...rest } = data;
const id = upsert(data.id, {
...rest,
description: typeof description === "function" ? void 0 : description,
title: loading,
type: "loading"
}, "loading");
const settle = typeof promiseOrFn === "function" ? promiseOrFn() : promiseOrFn;
let settled;
const chain = settle.then(async (result) => {
if (isHttpResponse(result) && !result.ok) {
settled = {
ok: false,
reason: result
};
const statusMessage = `HTTP error! status: ${result.status}`;
const resolvedDescription = typeof description === "function" ? await description(statusMessage) : description;
const resolved = typeof error === "function" ? await error(statusMessage) : error;
applyPromiseSettlement(id, resolved, "Error", resolvedDescription, "error");
return;
}
if (result instanceof Error) {
settled = {
ok: false,
reason: result
};
const resolvedDescription = typeof description === "function" ? await description(result) : description;
const resolved = typeof error === "function" ? await error(result) : error;
applyPromiseSettlement(id, resolved, "Error", resolvedDescription, "error");
return;
}
settled = {
ok: true,
value: result
};
const resolvedDescription = typeof description === "function" ? await description(result) : description;
const resolved = typeof success === "function" ? await success(result) : success;
applyPromiseSettlement(id, resolved, "Success", resolvedDescription, "success");
}).catch(async (err) => {
settled = {
ok: false,
reason: err
};
const resolvedDescription = typeof description === "function" ? await description(err) : description;
const resolved = typeof error === "function" ? await error(err) : error;
applyPromiseSettlement(id, resolved, "Error", resolvedDescription, "error");
}).finally(() => onFinally?.());
const unwrap = () => chain.then(() => {
if (!settled) throw new Error("scrollsheet toast.promise: chain settled with no outcome");
if (settled.ok) return settled.value;
throw settled.reason;
});
return Object.assign(id, { unwrap });
}
toastImpl.promise = promiseImpl;
toastImpl.dismiss = dismiss;
toastImpl.getToasts = () => toasts;
toastImpl.getHistory = () => history;
const toast = toastImpl;
function useSonner() {
return { toasts: React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) };
}
const useToasts = useSonner;
function selectToasterToasts(all, toasterId) {
return all.filter((t) => toasterId === void 0 ? t.toasterId === void 0 : t.toasterId === toasterId);
}
function selectPositionToasts(all, position, defaultPosition) {
return all.filter((t) => t.position === void 0 ? position === defaultPosition : t.position === position);
}
function selectToastWindow(toasts, visibleToasts) {
const newestFirst = [...toasts].reverse();
return {
visible: newestFirst.slice(0, visibleToasts),
hidden: newestFirst.slice(visibleToasts)
};
}
//#endregion
//#region packages/scrollsheet/src/toast/toast-styles.ts
const { injectDocument, injectShadowRoot } = createStyleInjector("", "data-sonner-toast-styles");
function injectToastStyles(nonce) {
injectDocument(nonce);
}
function injectToastStylesInto(root, nonce) {
injectShadowRoot(root, nonce);
}
//#endregion
//#region packages/scrollsheet/src/toast/toaster.tsx
function resolveVisibleToasts(count) {
if (count === void 0) return 3;
return Math.max(1, Math.floor(count));
}
function useMountedFlag() {
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => {
if (prefersReducedMotion()) {
setMounted(true);
return;
}
let raf2 = 0;
const raf1 = requestAnimationFrame(() => {
raf2 = requestAnimationFrame(() => setMounted(true));
});
return () => {
cancelAnimationFrame(raf1);
cancelAnimationFrame(raf2);
};
}, []);
return mounted;
}
function useIsDocumentHidden() {
const [hidden, setHidden] = React.useState(() => typeof document !== "undefined" && document.hidden);
React.useEffect(() => {
const onVisibilityChange = () => setHidden(document.hidden);
document.addEventListener("visibilitychange", onVisibilityChange);
return () => document.removeEventListener("visibilitychange", onVisibilityChange);
}, []);
return hidden;
}
const EMPTY_RECORDS = [];
const EMPTY_MAP = new Map();
function toIdMap(records) {
return new Map(records.map((r) => [r.id, r]));
}
function sameIds(a, b) {
if (a.size !== b.size) return false;
for (const id of a.keys()) if (!b.has(id)) return false;
return true;
}
function useToastExit(live, queued = EMPTY_RECORDS, exitMs = 220) {
const [exiting, setExiting] = React.useState(() => EMPTY_MAP);
const [prevLive, setPrevLive] = React.useState(() => toIdMap(live));
const timersRef = React.useRef(new Map());
const liveIds = new Set(live.map((r) => r.id));
let renderExiting = exiting;
if (!sameIds(prevLive, liveIds)) {
const queuedIds = new Set(queued.map((r) => r.id));
const justRemoved = [];
for (const [id, record] of prevLive) {
if (liveIds.has(id)) continue;
if (queuedIds.has(id)) continue;
justRemoved.push(record);
}
setPrevLive(toIdMap(live));
if (justRemoved.length > 0) {
const next = new Map(exiting);
for (const record of justRemoved) next.set(record.id, record);
renderExiting = next;
setExiting(next);
}
}
React.useEffect(() => {
const reduced = prefersReducedMotion();
for (const [id] of exiting) {
if (timersRef.current.has(id)) continue;
const timer = setTimeout(() => {
timersRef.current.delete(id);
setExiting((prev) => {
if (!prev.has(id)) return prev;
const next = new Map(prev);
next.delete(id);
return next;
});
}, reduced ? 0 : exitMs);
timersRef.current.set(id, timer);
}
for (const [id, timer] of timersRef.current) if (!exiting.has(id)) {
clearTimeout(timer);
timersRef.current.delete(id);
}
}, [exiting, exitMs]);
React.useEffect(() => {
const timers = timersRef.current;
return () => {
for (const timer of timers.values()) clearTimeout(timer);
timers.clear();
};
}, []);
const rows = live.map((record) => ({
record,
exiting: false
}));
for (const [id, record] of renderExiting) if (!liveIds.has(id)) rows.push({
record,
exiting: true
});
return rows;
}
//#endregion
//#region packages/scrollsheet/src/toast/shell/shell-selectors.ts
function splitPosition(position) {
const [y, x] = position.split("-");
return {
y,
x
};
}
function computePossiblePositions(defaultPosition, toasts) {
const seen = new Set([defaultPosition]);
for (const t of toasts) if (t.position) seen.add(t.position);
return [...seen];
}
function computeRowOffsets(newestFirst, heights, gap) {
let cumulativeHeight = 0;
return newestFirst.map((record, index) => {
const stackOffset = index * gap + cumulativeHeight;
cumulativeHeight += heights.get(record.id) ?? 0;
return {
id: record.id,
index,
toastsBefore: index,
stackOffset
};
});
}
function findNewestDismissible(toasts) {
for (let i = toasts.length - 1; i >= 0; i -= 1) {
const t = toasts[i];
if (t && t.dismissible !== false) return t;
}
}
const SWIPE_VELOCITY_THRESHOLD = .11;
function getDefaultSwipeDirections(position) {
const { y, x } = splitPosition(position);
const directions = [y];
if (x === "left" || x === "right") directions.push(x);
return directions;
}
function lockSwipeAxis(dx, dy) {
if (Math.abs(dx) <= 1 && Math.abs(dy) <= 1) return null;
return Math.abs(dx) > Math.abs(dy) ? "x" : "y";
}
function dampenSwipeDelta(delta) {
const dampened = delta * (1 / (1.5 + Math.abs(delta) / 20));
return Math.abs(dampened) < Math.abs(delta) ? dampened : delta;
}
function computeSwipeAxisAmount(axis, delta, directions) {
const negativeDir = axis === "x" ? "left" : "top";
const positiveDir = axis === "x" ? "right" : "bottom";
if (!(directions.includes(negativeDir) || directions.includes(positiveDir))) return 0;
return directions.includes(negativeDir) && delta < 0 || directions.includes(positiveDir) && delta > 0 ? delta : dampenSwipeDelta(delta);
}
function isSwipeReleaseAllowed(axis, amount, directions) {
if (axis === "x") return directions.includes(amount > 0 ? "right" : "left");
return directions.includes(amount > 0 ? "bottom" : "top");
}
function shouldDismissOnSwipeRelease(amount, velocity, thresholdPx = 45, velocityThreshold = SWIPE_VELOCITY_THRESHOLD) {
return Math.abs(amount) >= thresholdPx || velocity > velocityThreshold;
}
function resolveSwipeOutDirection(axis, amount) {
if (axis === "x") return amount > 0 ? "right" : "left";
return amount > 0 ? "down" : "up";
}
//#endregion
//#region packages/scrollsheet/src/toast/shell/use-toast-swipe.ts
const ZERO_AMOUNT = {
x: 0,
y: 0
};
function useToastSwipe(elRef, enabled, directions, onSwipeDismiss) {
const draggingRef = React.useRef(false);
const axisRef = React.useRef(null);
const startRef = React.useRef(ZERO_AMOUNT);
const amountRef = React.useRef(ZERO_AMOUNT);
const dragStartRef = React.useRef(0);
const onSwipeDismissRef = React.useRef(onSwipeDismiss);
onSwipeDismissRef.current = onSwipeDismiss;
const onPointerDown = React.useCallback((event) => {
if (!enabled || event.button !== 0) return;
if (event.target.tagName === "BUTTON") return;
const el = elRef.current;
if (!el) return;
draggingRef.current = true;
axisRef.current = null;
amountRef.current = ZERO_AMOUNT;
startRef.current = {
x: event.clientX,
y: event.clientY
};
dragStartRef.current = Date.now();
el.setPointerCapture(event.pointerId);
el.setAttribute("data-swiping", "true");
}, [enabled, elRef]);
const onPointerMove = React.useCallback((event) => {
if (!draggingRef.current) return;
const el = elRef.current;
if (!el) return;
if ((window.getSelection?.()?.toString().length ?? 0) > 0) return;
const start = startRef.current;
const dx = event.clientX - start.x;
const dy = event.clientY - start.y;
if (axisRef.current === null) axisRef.current = lockSwipeAxis(dx, dy);
const axis = axisRef.current;
if (axis === null) return;
const resolved = computeSwipeAxisAmount(axis, axis === "x" ? dx : dy, directions);
const amount = axis === "x" ? {
x: resolved,
y: 0
} : {
x: 0,
y: resolved
};
amountRef.current = amount;
if (resolved !== 0) el.setAttribute("data-swiped", "true");
el.style.setProperty("--scrollsheet-toast-swipe-x", `${amount.x}px`);
el.style.setProperty("--scrollsheet-toast-swipe-y", `${amount.y}px`);
}, [elRef, directions]);
const release = React.useCallback(() => {
if (!draggingRef.current) return;
draggingRef.current = false;
const el = elRef.current;
const axis = axisRef.current;
axisRef.current = null;
if (!el) return;
if (axis === null) {
el.setAttribute("data-swiping", "false");
return;
}
const amount = axis === "x" ? amountRef.current.x : amountRef.current.y;
const elapsed = Math.max(1, Date.now() - dragStartRef.current);
const velocity = Math.abs(amount) / elapsed;
if (isSwipeReleaseAllowed(axis, amount, directions) && shouldDismissOnSwipeRelease(amount, velocity)) {
const direction = resolveSwipeOutDirection(axis, amount);
el.setAttribute("data-swipe-direction", direction);
if (prefersReducedMotion()) {
el.setAttribute("data-swipe-out", "true");
onSwipeDismissRef.current();
return;
}
const onAnimEnd = () => {
el.removeEventListener("animationend", onAnimEnd);
onSwipeDismissRef.current();
};
el.addEventListener("animationend", onAnimEnd);
el.setAttribute("data-swipe-out", "true");
return;
}
el.setAttribute("data-swiping", "false");
el.setAttribute("data-swiped", "false");
el.style.setProperty("--scrollsheet-toast-swipe-x", "0px");
el.style.setProperty("--scrollsheet-toast-swipe-y", "0px");
}, [elRef, directions]);
return {
onPointerDown,
onPointerMove,
onPointerUp: React.useCallback((event) => {
if (elRef.current?.hasPointerCapture(event.pointerId)) elRef.current.releasePointerCapture(event.pointerId);
release();
}, [elRef, release]),
onPointerCancel: React.useCallback(() => release(), [release])
};
}
//#endregion
//#region packages/scrollsheet/src/toast/shell/toast-row.tsx
function ToastIcon({ type, icons, spinnerClassName }) {
if (type === "loading") {
if (icons?.loading) return jsx(Fragment, { children: icons.loading });
return jsx("span", { className: spinnerClassName });
}
switch (type) {
case "success": return jsx(Fragment, { children: icons?.success ?? "✓" });
case "error": return jsx(Fragment, { children: icons?.error ?? "✕" });
case "warning": return jsx(Fragment, { children: icons?.warning ?? "!" });
case "info": return jsx(Fragment, { children: icons?.info ?? "i" });
default: return null;
}
}
function ToastRow({ record, index, total, toastsBefore, stackOffset, height, frontHeight, visible, expanded, removed, yPosition, xPosition, closeButton, directions, onDismiss, observe, toasterClassNames, icons, toasterCloseButtonAriaLabel }) {
const role = record.type === "error" ? "alert" : "status";
const mounted = useMountedFlag();
const elRef = React.useRef(null);
const dismissible = record.dismissible !== false;
const isFront = index === 0;
const setRefs = React.useCallback((el) => {
elRef.current = el;
observe(record.id, el);
}, [observe, record.id]);
const swipeHandlers = useToastSwipe(elRef, !removed && dismissible && record.type !== "loading", directions, () => onDismiss(record));
React.useEffect(() => {
const el = elRef.current;
if (el) el.inert = removed;
}, [removed]);
const style = {
"--scrollsheet-toast-toasts-before": toastsBefore,
"--scrollsheet-toast-stack-offset": `${stackOffset}px`,
"--scrollsheet-toast-front-height": frontHeight !== void 0 ? `${frontHeight}px` : "0px",
"--scrollsheet-toast-initial-height": height !== void 0 ? `${height}px` : "auto",
zIndex: Math.max(0, total - index)
};
const rootClassName = cn("scrollsheet-toast", "sonner-toast", record.className, toasterClassNames?.toast, record.classNames?.toast, toasterClassNames?.default, toasterClassNames?.[record.type], record.classNames?.[record.type]);
const motionAttrs = {
"data-mounted": mounted ? "" : void 0,
"data-removed": removed ? "" : void 0,
"data-visible": visible ? "true" : "false",
"data-front": isFront ? "true" : "false",
"data-expanded": expanded ? "true" : "false",
"data-y-position": yPosition,
"data-x-position": xPosition,
"data-index": index,
"data-dismissible": dismissible ? "true" : "false",
"data-swiping": "false",
"data-swiped": "false",
"aria-hidden": removed ? "true" : void 0
};
if (record.jsx !== void 0) return jsx("div", {
ref: setRefs,
className: rootClassName,
"data-scrollsheet-toast": "",
"data-sonner-toast": "",
"data-scrollsheet-custom": "",
"data-sonner-custom": "",
"data-testid": record.testId,
role,
style,
...motionAttrs,
...swipeHandlers,
children: record.jsx
});
const showCloseButton = closeButton && dismissible && record.type !== "loading";
const hasActions = Boolean(record.action || record.cancel || showCloseButton);
const closeButtonAriaLabel = record.closeButtonAriaLabel ?? toasterCloseButtonAriaLabel ?? "Close toast";
return jsxs("div", {
ref: setRefs,
className: rootClassName,
"data-scrollsheet-toast": "",
"data-sonner-toast": "",
"data-type": record.type,
"data-testid": record.testId,
role,
style,
...motionAttrs,
...swipeHandlers,
children: [
jsx("span", {
className: cn("scrollsheet-toast-icon", "sonner-toast-icon", toasterClassNames?.icon, record.classNames?.icon),
"data-type": record.type,
"aria-hidden": "true",
children: record.icon ?? jsx(ToastIcon, {
type: record.type,
icons,
spinnerClassName: cn("scrollsheet-toast-spinner", "sonner-toast-spinner", toasterClassNames?.loader, record.classNames?.loader)
})
}),
jsxs("div", {
className: cn("scrollsheet-toast-body", "sonner-toast-body", toasterClassNames?.content, record.classNames?.content),
children: [record.title !== void 0 && jsx("div", {
className: cn("scrollsheet-toast-title", "sonner-toast-title", toasterClassNames?.title, record.classNames?.title),
children: record.title
}), record.description !== void 0 && jsx("div", {
className: cn("scrollsheet-toast-description", "sonner-toast-description", toasterClassNames?.description, record.classNames?.description),
children: record.description
})]
}),
hasActions && jsxs("div", {
className: "scrollsheet-toast-actions sonner-toast-actions",
children: [
record.cancel && jsx("button", {
type: "button",
className: cn("scrollsheet-toast-cancel", "sonner-toast-cancel", toasterClassNames?.cancelButton, record.classNames?.cancelButton),
onClick: (event) => {
if (!dismissible) return;
record.cancel?.onClick(event);
onDismiss(record);
},
children: record.cancel.label
}),
record.action && jsx("button", {
type: "button",
className: cn("scrollsheet-toast-action", "sonner-toast-action", toasterClassNames?.actionButton, record.classNames?.actionButton),
onClick: (event) => {
record.action?.onClick(event);
if (event.defaultPrevented) return;
onDismiss(record);
},
children: record.action.label
}),
showCloseButton && jsx("button", {
type: "button",
className: cn("scrollsheet-toast-close", "sonner-toast-close", toasterClassNames?.closeButton, record.classNames?.closeButton),
"aria-label": closeButtonAriaLabel,
onClick: () => onDismiss(record),
children: icons?.close ?? "×"
})
]
})
]
});
}
//#endregion
//#region packages/scrollsheet/src/toast/shell/use-toast-heights.ts
function useToastHeights() {
const [heights, setHeights] = React.useState(() => new Map());
const observersRef = React.useRef(new Map());
const setHeight = React.useCallback((id, height) => {
setHeights((prev) => prev.get(id) === height ? prev : new Map(prev).set(id, height));
}, []);
const forget = React.useCallback((id) => {
observersRef.current.get(id)?.disconnect();
observersRef.current.delete(id);
setHeights((prev) => {
if (!prev.has(id)) return prev;
const next = new Map(prev);
next.delete(id);
return next;
});
}, []);
const observe = React.useCallback((id, el) => {
const observers = observersRef.current;
observers.get(id)?.disconnect();
observers.delete(id);
if (!el || typeof ResizeObserver === "undefined") return;
const ro = new ResizeObserver(() => setHeight(id, el.getBoundingClientRect().height));
ro.observe(el);
observers.set(id, ro);
setHeight(id, el.getBoundingClientRect().height);
}, [setHeight]);
React.useEffect(() => {
const observers = observersRef.current;
return () => {
for (const ro of observers.values()) ro.disconnect();
observers.clear();
};
}, []);
return {
heights,
observe,
forget
};
}
//#endregion
//#region packages/scrollsheet/src/toast/shell/toaster-shell.tsx
const DEFAULT_HOTKEY = ["altKey", "KeyT"];
function PositionGroup({ position, defaultPosition, toasts, visibleToasts, gap, expanded, closeButton, swipeDirections, toastOptions, icons, className, baseStyle, registerListEl, dismissRow, onHoverEnter, onHoverLeave, onInteractingChange }) {
const directions = React.useMemo(() => swipeDirections ?? getDefaultSwipeDirections(position), [swipeDirections, position]);
const positionToasts = React.useMemo(() => selectPositionToasts(toasts, position, defaultPosition), [
toasts,
position,
defaultPosition
]);
const newestFirst = React.useMemo(() => [...positionToasts].reverse(), [positionToasts]);
const window_ = React.useMemo(() => selectToastWindow(positionToasts, visibleToasts), [positionToasts, visibleToasts]);
const visibleIds = React.useMemo(() => new Set(window_.visible.map((t) => t.id)), [window_]);
const { heights, observe, forget } = useToastHeights();
const exitRows = useToastExit(newestFirst);
const exitingIds = React.useMemo(() => new Set(exitRows.filter((r) => r.exiting).map((r) => r.record.id)), [exitRows]);
React.useEffect(() => {
for (const id of exitingIds) forget(id);
}, [exitingIds, forget]);
const offsets = React.useMemo(() => computeRowOffsets(newestFirst, heights, gap), [
newestFirst,
heights,
gap
]);
const offsetById = React.useMemo(() => new Map(offsets.map((o) => [o.id, o])), [offsets]);
const lastOffsetRef = React.useRef(new Map());
for (const o of offsets) lastOffsetRef.current.set(o.id, o);
if (lastOffsetRef.current.size > offsets.length) {
const keep = new Set([...offsets.map((o) => o.id), ...exitingIds]);
for (const id of [...lastOffsetRef.current.keys()]) if (!keep.has(id)) lastOffsetRef.current.delete(id);
}
const frontHeight = heights.get(newestFirst[0]?.id ?? "");
const { y, x } = splitPosition(position);
const listRef = React.useCallback((el) => registerListEl(position, el), [registerListEl, position]);
if (exitRows.length === 0) return null;
return jsx("ol", {
ref: listRef,
"data-scrollsheet-toaster": "",
"data-sonner-toaster": "",
"data-scrollsheet-theme": "light",
"data-sonner-theme": "light",
"data-y-position": y,
"data-x-position": x,
tabIndex: -1,
className,
style: {
...baseStyle,
"--scrollsheet-toast-gap": `${gap}px`,
"--scrollsheet-toast-front-height": frontHeight !== void 0 ? `${frontHeight}px` : void 0
},
onMouseEnter: onHoverEnter,
onMouseMove: onHoverEnter,
onMouseLeave: onHoverLeave,
onPointerDown: (event) => {
if (event.target.dataset.dismissible === "false") return;
onInteractingChange(true);
},
onPointerUp: () => onInteractingChange(false),
children: exitRows.map(({ record, exiting }) => {
const offset = offsetById.get(record.id) ?? lastOffsetRef.current.get(record.id);
const index = offset?.index ?? newestFirst.length;
return jsx(ToastRow, {
record,
index,
total: newestFirst.length,
toastsBefore: offset?.toastsBefore ?? index,
stackOffset: offset?.stackOffset ?? 0,
height: heights.get(record.id),
frontHeight,
visible: visibleIds.has(record.id),
expanded,
removed: exiting,
yPosition: y,
xPosition: x,
closeButton,
directions,
onDismiss: dismissRow,
observe,
toasterClassNames: toastOptions?.classNames,
icons,
toasterCloseButtonAriaLabel: toastOptions?.closeButtonAriaLabel
}, record.id);
})
});
}
function resolveShellPosition(position) {
return position ?? "bottom-right";
}
function ToasterShell({ id, toasterId, position, theme, richColors, expand: forceExpand, visibleToasts: visibleToastsProp, closeButton = false, duration: durationProp, gap = 14, offset, swipeDirections, toastOptions, icons, className, style, containerAriaLabel = "Notifications", hotkey = DEFAULT_HOTKEY, nonce }) {
const resolvedId = id ?? toasterId;
if (toasterId !== void 0) warnOnce("toaster-id-deprecated", "[scrollsheet Toaster] Toaster's \"toasterId\" prop is deprecated — use \"id\" instead.");
const { toasts: allToasts } = useSonner();
const toasts = React.useMemo(() => selectToasterToasts(allToasts, resolvedId), [allToasts, resolvedId]);
const defaultPosition = resolveShellPosition(position);
const possiblePositions = React.useMemo(() => computePossiblePositions(defaultPosition, toasts), [defaultPosition, toasts]);
const visibleToasts = resolveVisibleToasts(visibleToastsProp);
const defaultDuration = durationProp ?? toastOptions?.duration ?? 4e3;
if (theme !== void 0 && theme !== "light") warnOnce("shell-theme", `[scrollsheet Toaster] theme="${theme}" isn't implemented yet — v1 always renders the static light card real Sonner ships by default.`);
if (richColors) warnOnce("shell-rich-colors", "[scrollsheet Toaster] richColors isn't implemented yet — toasts render with their default icon color only.");
React.useEffect(() => {
injectToastStyles(nonce);
}, [nonce]);
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => setMounted(true), []);
const [hovered, setHovered] = React.useState(false);
const [hotkeyExpanded, setHotkeyExpanded] = React.useState(false);
const [interacting, setInteracting] = React.useState(false);
const expanded = Boolean(forceExpand) || hovered || hotkeyExpanded;
const isDocumentHidden = useIsDocumentHidden();
const handleHoverEnter = React.useCallback(() => setHovered(true), []);
const handleHoverLeave = React.useCallback(() => {
if (!interacting) setHovered(false);
}, [interacting]);
const dismissRow = React.useCallback((record) => {
toast.dismiss(record.id);
}, []);
const listElsRef = React.useRef(new Map());
const registerListEl = React.useCallback((pos, el) => {
if (el) listElsRef.current.set(pos, el);
else listElsRef.current.delete(pos);
}, []);
React.useEffect(() => {
if (hotkey.length === 0) return;
const handleKeyDown = (event) => {
if (hotkey.every((key) => event[key] || event.code === key)) {
setHotkeyExpanded(true);
const [firstList] = listElsRef.current.values();
firstList?.focus({ preventScroll: true });
}
if (event.code !== "Escape") return;
const active = document.activeElement;
const focusedEntry = [...listElsRef.current.entries()].find(([, el]) => active === el || el.contains(active));
if (!focusedEntry) return;
if (hotkeyExpanded) {
setHotkeyExpanded(false);
return;
}
const [focusedPosition] = focusedEntry;
const positionToasts = selectPositionToasts(toasts, focusedPosition, defaultPosition);
const front = positionToasts[positionToasts.length - 1];
if (front) dismissRow(front);
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [
hotkey,
hotkeyExpanded,
toasts,
defaultPosition,
dismissRow
]);
const timersRef = React.useRef(new Map());
React.useEffect(() => {
const timers = timersRef.current;
const liveIds = new Set(toasts.map((t) => t.id));
for (const [id, entry] of timers) {
if (liveIds.has(id)) continue;
if (entry.timer) clearTimeout(entry.timer);
timers.delete(id);
}
const paused = expanded || interacting || isDocumentHidden;
for (const t of toasts) {
if (t.type === "loading") {
const stale = timers.get(t.id);
if (stale) {
if (stale.timer) clearTimeout(stale.timer);
timers.delete(t.id);
}
continue;
}
const ms = t.duration ?? toastOptions?.duration ?? defaultDuration;
const existing = timers.get(t.id);
if (!existing || existing.record !== t) {
if (existing?.timer) clearTimeout(existing.timer);
const entry = {
timer: null,
startedAt: 0,
remaining: ms,
record: t
};
timers.set(t.id, entry);
if (Number.isFinite(ms) && !paused) {
entry.startedAt = Date.now();
entry.timer = setTimeout(() => {
timersRef.current.delete(t.id);
expire(t.id);
}, ms);
}
continue;
}
if (paused) {
if (existing.timer) {
clearTimeout(existing.timer);
const elapsed = Date.now() - existing.startedAt;
existing.remaining = Math.max(0, existing.remaining - elapsed);
existing.timer = null;
}
continue;
}
if (existing.timer || !Number.isFinite(existing.remaining)) continue;
existing.startedAt = Date.now();
existing.timer = setTimeout(() => {
timersRef.current.delete(t.id);
expire(t.id);
}, existing.remaining);
}
}, [
toasts,
expanded,
interacting,
isDocumentHidden,
defaultDuration,
toastOptions?.duration
]);
React.useEffect(() => {
const timers = timersRef.current;
return () => {
for (const entry of timers.values()) if (entry.timer) clearTimeout(entry.timer);
timers.clear();
};
}, []);
useCloseWatcher({
present: toasts.length > 0,
nonModal: true,
escapeDismissible: true,
onClose: () => {
const target = findNewestDismissible(toasts);
if (target) dismissRow(target);
}
});
if (!mounted || typeof document === "undefined") return null;
const offsetValue = offset !== void 0 ? typeof offset === "number" ? `${offset}px` : offset : void 0;
const baseStyle = {
...style,
"--scrollsheet-toast-offset": offsetValue
};
return createPortal(jsx("section", {
"aria-label": containerAriaLabel,
tabIndex: -1,
"aria-live": "polite",
"aria-relevant": "additions text",
"aria-atomic": "false",
suppressHydrationWarning: true,
"data-react-aria-top-layer": "",
children: possiblePositions.map((groupPosition) => jsx(PositionGroup, {
position: groupPosition,
defaultPosition,
toasts,
visibleToasts,
gap,
expanded,
closeButton,
swipeDirections,
toastOptions,
icons,
className,
baseStyle,
registerListEl,
dismissRow,
onHoverEnter: handleHoverEnter,
onHoverLeave: handleHoverLeave,
onInteractingChange: setInteracting
}, groupPosition))
}), document.body);
}
//#endregion
export { useSonner as a, toast as i, resolveVisibleToasts as n, useToasts as o, injectToastStylesInto as r, ToasterShell as t };