diff --git a/.changeset/wise-drawers-appear.md b/.changeset/wise-drawers-appear.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/wise-drawers-appear.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/headless/package.json b/packages/headless/package.json index 495046e13a7..fc489fe367a 100644 --- a/packages/headless/package.json +++ b/packages/headless/package.json @@ -41,6 +41,10 @@ "import": "./dist/primitives/dialog/index.js", "types": "./dist/primitives/dialog/index.d.ts" }, + "./drawer": { + "import": "./dist/primitives/drawer/index.js", + "types": "./dist/primitives/drawer/index.d.ts" + }, "./hooks": { "import": "./dist/hooks/index.js", "types": "./dist/hooks/index.d.ts" diff --git a/packages/headless/src/primitives/drawer/README.md b/packages/headless/src/primitives/drawer/README.md new file mode 100644 index 00000000000..adbab4164a8 --- /dev/null +++ b/packages/headless/src/primitives/drawer/README.md @@ -0,0 +1,256 @@ +# Drawer + +A modal bottom-sheet overlay with drag-to-dismiss, optional snap points, virtual-keyboard +awareness, and nesting. Built on the same Floating UI infrastructure as `Dialog` (portal, focus +trap, scroll lock, dismiss, `FloatingTree` nesting, enter/exit transitions) with a hand-rolled +pointer/transform drag engine layered on top. + +The headless layer emits **raw** CSS custom properties and `data-cl-*` attributes and ships **zero +CSS**. The styled (mosaic) layer composes the actual `transform` / `opacity` / `calc()` chains from +those inputs. + +## When to Use + +- A sheet that slides up from the bottom edge and is dismissed by swiping down. +- Mobile-first flows where snap points (peek / half / full) make sense. +- Prefer `Dialog` for centered modals that are not anchored to the bottom and have no drag gesture. + +## Usage + +```tsx +import { Drawer } from '@clerk/headless/drawer'; + + + Open + + + + + + Sheet title + Optional description. +

Sheet content.

+ Close +
+
+
+
; +``` + +### Controlled + +```tsx +const [open, setOpen] = useState(false); + + + {/* ... */} +; +``` + +### Detached trigger + +A trigger rendered **outside** `Drawer.Root` can drive it through a shared handle: + +```tsx +const handle = createDrawerHandle(); + +<> + Open from anywhere + {/* ... */} +; +``` + +`handle.open()` / `handle.close()` / `handle.toggle()` work imperatively; `handle.isOpen` and +`handle.subscribe(cb)` make it `useSyncExternalStore`-compatible. + +`Drawer.Root` stays the single source of truth, so the handle composes with `open` / `defaultOpen` +/ `onOpenChange`: `defaultOpen` and a controlled `open` prop still apply, and every handle-driven +transition fires `onOpenChange` just like a click or dismissal. + +### Snap points + +```tsx +// Ascending fractions of the viewport the sheet can rest at; 1 = full height. + + {/* ... */} + +``` + +Control the active point with `activeSnapPoint` / `onActiveSnapPointChange`. A slow release settles to +the nearest point; a fast flick steps one point (up = larger, down = smaller, or dismiss from the +first point when `dismissible`). An uncontrolled drawer returns to its default snap point when it +closes, so the next open starts fresh. Dragging past the fully-open edge is square-root damped so the +sheet resists overshooting. + +### Handle-only dragging + +```tsx +// Only a press starting on Drawer.Handle initiates the drag; the body scrolls normally. +{/* ... */} +``` + +## Parts + +| Part | Default Element | Description | +| -------------------- | --------------- | ---------------------------------------------------------------- | +| `Drawer.Root` | — | Root context provider; owns open/drag/snap/nesting state | +| `Drawer.Trigger` | `