@musecat/uikit 0.1.1

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.
Files changed (180) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +51 -0
  3. package/index.ts +78 -0
  4. package/package.json +103 -0
  5. package/packages/Components/Box/Box.tsx +173 -0
  6. package/packages/Components/Box/Box.types.ts +48 -0
  7. package/packages/Components/Button/Button.tsx +275 -0
  8. package/packages/Components/Button/Button.types.ts +44 -0
  9. package/packages/Components/Card/Card.default.tsx +180 -0
  10. package/packages/Components/Card/Card.foldable.tsx +383 -0
  11. package/packages/Components/Card/Card.module.scss +23 -0
  12. package/packages/Components/Card/Card.tsx +8 -0
  13. package/packages/Components/Card/Card.types.ts +91 -0
  14. package/packages/Components/Card/Card.utils.ts +66 -0
  15. package/packages/Components/Checkbox/Checkbox.module.scss +11 -0
  16. package/packages/Components/Checkbox/Checkbox.tsx +104 -0
  17. package/packages/Components/Checkbox/Checkbox.types.ts +24 -0
  18. package/packages/Components/CodeBox/CodeBox.copy.tsx +43 -0
  19. package/packages/Components/CodeBox/CodeBox.module.scss +72 -0
  20. package/packages/Components/CodeBox/CodeBox.tsx +91 -0
  21. package/packages/Components/CodeBox/CodeBox.types.ts +17 -0
  22. package/packages/Components/ContextMenu/ContextMenu.module.scss +8 -0
  23. package/packages/Components/ContextMenu/ContextMenu.options.tsx +126 -0
  24. package/packages/Components/ContextMenu/ContextMenu.tsx +381 -0
  25. package/packages/Components/ContextMenu/ContextMenu.types.ts +46 -0
  26. package/packages/Components/ContributionGraph/ContributionGraph.module.scss +42 -0
  27. package/packages/Components/ContributionGraph/ContributionGraph.tsx +454 -0
  28. package/packages/Components/ContributionGraph/ContributionGraph.types.ts +27 -0
  29. package/packages/Components/DatePicker/DatePicker.calendar.tsx +161 -0
  30. package/packages/Components/DatePicker/DatePicker.core.tsx +582 -0
  31. package/packages/Components/DatePicker/DatePicker.module.scss +26 -0
  32. package/packages/Components/DatePicker/DatePicker.tsx +786 -0
  33. package/packages/Components/DatePicker/DatePicker.types.ts +36 -0
  34. package/packages/Components/DatePicker/DatePicker.utils.ts +80 -0
  35. package/packages/Components/DatePicker/hooks/useCalendar.ts +61 -0
  36. package/packages/Components/DatePicker/hooks/useSelection.ts +208 -0
  37. package/packages/Components/Divider/Divider.tsx +46 -0
  38. package/packages/Components/Divider/Divider.types.ts +12 -0
  39. package/packages/Components/Header/Header.module.scss +74 -0
  40. package/packages/Components/Header/Header.tsx +47 -0
  41. package/packages/Components/Header/Header.types.ts +8 -0
  42. package/packages/Components/Icon/Icon.group.tsx +122 -0
  43. package/packages/Components/Icon/Icon.group.types.ts +20 -0
  44. package/packages/Components/Icon/Icon.tsx +341 -0
  45. package/packages/Components/Icon/Icon.types.ts +59 -0
  46. package/packages/Components/Icon/SVG/data.tsx +232 -0
  47. package/packages/Components/Input/Input.module.scss +5 -0
  48. package/packages/Components/Input/Input.tsx +87 -0
  49. package/packages/Components/Input/Input.types.ts +14 -0
  50. package/packages/Components/Label/Label.module.scss +19 -0
  51. package/packages/Components/Label/Label.tsx +115 -0
  52. package/packages/Components/Label/Label.types.ts +28 -0
  53. package/packages/Components/Layout/Layout.docs.module.scss +42 -0
  54. package/packages/Components/Layout/Layout.docs.tsx +150 -0
  55. package/packages/Components/Layout/Layout.docs.types.ts +18 -0
  56. package/packages/Components/Layout/Layout.module.scss +91 -0
  57. package/packages/Components/Layout/Layout.tsx +306 -0
  58. package/packages/Components/Layout/Layout.types.ts +71 -0
  59. package/packages/Components/Maps/OSM/MapOSM.module.scss +11 -0
  60. package/packages/Components/Maps/OSM/MapOSM.shared.ts +121 -0
  61. package/packages/Components/Maps/OSM/MapOSM.tsx +279 -0
  62. package/packages/Components/Maps/OSM/MapOSM.types.ts +18 -0
  63. package/packages/Components/Nav/Nav.module.scss +137 -0
  64. package/packages/Components/Nav/Nav.tsx +304 -0
  65. package/packages/Components/Nav/Nav.types.ts +32 -0
  66. package/packages/Components/Nav/hooks/useNavDrag.ts +374 -0
  67. package/packages/Components/Nav/hooks/useNavIndicator.ts +137 -0
  68. package/packages/Components/Pagination/Pagination.tsx +254 -0
  69. package/packages/Components/Pagination/Pagination.types.ts +18 -0
  70. package/packages/Components/Pill/Pill.tsx +127 -0
  71. package/packages/Components/Pill/Pill.types.ts +33 -0
  72. package/packages/Components/Profile/Profile.shared.ts +74 -0
  73. package/packages/Components/Profile/Profile.tsx +382 -0
  74. package/packages/Components/Profile/Profile.types.ts +55 -0
  75. package/packages/Components/Progress/Progress.module.scss +15 -0
  76. package/packages/Components/Progress/Progress.tsx +88 -0
  77. package/packages/Components/Progress/Progress.types.ts +22 -0
  78. package/packages/Components/Radio/Radio.module.scss +11 -0
  79. package/packages/Components/Radio/Radio.tsx +121 -0
  80. package/packages/Components/Radio/Radio.types.ts +20 -0
  81. package/packages/Components/Select/Select.keyboard.tsx +76 -0
  82. package/packages/Components/Select/Select.trigger.tsx +88 -0
  83. package/packages/Components/Select/Select.tsx +11 -0
  84. package/packages/Components/Select/Select.types.ts +38 -0
  85. package/packages/Components/Select/Select.utils.ts +84 -0
  86. package/packages/Components/Select/hooks/useSelectNavigation.ts +461 -0
  87. package/packages/Components/Select/hooks/useSelectSelection.ts +142 -0
  88. package/packages/Components/Select/hooks/useSelectState.ts +314 -0
  89. package/packages/Components/Select/select.control.tsx +228 -0
  90. package/packages/Components/Select/select.inner.tsx +312 -0
  91. package/packages/Components/Skeleton/Skeleton.tsx +30 -0
  92. package/packages/Components/Skeleton/Skeleton.types.ts +12 -0
  93. package/packages/Components/Spinner/Spinner.module.scss +78 -0
  94. package/packages/Components/Spinner/Spinner.tsx +125 -0
  95. package/packages/Components/Spinner/Spinner.types.ts +19 -0
  96. package/packages/Components/Text/Text.module.scss +7 -0
  97. package/packages/Components/Text/Text.tsx +49 -0
  98. package/packages/Components/Text/Text.types.ts +33 -0
  99. package/packages/Components/TimePicker/TimePicker.core.tsx +448 -0
  100. package/packages/Components/TimePicker/TimePicker.module.scss +30 -0
  101. package/packages/Components/TimePicker/TimePicker.tsx +511 -0
  102. package/packages/Components/TimePicker/TimePicker.types.ts +22 -0
  103. package/packages/Components/Timeline/Timeline.tsx +111 -0
  104. package/packages/Components/Timeline/Timeline.types.ts +23 -0
  105. package/packages/Components/Title/Title.tsx +305 -0
  106. package/packages/Components/Title/Title.types.ts +64 -0
  107. package/packages/Components/Toggle/Toggle.module.scss +32 -0
  108. package/packages/Components/Toggle/Toggle.tsx +272 -0
  109. package/packages/Components/Toggle/Toggle.types.ts +17 -0
  110. package/packages/Components/Tooltip/Tooltip.module.scss +44 -0
  111. package/packages/Components/Tooltip/Tooltip.tsx +340 -0
  112. package/packages/Components/Tooltip/Tooltip.types.ts +38 -0
  113. package/packages/Frameworks/Dialog/Dialog.background.tsx +88 -0
  114. package/packages/Frameworks/Dialog/Dialog.boot.tsx +32 -0
  115. package/packages/Frameworks/Dialog/Dialog.store.ts +199 -0
  116. package/packages/Frameworks/Dialog/Dialog.tsx +185 -0
  117. package/packages/Frameworks/Dialog/Dialog.types.ts +264 -0
  118. package/packages/Frameworks/Dialog/Dialog.utils.ts +22 -0
  119. package/packages/Frameworks/Dialog/contents/Dialog.footer.tsx +79 -0
  120. package/packages/Frameworks/Dialog/contents/Dialog.funnel.tsx +260 -0
  121. package/packages/Frameworks/Dialog/contents/Dialog.header.tsx +137 -0
  122. package/packages/Frameworks/Dialog/hooks/useDialogPosition.ts +298 -0
  123. package/packages/Frameworks/Dialog/hooks/useSheetDrag.ts +325 -0
  124. package/packages/Frameworks/Dialog/hooks/useSheetGeometry.ts +201 -0
  125. package/packages/Frameworks/Dialog/hooks/useSheetProgressive.ts +98 -0
  126. package/packages/Frameworks/Dialog/renderers/renderModal.tsx +228 -0
  127. package/packages/Frameworks/Dialog/renderers/renderPopover.tsx +220 -0
  128. package/packages/Frameworks/Dialog/renderers/renderSheet.tsx +571 -0
  129. package/packages/Frameworks/EdgeEffect/EdgeEffect.tsx +52 -0
  130. package/packages/Frameworks/EdgeEffect/EdgeEffect.types.ts +5 -0
  131. package/packages/Frameworks/Motion/Motion.presets.ts +49 -0
  132. package/packages/Frameworks/Pressable/Pressable.module.scss +19 -0
  133. package/packages/Frameworks/Pressable/Pressable.tsx +651 -0
  134. package/packages/Frameworks/Pressable/Pressable.types.ts +73 -0
  135. package/packages/Frameworks/Squircle/Squircle.tsx +263 -0
  136. package/packages/Frameworks/Squircle/Squircle.types.ts +15 -0
  137. package/packages/Frameworks/Theme/Radius.types.ts +55 -0
  138. package/packages/Frameworks/Theme/Theme.types.ts +325 -0
  139. package/packages/Frameworks/Toaster/Toaster.boot.tsx +47 -0
  140. package/packages/Frameworks/Toaster/Toaster.module.scss +168 -0
  141. package/packages/Frameworks/Toaster/Toaster.types.ts +13 -0
  142. package/packages/Frameworks/View/DNDView/DNDView.module.scss +8 -0
  143. package/packages/Frameworks/View/DNDView/DNDView.tsx +470 -0
  144. package/packages/Frameworks/View/DNDView/DNDView.types.ts +61 -0
  145. package/packages/Frameworks/View/HScrollView/HScrollView.module.scss +59 -0
  146. package/packages/Frameworks/View/HScrollView/HScrollView.tsx +386 -0
  147. package/packages/Frameworks/View/HScrollView/HScrollView.types.ts +46 -0
  148. package/packages/Frameworks/View/ImageView/Dialog/ImageDialog.counter.tsx +27 -0
  149. package/packages/Frameworks/View/ImageView/Dialog/ImageDialog.footer.tsx +76 -0
  150. package/packages/Frameworks/View/ImageView/Dialog/ImageDialog.header.tsx +49 -0
  151. package/packages/Frameworks/View/ImageView/Dialog/ImageDialog.list.tsx +74 -0
  152. package/packages/Frameworks/View/ImageView/Dialog/ImageDialog.slide.tsx +71 -0
  153. package/packages/Frameworks/View/ImageView/Dialog/ImageDialog.tsx +175 -0
  154. package/packages/Frameworks/View/ImageView/Image.controls.tsx +39 -0
  155. package/packages/Frameworks/View/ImageView/Image.hooks.ts +82 -0
  156. package/packages/Frameworks/View/ImageView/Image.module.scss +7 -0
  157. package/packages/Frameworks/View/ImageView/Image.tsx +375 -0
  158. package/packages/Frameworks/View/ImageView/Image.types.ts +71 -0
  159. package/packages/Frameworks/View/ImageView/Image.utils.ts +79 -0
  160. package/packages/Frameworks/View/View.module.scss +34 -0
  161. package/packages/Frameworks/View/View.tsx +200 -0
  162. package/packages/Frameworks/View/View.types.ts +36 -0
  163. package/packages/Frameworks/_shared/Padding.types.ts +52 -0
  164. package/packages/Frameworks/_shared/StopParentInteraction.tsx +45 -0
  165. package/packages/Frameworks/_shared/Wind.types.ts +17 -0
  166. package/packages/Frameworks/_shared/bodyScrollLock.ts +127 -0
  167. package/packages/Frameworks/_shared/layer.constants.ts +8 -0
  168. package/packages/Frameworks/_shared/normalize.ts +39 -0
  169. package/packages/Frameworks/_shared/sizing.ts +55 -0
  170. package/packages/Frameworks/_shared/useControllableState.ts +29 -0
  171. package/packages/Styles/_animation.scss +42 -0
  172. package/packages/Styles/_color.scss +131 -0
  173. package/packages/Styles/_font.scss +30 -0
  174. package/packages/Styles/_icon.scss +782 -0
  175. package/packages/Styles/_importer.scss +9 -0
  176. package/packages/Styles/_system.scss +69 -0
  177. package/packages/Styles/_textstyle.scss +111 -0
  178. package/packages/Styles/_theme.scss +452 -0
  179. package/packages/Styles/_viewport-global.scss +99 -0
  180. package/packages/Styles/_viewport.scss +108 -0
@@ -0,0 +1,298 @@
1
+ import { useViewportHeight } from "@musecat/functionkit";
2
+ import { useCallback, useLayoutEffect, useState } from "react";
3
+ import { SizePX } from "@/packages/Frameworks/_shared/sizing";
4
+ import type { OffsetConfig } from "@/packages/Frameworks/Dialog/Dialog.types";
5
+
6
+ export type PopoverPlacement =
7
+ | "top"
8
+ | "top-start"
9
+ | "top-end"
10
+ | "bottom"
11
+ | "bottom-start"
12
+ | "bottom-end"
13
+ | "left"
14
+ | "right";
15
+
16
+ interface UseDialogPositionProps {
17
+ open: boolean;
18
+ anchorRef: React.RefObject<HTMLElement | null>;
19
+ contentRef: React.RefObject<HTMLElement | null>;
20
+ placement?: PopoverPlacement;
21
+ strategy?: "anchored" | "center-selected";
22
+ selectedItemSelector?: string;
23
+ matchAnchorWidth?: boolean;
24
+ coverAnchor?: boolean;
25
+ gap?: number;
26
+ margin?: number;
27
+ recalcKey?: string | number;
28
+ offset?: OffsetConfig;
29
+ }
30
+
31
+ export function useDialogPosition(props: UseDialogPositionProps) {
32
+ const { height: viewportHeight } = useViewportHeight();
33
+ const {
34
+ open,
35
+ anchorRef,
36
+ contentRef,
37
+ placement = "bottom-start",
38
+ strategy = "anchored",
39
+ selectedItemSelector = '[data-selected="true"]',
40
+ matchAnchorWidth = false,
41
+ coverAnchor = false,
42
+ gap = 12,
43
+ margin = 8,
44
+ offset,
45
+ } = props;
46
+ const [topValue, setTopValue] = useState<number | null>(null);
47
+ const [leftValue, setLeftValue] = useState<number | null>(null);
48
+ const [maxHeightValue, setMaxHeightValue] = useState(400);
49
+ const [isPositionReady, setIsPositionReady] = useState(false);
50
+ const [computedWidth, setComputedWidth] = useState<number | null>(null);
51
+ const [resolvedPlacement, setResolvedPlacement] =
52
+ useState<string>("bottom-left");
53
+
54
+ const _toPx = useCallback(
55
+ (value: number | string | undefined, fallback: number) => {
56
+ if (typeof window === "undefined") return fallback;
57
+ const rootFont =
58
+ parseFloat(
59
+ window.getComputedStyle(document.documentElement).fontSize,
60
+ ) || 16;
61
+ if (typeof value === "string") {
62
+ const normalized = value.trim().toLowerCase();
63
+ const numeric = parseFloat(normalized);
64
+ if (Number.isNaN(numeric)) return fallback;
65
+ if (normalized.endsWith("px")) return numeric;
66
+ if (normalized.endsWith("vh"))
67
+ return window.innerHeight * (numeric / 100);
68
+ if (normalized.endsWith("vw"))
69
+ return window.innerWidth * (numeric / 100);
70
+ }
71
+ return SizePX(value, fallback, rootFont);
72
+ },
73
+ [],
74
+ );
75
+
76
+ const getPlacementPosition = useCallback(
77
+ (
78
+ row: "top" | "middle" | "bottom",
79
+ col: "left" | "center" | "right",
80
+ anchorRect: DOMRect,
81
+ contentRect: DOMRect,
82
+ ) => {
83
+ const controlTop = anchorRect.top;
84
+ const controlLeft = anchorRect.left;
85
+ const controlHeight = anchorRect.height;
86
+ const controlWidth = anchorRect.width;
87
+ const contentHeight = contentRect.height;
88
+ const contentWidth = contentRect.width;
89
+
90
+ const top =
91
+ row === "top"
92
+ ? controlTop - contentHeight - gap
93
+ : row === "middle"
94
+ ? controlTop + (controlHeight - contentHeight) / 2
95
+ : controlTop + controlHeight + gap;
96
+
97
+ const left =
98
+ col === "left"
99
+ ? row === "middle"
100
+ ? controlLeft - contentWidth - gap
101
+ : controlLeft
102
+ : col === "center"
103
+ ? controlLeft + (controlWidth - contentWidth) / 2
104
+ : row === "middle"
105
+ ? controlLeft + controlWidth + gap
106
+ : controlLeft + controlWidth - contentWidth;
107
+
108
+ return { top, left };
109
+ },
110
+ [gap],
111
+ );
112
+
113
+ const updatePosition = useCallback(() => {
114
+ if (!open || !anchorRef.current || !contentRef.current) return;
115
+
116
+ const anchorEl = anchorRef.current;
117
+ const contentEl = contentRef.current;
118
+ const anchorRect = anchorEl.getBoundingClientRect();
119
+ const contentRect = {
120
+ width: contentEl.offsetWidth || contentEl.clientWidth || 0,
121
+ height: contentEl.offsetHeight || contentEl.clientHeight || 0,
122
+ } as DOMRect;
123
+
124
+ if (matchAnchorWidth) {
125
+ setComputedWidth(anchorRect.width);
126
+ } else {
127
+ setComputedWidth(null);
128
+ }
129
+
130
+ let preferredTop = 0;
131
+ let preferredLeft = 0;
132
+ let finalPlacement = resolvedPlacement;
133
+
134
+ let row: "top" | "middle" | "bottom" = "bottom";
135
+ let col: "left" | "center" | "right" = "left";
136
+
137
+ if (placement === "top") {
138
+ row = "top";
139
+ col = "center";
140
+ } else if (placement === "top-start") {
141
+ row = "top";
142
+ col = "left";
143
+ } else if (placement === "top-end") {
144
+ row = "top";
145
+ col = "right";
146
+ } else if (placement === "bottom") {
147
+ row = "bottom";
148
+ col = "center";
149
+ } else if (placement === "bottom-start") {
150
+ row = "bottom";
151
+ col = "left";
152
+ } else if (placement === "bottom-end") {
153
+ row = "bottom";
154
+ col = "right";
155
+ } else if (placement === "left") {
156
+ row = "middle";
157
+ col = "left";
158
+ } else if (placement === "right") {
159
+ row = "middle";
160
+ col = "right";
161
+ }
162
+
163
+ if (strategy === "center-selected") {
164
+ const selectedEl = contentEl.querySelector(
165
+ selectedItemSelector,
166
+ ) as HTMLElement | null;
167
+ const firstOptionEl = contentEl.querySelector(
168
+ '[role="option"]',
169
+ ) as HTMLElement | null;
170
+ const targetEl = selectedEl ?? firstOptionEl;
171
+ if (targetEl) {
172
+ const controlCenterY = anchorRect.top + anchorRect.height / 2;
173
+ const targetCenterY = targetEl.offsetTop + targetEl.offsetHeight / 2;
174
+ preferredTop = controlCenterY - targetCenterY;
175
+ } else {
176
+ preferredTop = anchorRect.bottom + gap;
177
+ }
178
+ preferredLeft = anchorRect.left;
179
+ finalPlacement = "bottom-left";
180
+ } else {
181
+ const pos = getPlacementPosition(row, col, anchorRect, contentRect);
182
+ preferredTop = pos.top;
183
+ preferredLeft = pos.left;
184
+ finalPlacement = `${row}-${col}`;
185
+ }
186
+
187
+ if (coverAnchor) {
188
+ if (row === "bottom") {
189
+ preferredTop -= anchorRect.height + gap;
190
+ } else if (row === "top") {
191
+ preferredTop += anchorRect.height + gap;
192
+ } else if (row === "middle") {
193
+ if (col === "left") {
194
+ preferredLeft += anchorRect.width + gap;
195
+ } else if (col === "right") {
196
+ preferredLeft -= anchorRect.width + gap;
197
+ }
198
+ }
199
+ }
200
+
201
+ if (offset) {
202
+ const offsetX = _toPx(offset.x, 0);
203
+ const offsetY = _toPx(offset.y, 0);
204
+ preferredLeft += offsetX;
205
+ preferredTop += offsetY;
206
+ }
207
+
208
+ const minTop = margin;
209
+ const maxTop =
210
+ (viewportHeight || window.innerHeight) - margin - contentRect.height;
211
+ const nextTop = Math.max(minTop, Math.min(maxTop, preferredTop));
212
+ const minLeft = margin;
213
+ const maxLeft =
214
+ (window.visualViewport?.width ?? window.innerWidth) -
215
+ margin -
216
+ contentRect.width;
217
+ const nextLeft = Math.max(minLeft, Math.min(maxLeft, preferredLeft));
218
+
219
+ setTopValue(nextTop);
220
+ setLeftValue(nextLeft);
221
+ setResolvedPlacement(finalPlacement);
222
+
223
+ const availableHeight =
224
+ strategy === "center-selected"
225
+ ? (viewportHeight || window.innerHeight) - margin * 2
226
+ : row === "top"
227
+ ? anchorRect.top - gap - margin
228
+ : row === "bottom"
229
+ ? (viewportHeight || window.innerHeight) -
230
+ margin -
231
+ (anchorRect.bottom + gap)
232
+ : (viewportHeight || window.innerHeight) - margin * 2;
233
+ setMaxHeightValue(Math.max(100, availableHeight));
234
+ }, [
235
+ open,
236
+ anchorRef,
237
+ contentRef,
238
+ placement,
239
+ strategy,
240
+ selectedItemSelector,
241
+ matchAnchorWidth,
242
+ coverAnchor,
243
+ gap,
244
+ margin,
245
+ offset,
246
+ resolvedPlacement,
247
+ getPlacementPosition,
248
+ viewportHeight,
249
+ _toPx,
250
+ ]);
251
+
252
+ useLayoutEffect(() => {
253
+ if (!open) {
254
+ setIsPositionReady(false);
255
+ setTopValue(null);
256
+ setLeftValue(null);
257
+ return;
258
+ }
259
+ let raf = 0;
260
+ const schedule = () => {
261
+ cancelAnimationFrame(raf);
262
+ raf = requestAnimationFrame(() => {
263
+ updatePosition();
264
+ setIsPositionReady(true);
265
+ });
266
+ };
267
+
268
+ schedule();
269
+
270
+ const contentEl = contentRef.current;
271
+ const anchorEl = anchorRef.current;
272
+ let resizeObserver: ResizeObserver | null = null;
273
+ if (typeof ResizeObserver !== "undefined") {
274
+ resizeObserver = new ResizeObserver(() => schedule());
275
+ if (contentEl) resizeObserver.observe(contentEl);
276
+ if (anchorEl) resizeObserver.observe(anchorEl);
277
+ }
278
+
279
+ window.addEventListener("resize", schedule);
280
+ window.addEventListener("scroll", schedule, true);
281
+
282
+ return () => {
283
+ cancelAnimationFrame(raf);
284
+ resizeObserver?.disconnect();
285
+ window.removeEventListener("resize", schedule);
286
+ window.removeEventListener("scroll", schedule, true);
287
+ };
288
+ }, [open, updatePosition, anchorRef, contentRef]);
289
+
290
+ return {
291
+ topValue,
292
+ leftValue,
293
+ maxHeightValue,
294
+ resolvedPlacement,
295
+ computedWidth,
296
+ isPositionReady,
297
+ };
298
+ }
@@ -0,0 +1,325 @@
1
+ "use client";
2
+
3
+ import { animate, type MotionValue, useDragControls } from "motion/react";
4
+ import { useCallback, useRef } from "react";
5
+ import { toPx } from "@/packages/Frameworks/Dialog/hooks/useSheetGeometry";
6
+ import { motionTransitions } from "@/packages/Frameworks/Motion/Motion.presets";
7
+
8
+ const CLOSE_VELOCITY = 900;
9
+ const CLOSE_DISTANCE_RATIO = 0.34;
10
+ const SNAP_VELOCITY_PROJECTION = 0.18;
11
+
12
+ export interface DragHandlers {
13
+ handleDragEnd: (
14
+ _: MouseEvent | TouchEvent | PointerEvent,
15
+ info: { offset: { y: number }; velocity: { y: number } },
16
+ ) => void;
17
+ handleContentPointerDown: (e: React.PointerEvent) => void;
18
+ handleContentPointerMove: (e: React.PointerEvent) => void;
19
+ handleContentPointerUp: (e: React.PointerEvent) => void;
20
+ handleContentPointerCancel: () => void;
21
+ }
22
+
23
+ export interface UseSheetDragInput {
24
+ y: MotionValue<number>;
25
+ scrollRef: React.RefObject<HTMLDivElement | null>;
26
+ config?: {
27
+ disableDrag?: boolean;
28
+ freeDrag?: boolean;
29
+ min?: number | string;
30
+ snapPoints?: number[];
31
+ };
32
+ viewportHeight: number;
33
+ getViewportHeight: () => number;
34
+ sheetHeightPx: number;
35
+ isFreeDrag: boolean;
36
+ minPx: number;
37
+ maxPx: number;
38
+ maxSnap: number;
39
+ normalizedSnapPoints: number[];
40
+ resolvedFreeDragHeightPx: number;
41
+ isMinDefault: boolean;
42
+ snapIndex: number;
43
+ currentSnap: number;
44
+ getTargetYForSnap: (snap: number) => number;
45
+ getGapHiddenY: (
46
+ currentSnap: number,
47
+ isFreeDrag: boolean,
48
+ getTargetYForSnap: (snap: number) => number,
49
+ ) => number;
50
+ close: () => void;
51
+ cancelEntranceAnimation: () => void;
52
+ onSnapIndexChange?: (index: number) => void;
53
+ onDragStateChange?: (dragging: boolean) => void;
54
+ }
55
+
56
+ export function useSheetDrag({
57
+ y,
58
+ scrollRef,
59
+ config,
60
+ viewportHeight,
61
+ getViewportHeight,
62
+ sheetHeightPx,
63
+ isFreeDrag,
64
+ minPx,
65
+ maxPx,
66
+ maxSnap,
67
+ normalizedSnapPoints,
68
+ resolvedFreeDragHeightPx,
69
+ isMinDefault,
70
+ snapIndex,
71
+ currentSnap,
72
+ getTargetYForSnap,
73
+ getGapHiddenY,
74
+ close,
75
+ cancelEntranceAnimation,
76
+ onSnapIndexChange,
77
+ onDragStateChange,
78
+ }: UseSheetDragInput): DragHandlers {
79
+ const _dragControls = useDragControls();
80
+ const contentDraggingRef = useRef(false);
81
+ const contentPointerActiveRef = useRef(false);
82
+ const isScrollAtTopOnDownRef = useRef(false);
83
+ const contentDragStartY = useRef(0);
84
+ const contentDragSheetStartY = useRef(0);
85
+ const contentDragPrevY = useRef(0);
86
+ const contentDragPrevTime = useRef(0);
87
+
88
+ const handleDragEnd = useCallback(
89
+ (
90
+ _: MouseEvent | TouchEvent | PointerEvent,
91
+ info: { offset: { y: number }; velocity: { y: number } },
92
+ ) => {
93
+ const currentVh = viewportHeight || getViewportHeight();
94
+ const currentY = y.get();
95
+ const projectedY = currentY + info.velocity.y * SNAP_VELOCITY_PROJECTION;
96
+
97
+ if (isFreeDrag) {
98
+ const calculatedMinPx = toPx(
99
+ config?.min,
100
+ currentVh,
101
+ toPx("7.2rem", currentVh, 115.2),
102
+ );
103
+ const projectedHeightPx = resolvedFreeDragHeightPx - projectedY;
104
+
105
+ if (
106
+ config?.min === undefined &&
107
+ !isMinDefault &&
108
+ (info.velocity.y > CLOSE_VELOCITY ||
109
+ projectedHeightPx < calculatedMinPx * (1 - CLOSE_DISTANCE_RATIO))
110
+ ) {
111
+ onDragStateChange?.(false);
112
+ close();
113
+ return;
114
+ }
115
+
116
+ const minTargetY = Math.max(
117
+ 0,
118
+ resolvedFreeDragHeightPx - calculatedMinPx,
119
+ );
120
+ const maxTargetY = 0;
121
+ const targetY = Math.max(maxTargetY, Math.min(minTargetY, projectedY));
122
+
123
+ animate(y, targetY, motionTransitions.sheet.snap);
124
+ onDragStateChange?.(false);
125
+ return;
126
+ }
127
+
128
+ const projectedSnap = maxSnap - projectedY / currentVh;
129
+ const minSnap = normalizedSnapPoints[0] ?? 0.5;
130
+ const isAtLowestSnap = snapIndex === 0;
131
+ let releasedBelow: boolean;
132
+ if (normalizedSnapPoints.length === 1) {
133
+ releasedBelow = projectedY > (sheetHeightPx || currentVh) * 0.5;
134
+ } else {
135
+ releasedBelow = projectedSnap < minSnap * (1 - CLOSE_DISTANCE_RATIO);
136
+ }
137
+ const flickDown = info.velocity.y > CLOSE_VELOCITY && projectedY > 0;
138
+ if (
139
+ isAtLowestSnap &&
140
+ config?.min === undefined &&
141
+ (releasedBelow || flickDown)
142
+ ) {
143
+ onDragStateChange?.(false);
144
+ close();
145
+ return;
146
+ }
147
+
148
+ const nextIndex = normalizedSnapPoints.reduce(
149
+ (closestIndex, snap, index) => {
150
+ const closestDist = Math.abs(
151
+ normalizedSnapPoints[closestIndex] - projectedSnap,
152
+ );
153
+ return Math.abs(snap - projectedSnap) < closestDist
154
+ ? index
155
+ : closestIndex;
156
+ },
157
+ snapIndex,
158
+ );
159
+
160
+ onSnapIndexChange?.(nextIndex);
161
+ const targetY = getTargetYForSnap(normalizedSnapPoints[nextIndex]);
162
+ animate(y, targetY, motionTransitions.sheet.snap);
163
+ onDragStateChange?.(false);
164
+ },
165
+ [
166
+ close,
167
+ maxSnap,
168
+ normalizedSnapPoints,
169
+ snapIndex,
170
+ viewportHeight,
171
+ y,
172
+ getTargetYForSnap,
173
+ getViewportHeight,
174
+ isFreeDrag,
175
+ config?.min,
176
+ resolvedFreeDragHeightPx,
177
+ sheetHeightPx,
178
+ isMinDefault,
179
+ onSnapIndexChange,
180
+ onDragStateChange,
181
+ ],
182
+ );
183
+
184
+ const handleContentPointerDown = useCallback(
185
+ (e: React.PointerEvent) => {
186
+ if (config?.disableDrag) return;
187
+ contentPointerActiveRef.current = true;
188
+ onDragStateChange?.(true);
189
+
190
+ const el = scrollRef?.current;
191
+ isScrollAtTopOnDownRef.current = el ? el.scrollTop <= 1 : true;
192
+
193
+ y.stop();
194
+ cancelEntranceAnimation();
195
+ const baseY = isFreeDrag ? 0 : getTargetYForSnap(currentSnap);
196
+ y.jump(baseY);
197
+ contentDraggingRef.current = false;
198
+ contentDragStartY.current = e.clientY;
199
+ contentDragSheetStartY.current = baseY;
200
+ contentDragPrevY.current = e.clientY;
201
+ contentDragPrevTime.current = performance.now();
202
+ },
203
+ [
204
+ config?.disableDrag,
205
+ y,
206
+ isFreeDrag,
207
+ getTargetYForSnap,
208
+ currentSnap,
209
+ cancelEntranceAnimation,
210
+ onDragStateChange,
211
+ scrollRef,
212
+ ],
213
+ );
214
+
215
+ const handleContentPointerMove = useCallback(
216
+ (e: React.PointerEvent) => {
217
+ if (config?.disableDrag) return;
218
+ if (!contentPointerActiveRef.current) return;
219
+ if (!isScrollAtTopOnDownRef.current) return;
220
+ const el = scrollRef?.current;
221
+ if (!el) return;
222
+
223
+ if (contentDraggingRef.current) {
224
+ e.preventDefault();
225
+ const deltaY = e.clientY - contentDragStartY.current;
226
+ const maxDragY =
227
+ isFreeDrag || config?.min !== undefined
228
+ ? Math.max(0, resolvedFreeDragHeightPx - minPx)
229
+ : Number.POSITIVE_INFINITY;
230
+ y.set(
231
+ Math.min(
232
+ maxDragY,
233
+ Math.max(0, contentDragSheetStartY.current + deltaY),
234
+ ),
235
+ );
236
+ contentDragPrevY.current = e.clientY;
237
+ contentDragPrevTime.current = performance.now();
238
+ return;
239
+ }
240
+
241
+ if (el.scrollTop > 1) return;
242
+ const deltaY = e.clientY - contentDragStartY.current;
243
+ if (deltaY <= 4) return;
244
+
245
+ contentDraggingRef.current = true;
246
+ e.preventDefault();
247
+ el.setPointerCapture(e.pointerId);
248
+ el.scrollTop = 0;
249
+ el.style.touchAction = "none";
250
+ },
251
+ [
252
+ config?.disableDrag,
253
+ y,
254
+ isFreeDrag,
255
+ config?.min,
256
+ resolvedFreeDragHeightPx,
257
+ minPx,
258
+ scrollRef?.current,
259
+ ],
260
+ );
261
+
262
+ const handleContentPointerUp = useCallback(
263
+ (e: React.PointerEvent) => {
264
+ contentPointerActiveRef.current = false;
265
+ const el = scrollRef?.current;
266
+ if (el) {
267
+ el.style.touchAction = "";
268
+ try {
269
+ el.releasePointerCapture(e.pointerId);
270
+ } catch {}
271
+ }
272
+
273
+ if (!contentDraggingRef.current) return;
274
+ contentDraggingRef.current = false;
275
+
276
+ const dt = Math.max(1, performance.now() - contentDragPrevTime.current);
277
+ const dy = e.clientY - contentDragPrevY.current;
278
+ const velocity = (dy / dt) * 1000;
279
+
280
+ handleDragEnd(e as any, {
281
+ offset: { y: y.get() - contentDragSheetStartY.current },
282
+ velocity: { y: velocity },
283
+ });
284
+ },
285
+ [y, handleDragEnd, scrollRef?.current],
286
+ );
287
+
288
+ const handleContentPointerCancel = useCallback(() => {
289
+ contentPointerActiveRef.current = false;
290
+ if (!contentDraggingRef.current) return;
291
+ contentDraggingRef.current = false;
292
+ const el = scrollRef?.current;
293
+ if (!el) return;
294
+ el.style.touchAction = "";
295
+ const currentVh = viewportHeight || getViewportHeight();
296
+ const snapTargetY = isFreeDrag
297
+ ? Math.max(
298
+ 0,
299
+ resolvedFreeDragHeightPx -
300
+ toPx(config?.min, currentVh, toPx("7.2rem", currentVh, 115.2)),
301
+ )
302
+ : getTargetYForSnap(currentSnap);
303
+ animate(y, Math.max(0, snapTargetY), motionTransitions.sheet.snap);
304
+ onDragStateChange?.(false);
305
+ }, [
306
+ isFreeDrag,
307
+ config?.min,
308
+ viewportHeight,
309
+ resolvedFreeDragHeightPx,
310
+ y,
311
+ getTargetYForSnap,
312
+ currentSnap,
313
+ getViewportHeight,
314
+ onDragStateChange,
315
+ scrollRef?.current,
316
+ ]);
317
+
318
+ return {
319
+ handleDragEnd,
320
+ handleContentPointerDown,
321
+ handleContentPointerMove,
322
+ handleContentPointerUp,
323
+ handleContentPointerCancel,
324
+ };
325
+ }