@seed-design/react-drawer 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@seed-design/react-drawer",
3
+ "version": "1.0.0",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "git+https://github.com/daangn/seed-design.git",
7
+ "directory": "packages/react-headless/drawer"
8
+ },
9
+ "sideEffects": false,
10
+ "type": "module",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./lib/index.d.ts",
14
+ "import": "./lib/index.js",
15
+ "require": "./lib/index.cjs"
16
+ },
17
+ "./package.json": "./package.json"
18
+ },
19
+ "main": "./lib/index.cjs",
20
+ "files": [
21
+ "lib",
22
+ "src"
23
+ ],
24
+ "scripts": {
25
+ "clean": "rm -rf lib",
26
+ "build": "bunchee",
27
+ "lint:publish": "bun publint"
28
+ },
29
+ "dependencies": {
30
+ "@radix-ui/react-compose-refs": "^1.1.2",
31
+ "@radix-ui/react-dialog": "^1.1.15",
32
+ "@radix-ui/react-use-callback-ref": "^1.1.0",
33
+ "@radix-ui/react-use-controllable-state": "^1.2.2",
34
+ "@seed-design/dom-utils": "1.0.0",
35
+ "@seed-design/react-primitive": "1.0.0"
36
+ },
37
+ "devDependencies": {
38
+ "@types/react": "^19.1.6",
39
+ "react": "^19.1.0",
40
+ "react-dom": "^19.1.0"
41
+ },
42
+ "peerDependencies": {
43
+ "react": ">=18.0.0",
44
+ "react-dom": ">=18.0.0"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public"
48
+ }
49
+ }
@@ -0,0 +1,20 @@
1
+ export {
2
+ DrawerBackdrop as Backdrop,
3
+ DrawerCloseButton as CloseButton,
4
+ DrawerContent as Content,
5
+ DrawerDescription as Description,
6
+ DrawerHandle as Handle,
7
+ DrawerPositioner as Positioner,
8
+ DrawerRoot as Root,
9
+ DrawerTitle as Title,
10
+ DrawerTrigger as Trigger,
11
+ type DrawerBackdropProps as BackdropProps,
12
+ type DrawerCloseButtonProps as CloseButtonProps,
13
+ type DrawerContentProps as ContentProps,
14
+ type DrawerDescriptionProps as DescriptionProps,
15
+ type DrawerHandleProps as HandleProps,
16
+ type DrawerPositionerProps as PositionerProps,
17
+ type DrawerRootProps as RootProps,
18
+ type DrawerTitleProps as TitleProps,
19
+ type DrawerTriggerProps as TriggerProps,
20
+ } from "./Drawer";
package/src/Drawer.tsx ADDED
@@ -0,0 +1,410 @@
1
+ "use client";
2
+
3
+ import { useComposedRefs } from "@radix-ui/react-compose-refs";
4
+ import * as DialogPrimitive from "@radix-ui/react-dialog";
5
+ import { useCallbackRef } from "@radix-ui/react-use-callback-ref";
6
+ import { dataAttr } from "@seed-design/dom-utils";
7
+ import { Primitive, type PrimitiveProps } from "@seed-design/react-primitive";
8
+ import type * as React from "react";
9
+ import { forwardRef, useEffect, useRef, useState } from "react";
10
+ import type { DrawerDirection } from "./types";
11
+ import { useDrawer, type DialogProps } from "./useDrawer";
12
+ import { DrawerProvider, useDrawerContext } from "./useDrawerContext";
13
+
14
+ export interface DrawerRootProps extends DialogProps {
15
+ children?: React.ReactNode;
16
+ }
17
+
18
+ export const DrawerRoot = (props: DrawerRootProps) => {
19
+ const { children, defaultOpen, dismissible, onOpenChange, modal } = props;
20
+ const api = useDrawer(props);
21
+ return (
22
+ <DialogPrimitive.Root
23
+ defaultOpen={defaultOpen}
24
+ open={api.isOpen}
25
+ onOpenChange={(open) => {
26
+ if (!dismissible && !open) return;
27
+ if (open) {
28
+ api.setHasBeenOpened(true);
29
+ } else {
30
+ api.closeDrawer(true);
31
+ }
32
+
33
+ api.setIsOpen(open);
34
+ onOpenChange?.(open);
35
+ }}
36
+ modal={modal}
37
+ >
38
+ <DrawerProvider value={api}>{children}</DrawerProvider>
39
+ </DialogPrimitive.Root>
40
+ );
41
+ };
42
+
43
+ export interface DrawerTriggerProps extends DialogPrimitive.DialogTriggerProps {}
44
+
45
+ export const DrawerTrigger = DialogPrimitive.Trigger;
46
+
47
+ export interface DrawerPositionerProps
48
+ extends PrimitiveProps,
49
+ React.HTMLAttributes<HTMLDivElement> {}
50
+
51
+ export const DrawerPositioner = forwardRef<HTMLDivElement, DrawerPositionerProps>((props, ref) => {
52
+ const api = useDrawerContext();
53
+ return (
54
+ <Primitive.div
55
+ ref={ref}
56
+ {...props}
57
+ style={{ pointerEvents: api.isOpen ? undefined : "none" }}
58
+ />
59
+ );
60
+ });
61
+ DrawerPositioner.displayName = "DrawerPositioner";
62
+
63
+ export interface DrawerBackdropProps
64
+ extends DialogPrimitive.DialogOverlayProps,
65
+ React.HTMLAttributes<HTMLDivElement> {}
66
+
67
+ export const DrawerBackdrop = forwardRef<HTMLDivElement, DrawerBackdropProps>((props, ref) => {
68
+ const {
69
+ overlayRef,
70
+ onRelease,
71
+ modal,
72
+ snapPoints,
73
+ isOpen,
74
+ shouldFade,
75
+ shouldAnimate,
76
+ shouldOverlayAnimate,
77
+ } = useDrawerContext();
78
+ const composedRef = useComposedRefs(ref, overlayRef);
79
+ const hasSnapPoints = snapPoints && snapPoints.length > 0;
80
+ const onMouseUp = useCallbackRef((event: React.PointerEvent<HTMLDivElement>) => onRelease(event));
81
+
82
+ // Overlay is the component that is locking scroll, removing it will unlock the scroll without having to dig into Radix's Dialog library
83
+ if (!modal) {
84
+ return null;
85
+ }
86
+
87
+ return (
88
+ <DialogPrimitive.Overlay
89
+ ref={composedRef}
90
+ onMouseUp={onMouseUp}
91
+ data-snap-points={isOpen && hasSnapPoints ? "true" : "false"}
92
+ data-snap-points-overlay={isOpen && shouldFade ? "true" : "false"}
93
+ data-animate={shouldAnimate?.current ? "true" : "false"}
94
+ data-should-overlay-animate={shouldOverlayAnimate ? "true" : "false"}
95
+ data-open={dataAttr(isOpen)}
96
+ {...props}
97
+ />
98
+ );
99
+ });
100
+ DrawerBackdrop.displayName = "DrawerBackdrop";
101
+
102
+ export interface DrawerContentProps
103
+ extends DialogPrimitive.DialogContentProps,
104
+ React.HTMLAttributes<HTMLDivElement> {}
105
+
106
+ export const DrawerContent = forwardRef<HTMLDivElement, DrawerContentProps>((props, ref) => {
107
+ const { onPointerDownOutside, style, onOpenAutoFocus, ...restProps } = props;
108
+ const {
109
+ drawerRef,
110
+ onPress,
111
+ onRelease,
112
+ onDrag,
113
+ keyboardIsOpen,
114
+ snapPointsOffset,
115
+ activeSnapPointIndex,
116
+ modal,
117
+ isOpen,
118
+ direction,
119
+ snapPoints,
120
+ container,
121
+ handleOnly,
122
+ shouldAnimate,
123
+ autoFocus,
124
+ closeDrawer,
125
+ closeOnInteractOutside,
126
+ closeOnEscape,
127
+ dismissible,
128
+ } = useDrawerContext();
129
+ // Needed to use transition instead of animations
130
+ const [delayedSnapPoints, setDelayedSnapPoints] = useState(false);
131
+ const composedRef = useComposedRefs(ref, drawerRef);
132
+ const pointerStartRef = useRef<{ x: number; y: number } | null>(null);
133
+ const lastKnownPointerEventRef = useRef<React.PointerEvent<HTMLDivElement> | null>(null);
134
+ const wasBeyondThePointRef = useRef(false);
135
+ const hasSnapPoints = snapPoints && snapPoints.length > 0;
136
+
137
+ const isDeltaInDirection = (
138
+ delta: { x: number; y: number },
139
+ direction: DrawerDirection,
140
+ threshold = 0,
141
+ ) => {
142
+ if (wasBeyondThePointRef.current) return true;
143
+
144
+ const deltaY = Math.abs(delta.y);
145
+ const deltaX = Math.abs(delta.x);
146
+ const isDeltaX = deltaX > deltaY;
147
+ const dFactor = ["bottom", "right"].includes(direction) ? 1 : -1;
148
+
149
+ if (direction === "left" || direction === "right") {
150
+ const isReverseDirection = delta.x * dFactor < 0;
151
+ if (!isReverseDirection && deltaX >= 0 && deltaX <= threshold) {
152
+ return isDeltaX;
153
+ }
154
+ } else {
155
+ const isReverseDirection = delta.y * dFactor < 0;
156
+ if (!isReverseDirection && deltaY >= 0 && deltaY <= threshold) {
157
+ return !isDeltaX;
158
+ }
159
+ }
160
+
161
+ wasBeyondThePointRef.current = true;
162
+ return true;
163
+ };
164
+
165
+ useEffect(() => {
166
+ if (hasSnapPoints) {
167
+ window.requestAnimationFrame(() => {
168
+ setDelayedSnapPoints(true);
169
+ });
170
+ }
171
+ }, []);
172
+
173
+ function handleOnPointerUp(event: React.PointerEvent<HTMLDivElement> | null) {
174
+ pointerStartRef.current = null;
175
+ wasBeyondThePointRef.current = false;
176
+ onRelease(event);
177
+ }
178
+
179
+ return (
180
+ <DialogPrimitive.Content
181
+ data-delayed-snap-points={delayedSnapPoints ? "true" : "false"}
182
+ data-drawer-direction={direction}
183
+ data-open={dataAttr(isOpen)}
184
+ data-drawer=""
185
+ data-snap-points={isOpen && hasSnapPoints ? "true" : "false"}
186
+ data-custom-container={container ? "true" : "false"}
187
+ data-animate={shouldAnimate?.current ? "true" : "false"}
188
+ {...restProps}
189
+ ref={composedRef}
190
+ style={
191
+ snapPointsOffset && snapPointsOffset.length > 0
192
+ ? ({
193
+ "--snap-point-height": `${snapPointsOffset[activeSnapPointIndex ?? 0]!}px`,
194
+ ...style,
195
+ } as React.CSSProperties)
196
+ : (style ?? {})
197
+ }
198
+ onPointerDown={(event) => {
199
+ if (handleOnly) return;
200
+ restProps.onPointerDown?.(event);
201
+ pointerStartRef.current = { x: event.pageX, y: event.pageY };
202
+ onPress(event);
203
+ }}
204
+ onOpenAutoFocus={(e) => {
205
+ onOpenAutoFocus?.(e);
206
+
207
+ if (!autoFocus) {
208
+ e.preventDefault();
209
+ }
210
+ }}
211
+ onPointerDownOutside={(e) => {
212
+ onPointerDownOutside?.(e);
213
+
214
+ if (!modal || e.defaultPrevented) {
215
+ e.preventDefault();
216
+ return;
217
+ }
218
+
219
+ if (keyboardIsOpen.current) {
220
+ keyboardIsOpen.current = false;
221
+ }
222
+ }}
223
+ onFocusOutside={(e) => {
224
+ if (!modal) {
225
+ e.preventDefault();
226
+ return;
227
+ }
228
+ }}
229
+ onPointerMove={(event) => {
230
+ lastKnownPointerEventRef.current = event;
231
+ if (handleOnly) return;
232
+ restProps.onPointerMove?.(event);
233
+ if (!pointerStartRef.current) return;
234
+ const yPosition = event.pageY - pointerStartRef.current.y;
235
+ const xPosition = event.pageX - pointerStartRef.current.x;
236
+
237
+ const swipeStartThreshold = event.pointerType === "touch" ? 10 : 2;
238
+ const delta = { x: xPosition, y: yPosition };
239
+
240
+ const isAllowedToSwipe = isDeltaInDirection(delta, direction, swipeStartThreshold);
241
+ if (isAllowedToSwipe) onDrag(event);
242
+ else if (
243
+ Math.abs(xPosition) > swipeStartThreshold ||
244
+ Math.abs(yPosition) > swipeStartThreshold
245
+ ) {
246
+ pointerStartRef.current = null;
247
+ }
248
+ }}
249
+ onPointerUp={(event) => {
250
+ restProps.onPointerUp?.(event);
251
+ pointerStartRef.current = null;
252
+ wasBeyondThePointRef.current = false;
253
+ onRelease(event);
254
+ }}
255
+ onPointerOut={(event) => {
256
+ restProps.onPointerOut?.(event);
257
+ handleOnPointerUp(lastKnownPointerEventRef.current);
258
+ }}
259
+ onContextMenu={(event) => {
260
+ restProps.onContextMenu?.(event);
261
+ if (lastKnownPointerEventRef.current) {
262
+ handleOnPointerUp(lastKnownPointerEventRef.current);
263
+ }
264
+ }}
265
+ onInteractOutside={(e) => {
266
+ if (dismissible && closeOnInteractOutside) {
267
+ closeDrawer();
268
+ }
269
+ props.onInteractOutside?.(e);
270
+ }}
271
+ onEscapeKeyDown={(e) => {
272
+ if (dismissible && closeOnEscape) {
273
+ closeDrawer();
274
+ }
275
+ props.onEscapeKeyDown?.(e);
276
+ }}
277
+ />
278
+ );
279
+ });
280
+ DrawerContent.displayName = "DrawerContent";
281
+
282
+ export interface DrawerTitleProps extends DialogPrimitive.DialogTitleProps {}
283
+
284
+ export const DrawerTitle = DialogPrimitive.Title;
285
+
286
+ export interface DrawerDescriptionProps extends DialogPrimitive.DialogDescriptionProps {}
287
+
288
+ export const DrawerDescription = DialogPrimitive.Description;
289
+
290
+ export interface DrawerCloseButtonProps extends DialogPrimitive.DialogCloseProps {}
291
+
292
+ export const DrawerCloseButton = forwardRef<HTMLButtonElement, DrawerCloseButtonProps>(
293
+ (props, ref) => {
294
+ const api = useDrawerContext();
295
+ return (
296
+ <Primitive.button
297
+ ref={ref}
298
+ {...props}
299
+ onClick={(e) => {
300
+ props.onClick?.(e);
301
+ if (e.defaultPrevented) return;
302
+ api.setIsOpen(false);
303
+ }}
304
+ />
305
+ );
306
+ },
307
+ );
308
+
309
+ export interface DrawerHandleProps extends React.HTMLAttributes<HTMLDivElement> {
310
+ preventCycle?: boolean;
311
+ }
312
+
313
+ const LONG_HANDLE_PRESS_TIMEOUT = 250;
314
+ const DOUBLE_TAP_TIMEOUT = 120;
315
+
316
+ export const DrawerHandle = forwardRef<HTMLDivElement, DrawerHandleProps>((props, ref) => {
317
+ const { preventCycle = false, children, ...rest } = props;
318
+ const {
319
+ closeDrawer,
320
+ isDragging,
321
+ snapPoints,
322
+ activeSnapPoint,
323
+ setActiveSnapPoint,
324
+ dismissible,
325
+ handleOnly,
326
+ isOpen,
327
+ onPress,
328
+ onDrag,
329
+ } = useDrawerContext();
330
+
331
+ const closeTimeoutIdRef = useRef<number | null>(null);
332
+ const shouldCancelInteractionRef = useRef(false);
333
+
334
+ function handleStartCycle() {
335
+ // Stop if this is the second click of a double click
336
+ if (shouldCancelInteractionRef.current) {
337
+ handleCancelInteraction();
338
+ return;
339
+ }
340
+ window.setTimeout(() => {
341
+ handleCycleSnapPoints();
342
+ }, DOUBLE_TAP_TIMEOUT);
343
+ }
344
+
345
+ function handleCycleSnapPoints() {
346
+ // Prevent accidental taps while resizing drawer
347
+ if (isDragging || preventCycle || shouldCancelInteractionRef.current) {
348
+ handleCancelInteraction();
349
+ return;
350
+ }
351
+ // Make sure to clear the timeout id if the user releases the handle before the cancel timeout
352
+ handleCancelInteraction();
353
+
354
+ if (!snapPoints || snapPoints.length === 0) {
355
+ if (!dismissible) {
356
+ closeDrawer();
357
+ }
358
+ return;
359
+ }
360
+
361
+ const isLastSnapPoint = activeSnapPoint === snapPoints[snapPoints.length - 1];
362
+
363
+ if (isLastSnapPoint && dismissible) {
364
+ closeDrawer();
365
+ return;
366
+ }
367
+
368
+ const currentSnapIndex = snapPoints.findIndex((point) => point === activeSnapPoint);
369
+ if (currentSnapIndex === -1) return; // activeSnapPoint not found in snapPoints
370
+ const nextSnapPoint = snapPoints[currentSnapIndex + 1];
371
+ setActiveSnapPoint(nextSnapPoint);
372
+ }
373
+
374
+ function handleStartInteraction() {
375
+ closeTimeoutIdRef.current = window.setTimeout(() => {
376
+ // Cancel click interaction on a long press
377
+ shouldCancelInteractionRef.current = true;
378
+ }, LONG_HANDLE_PRESS_TIMEOUT);
379
+ }
380
+
381
+ function handleCancelInteraction() {
382
+ if (closeTimeoutIdRef.current) {
383
+ window.clearTimeout(closeTimeoutIdRef.current);
384
+ }
385
+ shouldCancelInteractionRef.current = false;
386
+ }
387
+
388
+ return (
389
+ <Primitive.div
390
+ ref={ref}
391
+ onClick={handleStartCycle}
392
+ onPointerCancel={handleCancelInteraction}
393
+ onPointerDown={(e) => {
394
+ if (handleOnly) onPress(e);
395
+ handleStartInteraction();
396
+ }}
397
+ onPointerMove={(e) => {
398
+ if (handleOnly) onDrag(e);
399
+ }}
400
+ // onPointerUp is already handled by the content component
401
+ data-drawer-visible={isOpen ? "true" : "false"}
402
+ data-handle=""
403
+ aria-hidden="true"
404
+ {...rest}
405
+ >
406
+ {children}
407
+ </Primitive.div>
408
+ );
409
+ });
410
+ DrawerHandle.displayName = "DrawerHandle";
package/src/browser.ts ADDED
@@ -0,0 +1,37 @@
1
+ export function isMobileFirefox(): boolean | undefined {
2
+ if (typeof window === "undefined" || typeof navigator === "undefined") return false;
3
+ return (
4
+ (/Firefox/.test(navigator.userAgent) && /Mobile/.test(navigator.userAgent)) ||
5
+ /FxiOS/.test(navigator.userAgent)
6
+ );
7
+ }
8
+
9
+ export function isMac(): boolean | undefined {
10
+ return testPlatform(/^Mac/);
11
+ }
12
+
13
+ export function isIPhone(): boolean | undefined {
14
+ return testPlatform(/^iPhone/);
15
+ }
16
+
17
+ export function isSafari(): boolean | undefined {
18
+ return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
19
+ }
20
+
21
+ export function isIPad(): boolean | undefined {
22
+ return (
23
+ testPlatform(/^iPad/) ||
24
+ // iPadOS 13 lies and says it's a Mac, but we can distinguish by detecting touch support.
25
+ (isMac() && navigator.maxTouchPoints > 1)
26
+ );
27
+ }
28
+
29
+ export function isIOS(): boolean | undefined {
30
+ return isIPhone() || isIPad();
31
+ }
32
+
33
+ export function testPlatform(re: RegExp): boolean | undefined {
34
+ return typeof window !== "undefined" && window.navigator != null
35
+ ? re.test(window.navigator.platform)
36
+ : undefined;
37
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * TODO: move to recipe
3
+ */
4
+ export const TRANSITIONS = {
5
+ ENTER_DURATION: 0.3, // $duration.d6
6
+ EXIT_DURATION: 0.2, // $duration.d4
7
+ OVERLAY_ENTER_TIMING_FUNCTION: "cubic-bezier(0, 0, 0.15, 1)", // $timing-function.enter
8
+ OVERLAY_EXIT_TIMING_FUNCTION: "cubic-bezier(0.35, 0, 1, 1)", // $timing-function.exit
9
+ CONTENT_ENTER_TIMING_FUNCTION: "cubic-bezier(0.03, 0.4, 0.1, 1)", // $timing-function.enter-expressive
10
+ CONTENT_EXIT_TIMING_FUNCTION: "cubic-bezier(0.35, 0, 1, 1)", // $timing-function.exit
11
+ };
12
+
13
+ export const VELOCITY_THRESHOLD = 0.4;
14
+
15
+ export const CLOSE_THRESHOLD = 0.25;
16
+
17
+ export const SCROLL_LOCK_TIMEOUT = 100;
18
+
19
+ export const WINDOW_TOP_OFFSET = 26;
20
+
21
+ export const DRAG_CLASS = "seed-dragging";
package/src/helpers.ts ADDED
@@ -0,0 +1,124 @@
1
+ import type { AnyFunction, DrawerDirection } from "./types";
2
+
3
+ interface Style {
4
+ [key: string]: string;
5
+ }
6
+
7
+ const cache = new WeakMap();
8
+
9
+ export function isInView(el: HTMLElement): boolean {
10
+ const rect = el.getBoundingClientRect();
11
+
12
+ if (!window.visualViewport) return false;
13
+
14
+ return (
15
+ rect.top >= 0 &&
16
+ rect.left >= 0 &&
17
+ // Need + 40 for safari detection
18
+ rect.bottom <= window.visualViewport.height - 40 &&
19
+ rect.right <= window.visualViewport.width
20
+ );
21
+ }
22
+
23
+ export function set(
24
+ el: Element | HTMLElement | null | undefined,
25
+ styles: Style,
26
+ ignoreCache = false,
27
+ ) {
28
+ if (!el || !(el instanceof HTMLElement)) return;
29
+ let originalStyles: Style = {};
30
+
31
+ Object.entries(styles).forEach(([key, value]: [string, string]) => {
32
+ if (key.startsWith("--")) {
33
+ el.style.setProperty(key, value);
34
+ return;
35
+ }
36
+
37
+ originalStyles[key] = (el.style as any)[key];
38
+ (el.style as any)[key] = value;
39
+ });
40
+
41
+ if (ignoreCache) return;
42
+
43
+ cache.set(el, originalStyles);
44
+ }
45
+
46
+ export function reset(el: Element | HTMLElement | null, prop?: string) {
47
+ if (!el || !(el instanceof HTMLElement)) return;
48
+ let originalStyles = cache.get(el);
49
+
50
+ if (!originalStyles) {
51
+ return;
52
+ }
53
+
54
+ if (prop) {
55
+ (el.style as any)[prop] = originalStyles[prop];
56
+ } else {
57
+ Object.entries(originalStyles).forEach(([key, value]) => {
58
+ (el.style as any)[key] = value;
59
+ });
60
+ }
61
+ }
62
+
63
+ export const isVertical = (direction: DrawerDirection) => {
64
+ switch (direction) {
65
+ case "top":
66
+ case "bottom":
67
+ return true;
68
+ case "left":
69
+ case "right":
70
+ return false;
71
+ default:
72
+ return direction satisfies never;
73
+ }
74
+ };
75
+
76
+ export function getTranslate(element: HTMLElement, direction: DrawerDirection): number | null {
77
+ if (!element) {
78
+ return null;
79
+ }
80
+ const style = window.getComputedStyle(element);
81
+ const transform =
82
+ // @ts-ignore
83
+ style.transform || style.webkitTransform || style.mozTransform;
84
+ let mat = transform.match(/^matrix3d\((.+)\)$/);
85
+ if (mat) {
86
+ // https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/matrix3d
87
+ return Number.parseFloat(mat[1].split(", ")[isVertical(direction) ? 13 : 12]);
88
+ }
89
+ // https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/matrix
90
+ mat = transform.match(/^matrix\((.+)\)$/);
91
+ return mat ? Number.parseFloat(mat[1].split(", ")[isVertical(direction) ? 5 : 4]) : null;
92
+ }
93
+
94
+ export function dampenValue(v: number) {
95
+ return 8 * (Math.log(v + 1) - 2);
96
+ }
97
+
98
+ export function assignStyle(
99
+ element: HTMLElement | null | undefined,
100
+ style: Partial<CSSStyleDeclaration>,
101
+ ) {
102
+ if (!element) return () => {};
103
+
104
+ const prevStyle = element.style.cssText;
105
+ Object.assign(element.style, style);
106
+
107
+ return () => {
108
+ element.style.cssText = prevStyle;
109
+ };
110
+ }
111
+
112
+ /**
113
+ * Receives functions as arguments and returns a new function that calls all.
114
+ */
115
+ export function chain<T>(...fns: T[]) {
116
+ return (...args: T extends AnyFunction ? Parameters<T> : never) => {
117
+ for (const fn of fns) {
118
+ if (typeof fn === "function") {
119
+ // @ts-ignore
120
+ fn(...args);
121
+ }
122
+ }
123
+ };
124
+ }
package/src/index.ts ADDED
@@ -0,0 +1,25 @@
1
+ export {
2
+ DrawerBackdrop,
3
+ DrawerCloseButton,
4
+ DrawerContent,
5
+ DrawerDescription,
6
+ DrawerHandle,
7
+ DrawerPositioner,
8
+ DrawerRoot,
9
+ DrawerTitle,
10
+ DrawerTrigger,
11
+ type DrawerBackdropProps,
12
+ type DrawerCloseButtonProps,
13
+ type DrawerContentProps,
14
+ type DrawerDescriptionProps,
15
+ type DrawerHandleProps,
16
+ type DrawerPositionerProps,
17
+ type DrawerRootProps,
18
+ type DrawerTitleProps,
19
+ type DrawerTriggerProps,
20
+ } from "./Drawer";
21
+
22
+ export { useDrawer, type DialogProps } from "./useDrawer";
23
+ export { useDrawerContext, type DrawerContextValue } from "./useDrawerContext";
24
+
25
+ export * as Drawer from "./Drawer.namespace";
package/src/types.ts ADDED
@@ -0,0 +1,7 @@
1
+ export type DrawerDirection = "top" | "bottom" | "left" | "right";
2
+ export interface SnapPoint {
3
+ fraction: number;
4
+ height: number;
5
+ }
6
+
7
+ export type AnyFunction = (...args: any) => any;