@comitor/ui 2.0.0 → 2.1.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.
Files changed (131) hide show
  1. package/LICENSE +35 -0
  2. package/README.md +14 -6
  3. package/dist/components/ui/action-bar-constants.d.ts +31 -0
  4. package/dist/components/ui/action-bar-constants.js +9 -0
  5. package/dist/components/ui/action-bar-constants.js.map +1 -0
  6. package/dist/components/ui/action-bar.d.ts +67 -0
  7. package/dist/components/ui/action-bar.js +522 -0
  8. package/dist/components/ui/action-bar.js.map +1 -0
  9. package/dist/components/ui/badge-overflow.d.ts +30 -0
  10. package/dist/components/ui/badge-overflow.js +179 -0
  11. package/dist/components/ui/badge-overflow.js.map +1 -0
  12. package/dist/components/ui/banner.d.ts +79 -0
  13. package/dist/components/ui/banner.js +518 -0
  14. package/dist/components/ui/banner.js.map +1 -0
  15. package/dist/components/ui/circular-progress.d.ts +50 -0
  16. package/dist/components/ui/circular-progress.js +266 -0
  17. package/dist/components/ui/circular-progress.js.map +1 -0
  18. package/dist/components/ui/cropper-constants.d.ts +34 -0
  19. package/dist/components/ui/cropper-constants.js +10 -0
  20. package/dist/components/ui/cropper-constants.js.map +1 -0
  21. package/dist/components/ui/cropper.d.ts +106 -0
  22. package/dist/components/ui/cropper.js +1269 -0
  23. package/dist/components/ui/cropper.js.map +1 -0
  24. package/dist/components/ui/drawer.d.ts +1 -1
  25. package/dist/components/ui/drawer.js +2 -2
  26. package/dist/components/ui/drawer.js.map +1 -1
  27. package/dist/components/ui/kanban-constants.d.ts +61 -0
  28. package/dist/components/ui/kanban-constants.js +21 -0
  29. package/dist/components/ui/kanban-constants.js.map +1 -0
  30. package/dist/components/ui/kanban.d.ts +72 -0
  31. package/dist/components/ui/kanban.js +787 -0
  32. package/dist/components/ui/kanban.js.map +1 -0
  33. package/dist/components/ui/key-value-constants.d.ts +33 -0
  34. package/dist/components/ui/key-value-constants.js +11 -0
  35. package/dist/components/ui/key-value-constants.js.map +1 -0
  36. package/dist/components/ui/key-value.d.ts +96 -0
  37. package/dist/components/ui/key-value.js +641 -0
  38. package/dist/components/ui/key-value.js.map +1 -0
  39. package/dist/components/ui/mask-input-constants.d.ts +99 -0
  40. package/dist/components/ui/mask-input-constants.js +601 -0
  41. package/dist/components/ui/mask-input-constants.js.map +1 -0
  42. package/dist/components/ui/mask-input.d.ts +36 -0
  43. package/dist/components/ui/mask-input.js +558 -0
  44. package/dist/components/ui/mask-input.js.map +1 -0
  45. package/dist/components/ui/mention.d.ts +31 -0
  46. package/dist/components/ui/mention.js +128 -0
  47. package/dist/components/ui/mention.js.map +1 -0
  48. package/dist/components/ui/phone-input-constants.d.ts +54 -0
  49. package/dist/components/ui/phone-input-constants.js +9 -0
  50. package/dist/components/ui/phone-input-constants.js.map +1 -0
  51. package/dist/components/ui/phone-input.d.ts +85 -0
  52. package/dist/components/ui/phone-input.js +743 -0
  53. package/dist/components/ui/phone-input.js.map +1 -0
  54. package/dist/components/ui/rating.d.ts +56 -0
  55. package/dist/components/ui/rating.js +646 -0
  56. package/dist/components/ui/rating.js.map +1 -0
  57. package/dist/components/ui/responsive-dialog.d.ts +35 -0
  58. package/dist/components/ui/responsive-dialog.js +193 -0
  59. package/dist/components/ui/responsive-dialog.js.map +1 -0
  60. package/dist/components/ui/scroll-spy.d.ts +42 -0
  61. package/dist/components/ui/scroll-spy.js +334 -0
  62. package/dist/components/ui/scroll-spy.js.map +1 -0
  63. package/dist/components/ui/stepper-constants.d.ts +29 -0
  64. package/dist/components/ui/stepper-constants.js +9 -0
  65. package/dist/components/ui/stepper-constants.js.map +1 -0
  66. package/dist/components/ui/stepper.d.ts +80 -0
  67. package/dist/components/ui/stepper.js +902 -0
  68. package/dist/components/ui/stepper.js.map +1 -0
  69. package/dist/components/ui/tags-input.d.ts +41 -0
  70. package/dist/components/ui/tags-input.js +127 -0
  71. package/dist/components/ui/tags-input.js.map +1 -0
  72. package/dist/components/ui/time-picker-constants.d.ts +48 -0
  73. package/dist/components/ui/time-picker-constants.js +14 -0
  74. package/dist/components/ui/time-picker-constants.js.map +1 -0
  75. package/dist/components/ui/time-picker.d.ts +115 -0
  76. package/dist/components/ui/time-picker.js +1620 -0
  77. package/dist/components/ui/time-picker.js.map +1 -0
  78. package/dist/components/ui/timeline.d.ts +32 -0
  79. package/dist/components/ui/timeline.js +559 -0
  80. package/dist/components/ui/timeline.js.map +1 -0
  81. package/dist/components/ui/tour-constants.d.ts +39 -0
  82. package/dist/components/ui/tour-constants.js +12 -0
  83. package/dist/components/ui/tour-constants.js.map +1 -0
  84. package/dist/components/ui/tour.d.ts +124 -0
  85. package/dist/components/ui/tour.js +1207 -0
  86. package/dist/components/ui/tour.js.map +1 -0
  87. package/dist/data-grid/data-grid-cell-wrapper.js +1 -1
  88. package/dist/data-grid/data-grid-cell-wrapper.js.map +1 -1
  89. package/dist/data-grid/data-grid-context-menu.js +1 -1
  90. package/dist/data-grid/data-grid-context-menu.js.map +1 -1
  91. package/dist/data-grid/data-grid-paste-dialog.js +1 -1
  92. package/dist/data-grid/data-grid-paste-dialog.js.map +1 -1
  93. package/dist/data-grid/data-grid-row.js +1 -1
  94. package/dist/data-grid/data-grid-row.js.map +1 -1
  95. package/dist/data-grid/data-grid-search.js +1 -1
  96. package/dist/data-grid/data-grid-search.js.map +1 -1
  97. package/dist/data-grid/data-grid.js +1 -1
  98. package/dist/data-grid/data-grid.js.map +1 -1
  99. package/dist/data-grid/sortable.js +1 -1
  100. package/dist/data-grid/sortable.js.map +1 -1
  101. package/dist/data-grid/use-data-grid-undo-redo.js +2 -2
  102. package/dist/data-grid/use-data-grid-undo-redo.js.map +1 -1
  103. package/dist/data-grid/use-data-grid.js +3 -3
  104. package/dist/data-grid/use-data-grid.js.map +1 -1
  105. package/dist/index.d.ts +31 -0
  106. package/dist/index.js +42 -0
  107. package/dist/index.js.map +1 -1
  108. package/dist/lib/compose-refs.js.map +1 -0
  109. package/dist/lib/use-as-ref.js.map +1 -0
  110. package/dist/lib/use-is-mobile.d.ts +10 -0
  111. package/dist/lib/use-is-mobile.js +21 -0
  112. package/dist/lib/use-is-mobile.js.map +1 -0
  113. package/dist/lib/use-isomorphic-layout-effect.js.map +1 -0
  114. package/dist/lib/use-lazy-ref.js.map +1 -0
  115. package/dist/lib/visually-hidden-input.d.ts +12 -0
  116. package/dist/lib/visually-hidden-input.js +105 -0
  117. package/dist/lib/visually-hidden-input.js.map +1 -0
  118. package/package.json +5 -1
  119. package/styles.css +19 -0
  120. package/dist/data-grid/compose-refs.js.map +0 -1
  121. package/dist/data-grid/use-as-ref.js.map +0 -1
  122. package/dist/data-grid/use-isomorphic-layout-effect.js.map +0 -1
  123. package/dist/data-grid/use-lazy-ref.js.map +0 -1
  124. /package/dist/{data-grid → lib}/compose-refs.d.ts +0 -0
  125. /package/dist/{data-grid → lib}/compose-refs.js +0 -0
  126. /package/dist/{data-grid → lib}/use-as-ref.d.ts +0 -0
  127. /package/dist/{data-grid → lib}/use-as-ref.js +0 -0
  128. /package/dist/{data-grid → lib}/use-isomorphic-layout-effect.d.ts +0 -0
  129. /package/dist/{data-grid → lib}/use-isomorphic-layout-effect.js +0 -0
  130. /package/dist/{data-grid → lib}/use-lazy-ref.d.ts +0 -0
  131. /package/dist/{data-grid → lib}/use-lazy-ref.js +0 -0
@@ -0,0 +1,1269 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { Slot } from "@radix-ui/react-slot";
4
+ import { cva } from "class-variance-authority";
5
+ import * as React from "react";
6
+ import { cn } from "../../lib/cn.js";
7
+ import { useComposedRefs } from "../../lib/compose-refs.js";
8
+ import { useAsRef } from "../../lib/use-as-ref.js";
9
+ import { useIsomorphicLayoutEffect } from "../../lib/use-isomorphic-layout-effect.js";
10
+ import { useLazyRef } from "../../lib/use-lazy-ref.js";
11
+ import { DEFAULT_CROPPER_LABELS } from "./cropper-constants.js";
12
+ const ROOT_NAME = "Cropper";
13
+ const ROOT_IMPL_NAME = "CropperImpl";
14
+ const IMAGE_NAME = "CropperImage";
15
+ const VIDEO_NAME = "CropperVideo";
16
+ const AREA_NAME = "CropperArea";
17
+ const MAX_CACHE_SIZE = 200;
18
+ const ZOOM_IN_KEYS = /* @__PURE__ */ new Set(["+", "="]);
19
+ const ZOOM_OUT_KEYS = /* @__PURE__ */ new Set(["-", "_"]);
20
+ const KEYBOARD_ZOOM_STEP = 0.1;
21
+ function getDevicePixelRatio() {
22
+ return typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
23
+ }
24
+ function getServerDevicePixelRatio() {
25
+ return 1;
26
+ }
27
+ function subscribeToDevicePixelRatio(onChange) {
28
+ window.addEventListener("resize", onChange);
29
+ return () => window.removeEventListener("resize", onChange);
30
+ }
31
+ function useDevicePixelRatio() {
32
+ return React.useSyncExternalStore(subscribeToDevicePixelRatio, getDevicePixelRatio, getServerDevicePixelRatio);
33
+ }
34
+ const rotationSizeCache = /* @__PURE__ */ new Map();
35
+ const cropSizeCache = /* @__PURE__ */ new Map();
36
+ const croppedAreaCache = /* @__PURE__ */ new Map();
37
+ const onPositionClampCache = /* @__PURE__ */ new Map();
38
+ function clamp(value, min, max) {
39
+ return Math.min(Math.max(value, min), max);
40
+ }
41
+ function quantize(n, step = 2 / getDevicePixelRatio()) {
42
+ return Math.round(n / step) * step;
43
+ }
44
+ function quantizePosition(n, step = 4 / getDevicePixelRatio()) {
45
+ return Math.round(n / step) * step;
46
+ }
47
+ function quantizeZoom(n, step = 0.01) {
48
+ return Math.round(n / step) * step;
49
+ }
50
+ function quantizeRotation(n, step = 1) {
51
+ return Math.round(n / step) * step;
52
+ }
53
+ function snapToDevicePixel(n, dpr) {
54
+ return Math.round(n * dpr) / dpr;
55
+ }
56
+ function lruGet(map, key) {
57
+ const v = map.get(key);
58
+ if (v !== void 0) {
59
+ map.delete(key);
60
+ map.set(key, v);
61
+ }
62
+ return v;
63
+ }
64
+ function lruSet(map, key, val, max = MAX_CACHE_SIZE) {
65
+ if (map.has(key)) {
66
+ map.delete(key);
67
+ }
68
+ map.set(key, val);
69
+ if (map.size > max) {
70
+ const firstKey = map.keys().next().value;
71
+ if (firstKey !== void 0) {
72
+ map.delete(firstKey);
73
+ }
74
+ }
75
+ }
76
+ function getDistanceBetweenPoints(pointA, pointB) {
77
+ return Math.sqrt((pointA.y - pointB.y) ** 2 + (pointA.x - pointB.x) ** 2);
78
+ }
79
+ function getCenter(a, b) {
80
+ return {
81
+ x: (b.x + a.x) * 0.5,
82
+ y: (b.y + a.y) * 0.5
83
+ };
84
+ }
85
+ function getRotationBetweenPoints(pointA, pointB) {
86
+ return Math.atan2(pointB.y - pointA.y, pointB.x - pointA.x) * 180 / Math.PI;
87
+ }
88
+ function getRadianAngle(degreeValue) {
89
+ return degreeValue * Math.PI / 180;
90
+ }
91
+ function rotateSize(width, height, rotation) {
92
+ const cacheKey = `${quantize(width)}-${quantize(height)}-${quantizeRotation(rotation)}`;
93
+ const cached = lruGet(rotationSizeCache, cacheKey);
94
+ if (cached) {
95
+ return cached;
96
+ }
97
+ const rotRad = getRadianAngle(rotation);
98
+ const cosRot = Math.cos(rotRad);
99
+ const sinRot = Math.sin(rotRad);
100
+ const result = {
101
+ width: Math.abs(cosRot * width) + Math.abs(sinRot * height),
102
+ height: Math.abs(sinRot * width) + Math.abs(cosRot * height)
103
+ };
104
+ lruSet(rotationSizeCache, cacheKey, result, MAX_CACHE_SIZE);
105
+ return result;
106
+ }
107
+ function getCropSize(mediaWidth, mediaHeight, contentWidth, contentHeight, aspect, rotation = 0) {
108
+ const cacheKey = `${quantize(mediaWidth, 8)}-${quantize(mediaHeight, 8)}-${quantize(contentWidth, 8)}-${quantize(contentHeight, 8)}-${quantize(aspect, 0.01)}-${quantizeRotation(rotation)}`;
109
+ const cached = lruGet(cropSizeCache, cacheKey);
110
+ if (cached) {
111
+ return cached;
112
+ }
113
+ const { width, height } = rotateSize(mediaWidth, mediaHeight, rotation);
114
+ const fittingWidth = Math.min(width, contentWidth);
115
+ const fittingHeight = Math.min(height, contentHeight);
116
+ const result = fittingWidth > fittingHeight * aspect ? {
117
+ width: fittingHeight * aspect,
118
+ height: fittingHeight
119
+ } : {
120
+ width: fittingWidth,
121
+ height: fittingWidth / aspect
122
+ };
123
+ lruSet(cropSizeCache, cacheKey, result, MAX_CACHE_SIZE);
124
+ return result;
125
+ }
126
+ function onPositionClamp(position, mediaSize, cropSize, zoom, rotation = 0) {
127
+ const cacheKey = `${quantize(mediaSize.width)}-${quantize(mediaSize.height)}-${quantize(cropSize.width)}-${quantize(cropSize.height)}-${quantizeZoom(zoom)}-${quantizeRotation(rotation)}`;
128
+ let maxPosition = lruGet(onPositionClampCache, cacheKey);
129
+ if (!maxPosition) {
130
+ const { width, height } = rotateSize(mediaSize.width, mediaSize.height, rotation);
131
+ maxPosition = {
132
+ x: width * zoom * 0.5 - cropSize.width * 0.5,
133
+ y: height * zoom * 0.5 - cropSize.height * 0.5
134
+ };
135
+ lruSet(onPositionClampCache, cacheKey, maxPosition, MAX_CACHE_SIZE);
136
+ }
137
+ return {
138
+ x: clamp(position.x, -maxPosition.x, maxPosition.x),
139
+ y: clamp(position.y, -maxPosition.y, maxPosition.y)
140
+ };
141
+ }
142
+ function getCroppedArea(crop, mediaSize, cropSize, aspect, zoom, rotation = 0, allowOverflow = false) {
143
+ const cacheKey = `${quantizePosition(crop.x)}-${quantizePosition(crop.y)}-${quantize(mediaSize.width)}-${quantize(mediaSize.height)}-${quantize(mediaSize.naturalWidth)}-${quantize(mediaSize.naturalHeight)}-${quantize(cropSize.width)}-${quantize(cropSize.height)}-${quantize(aspect, 0.01)}-${quantizeZoom(zoom)}-${quantizeRotation(rotation)}-${allowOverflow}`;
144
+ const cached = lruGet(croppedAreaCache, cacheKey);
145
+ if (cached) return cached;
146
+ const onAreaLimit = !allowOverflow ? (max, value) => Math.min(max, Math.max(0, value)) : (_max, value) => value;
147
+ const mediaBBoxSize = rotateSize(mediaSize.width, mediaSize.height, rotation);
148
+ const mediaNaturalBBoxSize = rotateSize(mediaSize.naturalWidth, mediaSize.naturalHeight, rotation);
149
+ const croppedAreaPercentages = {
150
+ x: onAreaLimit(
151
+ 100,
152
+ ((mediaBBoxSize.width - cropSize.width / zoom) / 2 - crop.x / zoom) / mediaBBoxSize.width * 100
153
+ ),
154
+ y: onAreaLimit(
155
+ 100,
156
+ ((mediaBBoxSize.height - cropSize.height / zoom) / 2 - crop.y / zoom) / mediaBBoxSize.height * 100
157
+ ),
158
+ width: onAreaLimit(100, cropSize.width / mediaBBoxSize.width * 100 / zoom),
159
+ height: onAreaLimit(100, cropSize.height / mediaBBoxSize.height * 100 / zoom)
160
+ };
161
+ const widthInPixels = Math.round(
162
+ onAreaLimit(mediaNaturalBBoxSize.width, croppedAreaPercentages.width * mediaNaturalBBoxSize.width / 100)
163
+ );
164
+ const heightInPixels = Math.round(
165
+ onAreaLimit(mediaNaturalBBoxSize.height, croppedAreaPercentages.height * mediaNaturalBBoxSize.height / 100)
166
+ );
167
+ const isImageWiderThanHigh = mediaNaturalBBoxSize.width >= mediaNaturalBBoxSize.height * aspect;
168
+ const sizePixels = isImageWiderThanHigh ? {
169
+ width: Math.round(heightInPixels * aspect),
170
+ height: heightInPixels
171
+ } : {
172
+ width: widthInPixels,
173
+ height: Math.round(widthInPixels / aspect)
174
+ };
175
+ const croppedAreaPixels = {
176
+ ...sizePixels,
177
+ x: Math.round(
178
+ onAreaLimit(
179
+ mediaNaturalBBoxSize.width - sizePixels.width,
180
+ croppedAreaPercentages.x * mediaNaturalBBoxSize.width / 100
181
+ )
182
+ ),
183
+ y: Math.round(
184
+ onAreaLimit(
185
+ mediaNaturalBBoxSize.height - sizePixels.height,
186
+ croppedAreaPercentages.y * mediaNaturalBBoxSize.height / 100
187
+ )
188
+ )
189
+ };
190
+ const result = { croppedAreaPercentages, croppedAreaPixels };
191
+ lruSet(croppedAreaCache, cacheKey, result, MAX_CACHE_SIZE);
192
+ return result;
193
+ }
194
+ const StoreContext = React.createContext(null);
195
+ function useStoreContext(consumerName) {
196
+ const context = React.useContext(StoreContext);
197
+ if (!context) {
198
+ throw new Error(`\`${consumerName}\` ph\u1EA3i n\u1EB1m b\xEAn trong \`${ROOT_NAME}\``);
199
+ }
200
+ return context;
201
+ }
202
+ function useStore(selector) {
203
+ const store = useStoreContext("useCropper");
204
+ const getSnapshot = React.useCallback(() => selector(store.getState()), [store, selector]);
205
+ return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);
206
+ }
207
+ const CropperContext = React.createContext(null);
208
+ function useCropperContext(consumerName) {
209
+ const context = React.useContext(CropperContext);
210
+ if (!context) {
211
+ throw new Error(`\`${consumerName}\` ph\u1EA3i n\u1EB1m b\xEAn trong \`${ROOT_NAME}\``);
212
+ }
213
+ return context;
214
+ }
215
+ function Cropper(props) {
216
+ const {
217
+ /*
218
+ * ⚠ Khác thượng nguồn: bản gốc gán mặc định ngay ở đây (`crop = { x: 0, y: 0 }`, `zoom = 1`,
219
+ * `rotation = 0`), nên nhánh `!== undefined` trong effect đồng bộ bên dưới LUÔN đúng, và
220
+ * `{ x: 0, y: 0 }` là object MỚI mỗi lượt render. Hệ quả ở chế độ không kiểm soát: mỗi lần
221
+ * component cha render lại — kể cả vì chính `onCropComplete` gọi `setState` ở cha — vị trí và
222
+ * độ phóng bị kéo về mặc định ngay sau khi người dùng vừa chỉnh xong. Mặc định nay chỉ dùng
223
+ * cho state KHỞI TẠO; effect chỉ đồng bộ prop nào thật sự được truyền, đúng ý của nhánh đó.
224
+ */
225
+ crop: cropProp,
226
+ zoom: zoomProp,
227
+ minZoom = 1,
228
+ maxZoom = 3,
229
+ zoomSpeed = 1,
230
+ rotation: rotationProp,
231
+ keyboardStep = 1,
232
+ aspectRatio = 4 / 3,
233
+ shape = "rectangle",
234
+ objectFit = "contain",
235
+ allowOverflow = false,
236
+ preventScrollZoom = false,
237
+ withGrid = false,
238
+ labels: labelsProp,
239
+ onCropChange,
240
+ onCropSizeChange,
241
+ onCropAreaChange,
242
+ onCropComplete,
243
+ onZoomChange,
244
+ onRotationChange,
245
+ onMediaLoaded,
246
+ onInteractionStart,
247
+ onInteractionEnd,
248
+ className,
249
+ ...rootProps
250
+ } = props;
251
+ const listenersRef = useLazyRef(() => /* @__PURE__ */ new Set());
252
+ const stateRef = useLazyRef(() => ({
253
+ crop: cropProp ?? { x: 0, y: 0 },
254
+ zoom: zoomProp ?? 1,
255
+ rotation: rotationProp ?? 0,
256
+ mediaSize: null,
257
+ cropSize: null,
258
+ isDragging: false,
259
+ isWheelZooming: false
260
+ }));
261
+ const propsRef = useAsRef({
262
+ onCropChange,
263
+ onCropSizeChange,
264
+ onCropAreaChange,
265
+ onCropComplete,
266
+ onZoomChange,
267
+ onRotationChange,
268
+ onMediaLoaded,
269
+ onInteractionStart,
270
+ onInteractionEnd
271
+ });
272
+ const rootRef = React.useRef(null);
273
+ const store = React.useMemo(() => {
274
+ let isBatching = false;
275
+ let raf = null;
276
+ function notifyCropAreaChange() {
277
+ if (raf != null) return;
278
+ raf = requestAnimationFrame(() => {
279
+ raf = null;
280
+ const s = stateRef.current;
281
+ if (s?.mediaSize && s.cropSize && propsRef.current.onCropAreaChange) {
282
+ const { croppedAreaPercentages, croppedAreaPixels } = getCroppedArea(
283
+ s.crop,
284
+ s.mediaSize,
285
+ s.cropSize,
286
+ aspectRatio,
287
+ s.zoom,
288
+ s.rotation
289
+ );
290
+ propsRef.current.onCropAreaChange(croppedAreaPercentages, croppedAreaPixels);
291
+ }
292
+ });
293
+ }
294
+ return {
295
+ subscribe: (cb) => {
296
+ listenersRef.current.add(cb);
297
+ return () => listenersRef.current.delete(cb);
298
+ },
299
+ getState: () => stateRef.current,
300
+ setState: (key, value) => {
301
+ if (Object.is(stateRef.current[key], value)) return;
302
+ stateRef.current[key] = value;
303
+ if (key === "crop" && typeof value === "object" && value && "x" in value) {
304
+ propsRef.current.onCropChange?.(value);
305
+ } else if (key === "zoom" && typeof value === "number") {
306
+ propsRef.current.onZoomChange?.(value);
307
+ } else if (key === "rotation" && typeof value === "number") {
308
+ propsRef.current.onRotationChange?.(value);
309
+ } else if (key === "cropSize" && typeof value === "object" && value && "width" in value) {
310
+ propsRef.current.onCropSizeChange?.(value);
311
+ } else if (key === "mediaSize" && typeof value === "object" && value && "naturalWidth" in value) {
312
+ propsRef.current.onMediaLoaded?.(value);
313
+ } else if (key === "isDragging") {
314
+ if (value) {
315
+ propsRef.current.onInteractionStart?.();
316
+ } else {
317
+ propsRef.current.onInteractionEnd?.();
318
+ const currentState = stateRef.current;
319
+ if (currentState?.mediaSize && currentState.cropSize && propsRef.current.onCropComplete) {
320
+ const { croppedAreaPercentages, croppedAreaPixels } = getCroppedArea(
321
+ currentState.crop,
322
+ currentState.mediaSize,
323
+ currentState.cropSize,
324
+ aspectRatio,
325
+ currentState.zoom,
326
+ currentState.rotation
327
+ );
328
+ propsRef.current.onCropComplete(croppedAreaPercentages, croppedAreaPixels);
329
+ }
330
+ }
331
+ }
332
+ if ((key === "crop" || key === "zoom" || key === "rotation" || key === "mediaSize" || key === "cropSize") && propsRef.current.onCropAreaChange) {
333
+ notifyCropAreaChange();
334
+ }
335
+ if (!isBatching) {
336
+ store.notify();
337
+ }
338
+ },
339
+ notify: () => {
340
+ for (const cb of listenersRef.current) {
341
+ cb();
342
+ }
343
+ },
344
+ batch: (fn) => {
345
+ if (isBatching) {
346
+ fn();
347
+ return;
348
+ }
349
+ isBatching = true;
350
+ try {
351
+ fn();
352
+ } finally {
353
+ isBatching = false;
354
+ store.notify();
355
+ }
356
+ }
357
+ };
358
+ }, [listenersRef, stateRef, propsRef, aspectRatio]);
359
+ useIsomorphicLayoutEffect(() => {
360
+ const updates = {};
361
+ let hasUpdates = false;
362
+ let shouldRecompute = false;
363
+ if (cropProp !== void 0) {
364
+ const currentState = store.getState();
365
+ if (!Object.is(currentState.crop, cropProp)) {
366
+ updates.crop = cropProp;
367
+ hasUpdates = true;
368
+ }
369
+ }
370
+ if (zoomProp !== void 0) {
371
+ const currentState = store.getState();
372
+ if (currentState.zoom !== zoomProp) {
373
+ updates.zoom = zoomProp;
374
+ hasUpdates = true;
375
+ shouldRecompute = true;
376
+ }
377
+ }
378
+ if (rotationProp !== void 0) {
379
+ const currentState = store.getState();
380
+ if (currentState.rotation !== rotationProp) {
381
+ updates.rotation = rotationProp;
382
+ hasUpdates = true;
383
+ shouldRecompute = true;
384
+ }
385
+ }
386
+ if (hasUpdates) {
387
+ store.batch(() => {
388
+ Object.entries(updates).forEach(([key, value]) => {
389
+ store.setState(key, value);
390
+ });
391
+ });
392
+ if (shouldRecompute && rootRef.current) {
393
+ requestAnimationFrame(() => {
394
+ const currentState = store.getState();
395
+ if (currentState.cropSize && currentState.mediaSize) {
396
+ const newPosition = !allowOverflow ? onPositionClamp(
397
+ currentState.crop,
398
+ currentState.mediaSize,
399
+ currentState.cropSize,
400
+ currentState.zoom,
401
+ currentState.rotation
402
+ ) : currentState.crop;
403
+ if (Math.abs(newPosition.x - currentState.crop.x) > 1e-3 || Math.abs(newPosition.y - currentState.crop.y) > 1e-3) {
404
+ store.setState("crop", newPosition);
405
+ }
406
+ }
407
+ });
408
+ }
409
+ }
410
+ }, [cropProp, zoomProp, rotationProp, store, allowOverflow]);
411
+ const labels = React.useMemo(() => ({ ...DEFAULT_CROPPER_LABELS, ...labelsProp }), [labelsProp]);
412
+ const contextValue = React.useMemo(
413
+ () => ({
414
+ minZoom,
415
+ maxZoom,
416
+ zoomSpeed,
417
+ keyboardStep,
418
+ aspectRatio,
419
+ shape,
420
+ objectFit,
421
+ preventScrollZoom,
422
+ allowOverflow,
423
+ withGrid,
424
+ rootRef,
425
+ labels
426
+ }),
427
+ [
428
+ minZoom,
429
+ maxZoom,
430
+ zoomSpeed,
431
+ keyboardStep,
432
+ aspectRatio,
433
+ shape,
434
+ objectFit,
435
+ preventScrollZoom,
436
+ allowOverflow,
437
+ withGrid,
438
+ labels
439
+ ]
440
+ );
441
+ return /* @__PURE__ */ jsx(StoreContext.Provider, { value: store, children: /* @__PURE__ */ jsx(CropperContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx("div", { "data-slot": "cropper-wrapper", className: cn("relative size-full overflow-hidden", className), children: /* @__PURE__ */ jsx(CropperImpl, { ...rootProps }) }) }) });
442
+ }
443
+ function CropperImpl(props) {
444
+ const {
445
+ onWheelZoom: onWheelZoomProp,
446
+ onKeyUp: onKeyUpProp,
447
+ onKeyDown: onKeyDownProp,
448
+ onMouseDown: onMouseDownProp,
449
+ onTouchStart: onTouchStartProp,
450
+ "aria-describedby": ariaDescribedByProp,
451
+ asChild,
452
+ className,
453
+ ref,
454
+ ...rootImplProps
455
+ } = props;
456
+ const context = useCropperContext(ROOT_IMPL_NAME);
457
+ const store = useStoreContext(ROOT_IMPL_NAME);
458
+ const crop = useStore((state) => state.crop);
459
+ const zoom = useStore((state) => state.zoom);
460
+ const rotation = useStore((state) => state.rotation);
461
+ const mediaSize = useStore((state) => state.mediaSize);
462
+ const cropSize = useStore((state) => state.cropSize);
463
+ const descriptionId = React.useId();
464
+ const [zoomStatus, setZoomStatus] = React.useState("");
465
+ const propsRef = useAsRef({
466
+ onWheelZoom: onWheelZoomProp,
467
+ onKeyUp: onKeyUpProp,
468
+ onKeyDown: onKeyDownProp,
469
+ onMouseDown: onMouseDownProp,
470
+ onTouchStart: onTouchStartProp
471
+ });
472
+ const composedRef = useComposedRefs(ref, context.rootRef);
473
+ const dragStartPositionRef = React.useRef({ x: 0, y: 0 });
474
+ const dragStartCropRef = React.useRef({ x: 0, y: 0 });
475
+ const contentPositionRef = React.useRef({ x: 0, y: 0 });
476
+ const lastPinchDistanceRef = React.useRef(0);
477
+ const lastPinchRotationRef = React.useRef(0);
478
+ const rafDragTimeoutRef = React.useRef(null);
479
+ const rafPinchTimeoutRef = React.useRef(null);
480
+ const wheelTimerRef = React.useRef(null);
481
+ const isTouchingRef = React.useRef(false);
482
+ const gestureZoomStartRef = React.useRef(0);
483
+ const gestureRotationStartRef = React.useRef(0);
484
+ const onRefsCleanup = React.useCallback(() => {
485
+ if (rafDragTimeoutRef.current) {
486
+ cancelAnimationFrame(rafDragTimeoutRef.current);
487
+ rafDragTimeoutRef.current = null;
488
+ }
489
+ if (rafPinchTimeoutRef.current) {
490
+ cancelAnimationFrame(rafPinchTimeoutRef.current);
491
+ rafPinchTimeoutRef.current = null;
492
+ }
493
+ if (wheelTimerRef.current) {
494
+ clearTimeout(wheelTimerRef.current);
495
+ wheelTimerRef.current = null;
496
+ }
497
+ isTouchingRef.current = false;
498
+ }, []);
499
+ const onCacheCleanup = React.useCallback(() => {
500
+ if (onPositionClampCache.size > MAX_CACHE_SIZE * 1.5) {
501
+ onPositionClampCache.clear();
502
+ }
503
+ if (croppedAreaCache.size > MAX_CACHE_SIZE * 1.5) {
504
+ croppedAreaCache.clear();
505
+ }
506
+ }, []);
507
+ const getMousePoint = React.useCallback(
508
+ (event) => ({
509
+ x: Number(event.clientX),
510
+ y: Number(event.clientY)
511
+ }),
512
+ []
513
+ );
514
+ const getTouchPoint = React.useCallback(
515
+ (touch) => ({
516
+ x: Number(touch.clientX),
517
+ y: Number(touch.clientY)
518
+ }),
519
+ []
520
+ );
521
+ const onContentPositionChange = React.useCallback(() => {
522
+ if (context.rootRef?.current) {
523
+ const bounds = context.rootRef.current.getBoundingClientRect();
524
+ contentPositionRef.current = { x: bounds.left, y: bounds.top };
525
+ }
526
+ }, [context.rootRef]);
527
+ const getPointOnContent = React.useCallback(
528
+ ({ x, y }, contentTopLeft) => {
529
+ if (!context.rootRef?.current) {
530
+ return { x: 0, y: 0 };
531
+ }
532
+ const contentRect = context.rootRef.current.getBoundingClientRect();
533
+ return {
534
+ x: contentRect.width / 2 - (x - contentTopLeft.x),
535
+ y: contentRect.height / 2 - (y - contentTopLeft.y)
536
+ };
537
+ },
538
+ [context.rootRef]
539
+ );
540
+ const getPointOnMedia = React.useCallback(
541
+ ({ x, y }) => {
542
+ return {
543
+ x: (x + crop.x) / zoom,
544
+ y: (y + crop.y) / zoom
545
+ };
546
+ },
547
+ [crop, zoom]
548
+ );
549
+ const recomputeCropPosition = React.useCallback(() => {
550
+ if (!cropSize || !mediaSize) return;
551
+ const newPosition = !context.allowOverflow ? onPositionClamp(crop, mediaSize, cropSize, zoom, rotation) : crop;
552
+ if (Math.abs(newPosition.x - crop.x) > 1e-3 || Math.abs(newPosition.y - crop.y) > 1e-3) {
553
+ store.setState("crop", newPosition);
554
+ }
555
+ }, [cropSize, mediaSize, context.allowOverflow, crop, zoom, rotation, store]);
556
+ const onZoomChange = React.useCallback(
557
+ (newZoom, point, shouldUpdatePosition = true) => {
558
+ if (!cropSize || !mediaSize) return;
559
+ const clampedZoom = clamp(newZoom, context.minZoom, context.maxZoom);
560
+ store.batch(() => {
561
+ if (shouldUpdatePosition) {
562
+ const zoomPoint = getPointOnContent(point, contentPositionRef.current);
563
+ const zoomTarget = getPointOnMedia(zoomPoint);
564
+ const requestedPosition = {
565
+ x: zoomTarget.x * clampedZoom - zoomPoint.x,
566
+ y: zoomTarget.y * clampedZoom - zoomPoint.y
567
+ };
568
+ const newPosition = !context.allowOverflow ? onPositionClamp(requestedPosition, mediaSize, cropSize, clampedZoom, rotation) : requestedPosition;
569
+ store.setState("crop", newPosition);
570
+ }
571
+ store.setState("zoom", clampedZoom);
572
+ });
573
+ requestAnimationFrame(() => {
574
+ recomputeCropPosition();
575
+ });
576
+ },
577
+ [
578
+ cropSize,
579
+ mediaSize,
580
+ context.minZoom,
581
+ context.maxZoom,
582
+ context.allowOverflow,
583
+ getPointOnContent,
584
+ getPointOnMedia,
585
+ rotation,
586
+ store,
587
+ recomputeCropPosition
588
+ ]
589
+ );
590
+ const onDragStart = React.useCallback(
591
+ ({ x, y }) => {
592
+ dragStartPositionRef.current = { x, y };
593
+ dragStartCropRef.current = { ...crop };
594
+ store.setState("isDragging", true);
595
+ },
596
+ [crop, store]
597
+ );
598
+ const onDrag = React.useCallback(
599
+ ({ x, y }) => {
600
+ if (rafDragTimeoutRef.current) {
601
+ cancelAnimationFrame(rafDragTimeoutRef.current);
602
+ }
603
+ rafDragTimeoutRef.current = requestAnimationFrame(() => {
604
+ if (!cropSize || !mediaSize) return;
605
+ if (x === void 0 || y === void 0) return;
606
+ const offsetX = x - dragStartPositionRef.current.x;
607
+ const offsetY = y - dragStartPositionRef.current.y;
608
+ if (Math.abs(offsetX) < 2 && Math.abs(offsetY) < 2) {
609
+ return;
610
+ }
611
+ const requestedPosition = {
612
+ x: dragStartCropRef.current.x + offsetX,
613
+ y: dragStartCropRef.current.y + offsetY
614
+ };
615
+ const newPosition = !context.allowOverflow ? onPositionClamp(requestedPosition, mediaSize, cropSize, zoom, rotation) : requestedPosition;
616
+ const currentCrop = store.getState().crop;
617
+ if (Math.abs(newPosition.x - currentCrop.x) > 1 || Math.abs(newPosition.y - currentCrop.y) > 1) {
618
+ store.setState("crop", newPosition);
619
+ }
620
+ });
621
+ },
622
+ [cropSize, mediaSize, context.allowOverflow, zoom, rotation, store]
623
+ );
624
+ const onMouseMove = React.useCallback((event) => onDrag(getMousePoint(event)), [getMousePoint, onDrag]);
625
+ const onTouchMove = React.useCallback(
626
+ (event) => {
627
+ event.preventDefault();
628
+ if (event.touches.length === 2) {
629
+ const [firstTouch, secondTouch] = event.touches ?? [];
630
+ if (firstTouch && secondTouch) {
631
+ const pointA = getTouchPoint(firstTouch);
632
+ const pointB = getTouchPoint(secondTouch);
633
+ const center = getCenter(pointA, pointB);
634
+ onDrag(center);
635
+ if (rafPinchTimeoutRef.current) {
636
+ cancelAnimationFrame(rafPinchTimeoutRef.current);
637
+ }
638
+ rafPinchTimeoutRef.current = requestAnimationFrame(() => {
639
+ const distance = getDistanceBetweenPoints(pointA, pointB);
640
+ const distanceRatio = distance / lastPinchDistanceRef.current;
641
+ if (Math.abs(distanceRatio - 1) > 0.01) {
642
+ const newZoom = zoom * distanceRatio;
643
+ onZoomChange(newZoom, center, false);
644
+ lastPinchDistanceRef.current = distance;
645
+ }
646
+ const rotationAngle = getRotationBetweenPoints(pointA, pointB);
647
+ const rotationDiff = rotationAngle - lastPinchRotationRef.current;
648
+ if (Math.abs(rotationDiff) > 0.5) {
649
+ const newRotation = rotation + rotationDiff;
650
+ store.setState("rotation", newRotation);
651
+ lastPinchRotationRef.current = rotationAngle;
652
+ }
653
+ });
654
+ }
655
+ } else if (event.touches.length === 1) {
656
+ const firstTouch = event.touches[0];
657
+ if (firstTouch) {
658
+ onDrag(getTouchPoint(firstTouch));
659
+ }
660
+ }
661
+ },
662
+ [getTouchPoint, onDrag, zoom, onZoomChange, rotation, store]
663
+ );
664
+ const onGestureChange = React.useCallback(
665
+ (event) => {
666
+ event.preventDefault();
667
+ if (isTouchingRef.current) {
668
+ return;
669
+ }
670
+ const point = { x: Number(event.clientX), y: Number(event.clientY) };
671
+ const newZoom = gestureZoomStartRef.current - 1 + event.scale;
672
+ onZoomChange(newZoom, point, true);
673
+ const newRotation = gestureRotationStartRef.current + event.rotation;
674
+ store.setState("rotation", newRotation);
675
+ },
676
+ [onZoomChange, store]
677
+ );
678
+ const onGestureEnd = React.useCallback(() => {
679
+ document.removeEventListener("gesturechange", onGestureChange);
680
+ document.removeEventListener("gestureend", onGestureEnd);
681
+ }, [onGestureChange]);
682
+ const onGestureStart = React.useCallback(
683
+ (event) => {
684
+ event.preventDefault();
685
+ onContentPositionChange();
686
+ document.addEventListener("gesturechange", onGestureChange);
687
+ document.addEventListener("gestureend", onGestureEnd);
688
+ gestureZoomStartRef.current = zoom;
689
+ gestureRotationStartRef.current = rotation;
690
+ },
691
+ [zoom, rotation, onGestureChange, onGestureEnd, onContentPositionChange]
692
+ );
693
+ const onSafariZoomPrevent = React.useCallback((event) => event.preventDefault(), []);
694
+ const onEventsCleanup = React.useCallback(() => {
695
+ document.removeEventListener("mousemove", onMouseMove);
696
+ document.removeEventListener("touchmove", onTouchMove);
697
+ document.removeEventListener("gesturechange", onGestureChange);
698
+ document.removeEventListener("gestureend", onGestureEnd);
699
+ }, [onMouseMove, onTouchMove, onGestureChange, onGestureEnd]);
700
+ const onDragStopped = React.useCallback(() => {
701
+ isTouchingRef.current = false;
702
+ store.setState("isDragging", false);
703
+ onRefsCleanup();
704
+ document.removeEventListener("mouseup", onDragStopped);
705
+ document.removeEventListener("touchend", onDragStopped);
706
+ onEventsCleanup();
707
+ }, [store, onEventsCleanup, onRefsCleanup]);
708
+ const getWheelDelta = React.useCallback((event) => {
709
+ let deltaX = event.deltaX;
710
+ let deltaY = event.deltaY;
711
+ let deltaZ = event.deltaZ;
712
+ if (event.deltaMode === 1) {
713
+ deltaX *= 16;
714
+ deltaY *= 16;
715
+ deltaZ *= 16;
716
+ } else if (event.deltaMode === 2) {
717
+ deltaX *= 400;
718
+ deltaY *= 400;
719
+ deltaZ *= 400;
720
+ }
721
+ return { deltaX, deltaY, deltaZ };
722
+ }, []);
723
+ const onWheelZoom = React.useCallback(
724
+ (event) => {
725
+ propsRef.current.onWheelZoom?.(event);
726
+ if (event.defaultPrevented) return;
727
+ event.preventDefault();
728
+ onContentPositionChange();
729
+ const point = getMousePoint(event);
730
+ const { deltaY } = getWheelDelta(event);
731
+ const newZoom = zoom - deltaY * context.zoomSpeed / 200;
732
+ onZoomChange(newZoom, point, true);
733
+ store.batch(() => {
734
+ const currentState = store.getState();
735
+ if (!currentState.isWheelZooming) {
736
+ store.setState("isWheelZooming", true);
737
+ }
738
+ if (!currentState.isDragging) {
739
+ store.setState("isDragging", true);
740
+ }
741
+ });
742
+ if (wheelTimerRef.current) {
743
+ clearTimeout(wheelTimerRef.current);
744
+ }
745
+ wheelTimerRef.current = window.setTimeout(() => {
746
+ store.batch(() => {
747
+ store.setState("isWheelZooming", false);
748
+ store.setState("isDragging", false);
749
+ });
750
+ }, 250);
751
+ },
752
+ [propsRef, getMousePoint, zoom, context.zoomSpeed, onZoomChange, getWheelDelta, store, onContentPositionChange]
753
+ );
754
+ const onKeyUp = React.useCallback(
755
+ (event) => {
756
+ propsRef.current.onKeyUp?.(event);
757
+ if (event.defaultPrevented) return;
758
+ const arrowKeys = /* @__PURE__ */ new Set(["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"]);
759
+ if (arrowKeys.has(event.key) || ZOOM_IN_KEYS.has(event.key) || ZOOM_OUT_KEYS.has(event.key)) {
760
+ event.preventDefault();
761
+ store.setState("isDragging", false);
762
+ }
763
+ },
764
+ [propsRef, store]
765
+ );
766
+ const onKeyDown = React.useCallback(
767
+ (event) => {
768
+ propsRef.current.onKeyDown?.(event);
769
+ if (event.defaultPrevented || !cropSize || !mediaSize) return;
770
+ const zoomDirection = ZOOM_IN_KEYS.has(event.key) ? 1 : ZOOM_OUT_KEYS.has(event.key) ? -1 : 0;
771
+ if (zoomDirection !== 0) {
772
+ if (event.metaKey || event.ctrlKey || event.altKey) return;
773
+ const root = context.rootRef.current;
774
+ if (!root) return;
775
+ event.preventDefault();
776
+ if (!event.repeat) {
777
+ store.setState("isDragging", true);
778
+ }
779
+ const nextZoom = clamp(zoom + zoomDirection * KEYBOARD_ZOOM_STEP, context.minZoom, context.maxZoom);
780
+ onContentPositionChange();
781
+ const rect = root.getBoundingClientRect();
782
+ onZoomChange(nextZoom, { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }, true);
783
+ setZoomStatus(context.labels.zoomStatus(nextZoom));
784
+ return;
785
+ }
786
+ let step = context.keyboardStep;
787
+ if (event.shiftKey) {
788
+ step *= 0.2;
789
+ }
790
+ const keyCallbacks = {
791
+ ArrowUp: () => ({ ...crop, y: crop.y - step }),
792
+ ArrowDown: () => ({ ...crop, y: crop.y + step }),
793
+ ArrowLeft: () => ({ ...crop, x: crop.x - step }),
794
+ ArrowRight: () => ({ ...crop, x: crop.x + step })
795
+ };
796
+ const callback = keyCallbacks[event.key];
797
+ if (!callback) return;
798
+ event.preventDefault();
799
+ let newCrop = callback();
800
+ if (!context.allowOverflow) {
801
+ newCrop = onPositionClamp(newCrop, mediaSize, cropSize, zoom, rotation);
802
+ }
803
+ if (!event.repeat) {
804
+ store.setState("isDragging", true);
805
+ }
806
+ store.setState("crop", newCrop);
807
+ },
808
+ [
809
+ propsRef,
810
+ cropSize,
811
+ mediaSize,
812
+ context.keyboardStep,
813
+ context.allowOverflow,
814
+ context.rootRef,
815
+ context.minZoom,
816
+ context.maxZoom,
817
+ context.labels,
818
+ crop,
819
+ zoom,
820
+ rotation,
821
+ store,
822
+ onContentPositionChange,
823
+ onZoomChange
824
+ ]
825
+ );
826
+ const onMouseDown = React.useCallback(
827
+ (event) => {
828
+ propsRef.current.onMouseDown?.(event);
829
+ if (event.defaultPrevented) return;
830
+ event.preventDefault();
831
+ document.addEventListener("mousemove", onMouseMove);
832
+ document.addEventListener("mouseup", onDragStopped);
833
+ onContentPositionChange();
834
+ onDragStart(getMousePoint(event));
835
+ },
836
+ [propsRef, getMousePoint, onDragStart, onDragStopped, onMouseMove, onContentPositionChange]
837
+ );
838
+ const onTouchStart = React.useCallback(
839
+ (event) => {
840
+ propsRef.current.onTouchStart?.(event);
841
+ if (event.defaultPrevented) return;
842
+ isTouchingRef.current = true;
843
+ document.addEventListener("touchmove", onTouchMove, { passive: false });
844
+ document.addEventListener("touchend", onDragStopped);
845
+ onContentPositionChange();
846
+ if (event.touches.length === 2) {
847
+ const [firstTouch, secondTouch] = event.touches ? Array.from(event.touches) : [];
848
+ if (firstTouch && secondTouch) {
849
+ const pointA = getTouchPoint(firstTouch);
850
+ const pointB = getTouchPoint(secondTouch);
851
+ lastPinchDistanceRef.current = getDistanceBetweenPoints(pointA, pointB);
852
+ lastPinchRotationRef.current = getRotationBetweenPoints(pointA, pointB);
853
+ onDragStart(getCenter(pointA, pointB));
854
+ }
855
+ } else if (event.touches.length === 1) {
856
+ const firstTouch = event.touches[0];
857
+ if (firstTouch) {
858
+ onDragStart(getTouchPoint(firstTouch));
859
+ }
860
+ }
861
+ },
862
+ [propsRef, onDragStopped, onTouchMove, onContentPositionChange, getTouchPoint, onDragStart]
863
+ );
864
+ React.useEffect(() => {
865
+ const content = context.rootRef?.current;
866
+ if (!content) return;
867
+ if (!context.preventScrollZoom) {
868
+ content.addEventListener("wheel", onWheelZoom, { passive: false });
869
+ }
870
+ content.addEventListener("gesturestart", onSafariZoomPrevent);
871
+ content.addEventListener("gesturestart", onGestureStart);
872
+ return () => {
873
+ if (!context.preventScrollZoom) {
874
+ content.removeEventListener("wheel", onWheelZoom);
875
+ }
876
+ content.removeEventListener("gesturestart", onSafariZoomPrevent);
877
+ content.removeEventListener("gesturestart", onGestureStart);
878
+ };
879
+ }, [context.rootRef, context.preventScrollZoom, onWheelZoom, onSafariZoomPrevent, onGestureStart]);
880
+ React.useEffect(() => {
881
+ return () => {
882
+ onRefsCleanup();
883
+ onCacheCleanup();
884
+ };
885
+ }, [onRefsCleanup, onCacheCleanup]);
886
+ const RootPrimitive = asChild ? Slot : "div";
887
+ const describedBy = [descriptionId, ariaDescribedByProp].filter(Boolean).join(" ");
888
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
889
+ /* @__PURE__ */ jsx(
890
+ RootPrimitive,
891
+ {
892
+ role: "application",
893
+ "aria-roledescription": context.labels.roleDescription,
894
+ "aria-label": context.labels.label,
895
+ "aria-describedby": describedBy,
896
+ "data-slot": "cropper",
897
+ tabIndex: 0,
898
+ ...rootImplProps,
899
+ ref: composedRef,
900
+ className: cn(
901
+ "absolute inset-0 flex cursor-move touch-none items-center justify-center overflow-hidden outline-none select-none",
902
+ /*
903
+ * ⚠ Khác thượng nguồn: bản gốc `outline-none` mà KHÔNG có chỉ báo focus nào (WCAG 2.4.7).
904
+ * Vòng focus vẽ trên `::after` phủ kín khung chứ không trên chính phần tử: ảnh và khung
905
+ * cắt là con `absolute` nên được vẽ ĐÈ lên outline của cha, còn `::after` là con cuối
906
+ * cùng nên nằm trên cùng. Cùng khuôn `outline-ring` với `Button`, thụt vào trong vì hai
907
+ * lớp bọc ngoài đều `overflow-hidden`.
908
+ */
909
+ "after:pointer-events-none after:absolute after:inset-0 focus-visible:after:-outline-offset-2 focus-visible:after:outline-2 focus-visible:after:outline-ring",
910
+ className
911
+ ),
912
+ onKeyUp,
913
+ onKeyDown,
914
+ onMouseDown,
915
+ onTouchStart
916
+ }
917
+ ),
918
+ /* @__PURE__ */ jsx("span", { id: descriptionId, hidden: true, children: context.labels.instructions }),
919
+ /* @__PURE__ */ jsx("span", { "aria-live": "polite", "aria-atomic": "true", className: "sr-only", children: zoomStatus })
920
+ ] });
921
+ }
922
+ const cropperMediaVariants = cva("will-change-transform", {
923
+ variants: {
924
+ objectFit: {
925
+ contain: "absolute inset-0 m-auto max-h-full max-w-full",
926
+ cover: "h-auto w-full",
927
+ "horizontal-cover": "h-auto w-full",
928
+ "vertical-cover": "h-full w-auto"
929
+ }
930
+ },
931
+ defaultVariants: {
932
+ objectFit: "contain"
933
+ }
934
+ });
935
+ function useMediaComputation({
936
+ mediaRef,
937
+ context,
938
+ store,
939
+ rotation,
940
+ getNaturalDimensions
941
+ }) {
942
+ const computeSizes = React.useCallback(() => {
943
+ const media = mediaRef.current;
944
+ const content = context.rootRef?.current;
945
+ if (!media || !content) return;
946
+ const contentRect = content.getBoundingClientRect();
947
+ const containerAspect = contentRect.width / contentRect.height;
948
+ const { width: naturalWidth, height: naturalHeight } = getNaturalDimensions(media);
949
+ const isScaledDown = media.offsetWidth < naturalWidth || media.offsetHeight < naturalHeight;
950
+ const mediaAspect = naturalWidth / naturalHeight;
951
+ let renderedMediaSize;
952
+ if (isScaledDown) {
953
+ const objectFitCallbacks = {
954
+ contain: () => containerAspect > mediaAspect ? {
955
+ width: contentRect.height * mediaAspect,
956
+ height: contentRect.height
957
+ } : {
958
+ width: contentRect.width,
959
+ height: contentRect.width / mediaAspect
960
+ },
961
+ "horizontal-cover": () => ({
962
+ width: contentRect.width,
963
+ height: contentRect.width / mediaAspect
964
+ }),
965
+ "vertical-cover": () => ({
966
+ width: contentRect.height * mediaAspect,
967
+ height: contentRect.height
968
+ }),
969
+ cover: () => containerAspect < mediaAspect ? {
970
+ width: contentRect.width,
971
+ height: contentRect.width / mediaAspect
972
+ } : {
973
+ width: contentRect.height * mediaAspect,
974
+ height: contentRect.height
975
+ }
976
+ };
977
+ const callback = objectFitCallbacks[context.objectFit];
978
+ renderedMediaSize = callback ? callback() : containerAspect > mediaAspect ? {
979
+ width: contentRect.height * mediaAspect,
980
+ height: contentRect.height
981
+ } : {
982
+ width: contentRect.width,
983
+ height: contentRect.width / mediaAspect
984
+ };
985
+ } else {
986
+ renderedMediaSize = {
987
+ width: media.offsetWidth,
988
+ height: media.offsetHeight
989
+ };
990
+ }
991
+ const mediaSize = {
992
+ ...renderedMediaSize,
993
+ naturalWidth,
994
+ naturalHeight
995
+ };
996
+ store.setState("mediaSize", mediaSize);
997
+ const cropSize = getCropSize(
998
+ mediaSize.width,
999
+ mediaSize.height,
1000
+ contentRect.width,
1001
+ contentRect.height,
1002
+ context.aspectRatio,
1003
+ rotation
1004
+ );
1005
+ store.setState("cropSize", cropSize);
1006
+ requestAnimationFrame(() => {
1007
+ const currentState = store.getState();
1008
+ if (currentState.cropSize && currentState.mediaSize) {
1009
+ const newPosition = onPositionClamp(
1010
+ currentState.crop,
1011
+ currentState.mediaSize,
1012
+ currentState.cropSize,
1013
+ currentState.zoom,
1014
+ currentState.rotation
1015
+ );
1016
+ if (Math.abs(newPosition.x - currentState.crop.x) > 1e-3 || Math.abs(newPosition.y - currentState.crop.y) > 1e-3) {
1017
+ store.setState("crop", newPosition);
1018
+ }
1019
+ }
1020
+ });
1021
+ return { mediaSize, cropSize };
1022
+ }, [mediaRef, context.aspectRatio, context.rootRef, context.objectFit, store, rotation, getNaturalDimensions]);
1023
+ return { computeSizes };
1024
+ }
1025
+ function CropperImage(props) {
1026
+ const { className, style, asChild, ref, onLoad, objectFit, snapPixels = false, ...imageProps } = props;
1027
+ const context = useCropperContext(IMAGE_NAME);
1028
+ const store = useStoreContext(IMAGE_NAME);
1029
+ const crop = useStore((state) => state.crop);
1030
+ const zoom = useStore((state) => state.zoom);
1031
+ const rotation = useStore((state) => state.rotation);
1032
+ const dpr = useDevicePixelRatio();
1033
+ const imageRef = React.useRef(null);
1034
+ const composedRef = useComposedRefs(ref, imageRef);
1035
+ const getNaturalDimensions = React.useCallback(
1036
+ (image) => ({
1037
+ width: image.naturalWidth,
1038
+ height: image.naturalHeight
1039
+ }),
1040
+ []
1041
+ );
1042
+ const { computeSizes } = useMediaComputation({
1043
+ mediaRef: imageRef,
1044
+ context,
1045
+ store,
1046
+ rotation,
1047
+ getNaturalDimensions
1048
+ });
1049
+ const onMediaLoad = React.useCallback(() => {
1050
+ const image = imageRef.current;
1051
+ if (!image) return;
1052
+ computeSizes();
1053
+ onLoad?.(new Event("load"));
1054
+ }, [computeSizes, onLoad]);
1055
+ React.useEffect(() => {
1056
+ const image = imageRef.current;
1057
+ if (image?.complete && image.naturalWidth > 0) {
1058
+ onMediaLoad();
1059
+ }
1060
+ }, [onMediaLoad]);
1061
+ React.useEffect(() => {
1062
+ const content = context.rootRef?.current;
1063
+ if (!content) return;
1064
+ if (typeof ResizeObserver !== "undefined") {
1065
+ let isFirstResize = true;
1066
+ const resizeObserver = new ResizeObserver(() => {
1067
+ if (isFirstResize) {
1068
+ isFirstResize = false;
1069
+ return;
1070
+ }
1071
+ const callback = () => {
1072
+ const image = imageRef.current;
1073
+ if (image?.complete && image.naturalWidth > 0) {
1074
+ computeSizes();
1075
+ }
1076
+ };
1077
+ if ("requestIdleCallback" in window) {
1078
+ requestIdleCallback(callback);
1079
+ } else {
1080
+ setTimeout(callback, 16);
1081
+ }
1082
+ });
1083
+ resizeObserver.observe(content);
1084
+ return () => {
1085
+ resizeObserver.disconnect();
1086
+ };
1087
+ } else {
1088
+ const onWindowResize = () => {
1089
+ const image = imageRef.current;
1090
+ if (image?.complete && image.naturalWidth > 0) {
1091
+ computeSizes();
1092
+ }
1093
+ };
1094
+ window.addEventListener("resize", onWindowResize);
1095
+ return () => {
1096
+ window.removeEventListener("resize", onWindowResize);
1097
+ };
1098
+ }
1099
+ }, [context.rootRef, computeSizes]);
1100
+ const ImagePrimitive = asChild ? Slot : "img";
1101
+ return /* @__PURE__ */ jsx(
1102
+ ImagePrimitive,
1103
+ {
1104
+ "data-slot": "cropper-image",
1105
+ ...imageProps,
1106
+ ref: composedRef,
1107
+ className: cn(
1108
+ cropperMediaVariants({
1109
+ objectFit: objectFit ?? context.objectFit,
1110
+ className
1111
+ })
1112
+ ),
1113
+ style: {
1114
+ transform: snapPixels ? `translate(${snapToDevicePixel(crop.x, dpr)}px, ${snapToDevicePixel(crop.y, dpr)}px) rotate(${rotation}deg) scale(${zoom})` : `translate(${crop.x}px, ${crop.y}px) rotate(${rotation}deg) scale(${zoom})`,
1115
+ ...style
1116
+ },
1117
+ onLoad: onMediaLoad
1118
+ }
1119
+ );
1120
+ }
1121
+ function CropperVideo(props) {
1122
+ const { className, style, asChild, ref, onLoadedMetadata, objectFit, snapPixels = false, ...videoProps } = props;
1123
+ const context = useCropperContext(VIDEO_NAME);
1124
+ const store = useStoreContext(VIDEO_NAME);
1125
+ const crop = useStore((state) => state.crop);
1126
+ const zoom = useStore((state) => state.zoom);
1127
+ const rotation = useStore((state) => state.rotation);
1128
+ const dpr = useDevicePixelRatio();
1129
+ const videoRef = React.useRef(null);
1130
+ const composedRef = useComposedRefs(ref, videoRef);
1131
+ const getNaturalDimensions = React.useCallback(
1132
+ (video) => ({
1133
+ width: video.videoWidth,
1134
+ height: video.videoHeight
1135
+ }),
1136
+ []
1137
+ );
1138
+ const { computeSizes } = useMediaComputation({
1139
+ mediaRef: videoRef,
1140
+ context,
1141
+ store,
1142
+ rotation,
1143
+ getNaturalDimensions
1144
+ });
1145
+ const onMediaLoad = React.useCallback(() => {
1146
+ const video = videoRef.current;
1147
+ if (!video) return;
1148
+ computeSizes();
1149
+ onLoadedMetadata?.(new Event("loadedmetadata"));
1150
+ }, [computeSizes, onLoadedMetadata]);
1151
+ React.useEffect(() => {
1152
+ const content = context.rootRef?.current;
1153
+ if (!content) return;
1154
+ if (typeof ResizeObserver !== "undefined") {
1155
+ let isFirstResize = true;
1156
+ const resizeObserver = new ResizeObserver(() => {
1157
+ if (isFirstResize) {
1158
+ isFirstResize = false;
1159
+ return;
1160
+ }
1161
+ const callback = () => {
1162
+ const video = videoRef.current;
1163
+ if (video && video.videoWidth > 0 && video.videoHeight > 0) {
1164
+ computeSizes();
1165
+ }
1166
+ };
1167
+ if ("requestIdleCallback" in window) {
1168
+ requestIdleCallback(callback);
1169
+ } else {
1170
+ setTimeout(callback, 16);
1171
+ }
1172
+ });
1173
+ resizeObserver.observe(content);
1174
+ return () => {
1175
+ resizeObserver.disconnect();
1176
+ };
1177
+ } else {
1178
+ const onWindowResize = () => {
1179
+ const video = videoRef.current;
1180
+ if (video && video.videoWidth > 0 && video.videoHeight > 0) {
1181
+ computeSizes();
1182
+ }
1183
+ };
1184
+ window.addEventListener("resize", onWindowResize);
1185
+ return () => {
1186
+ window.removeEventListener("resize", onWindowResize);
1187
+ };
1188
+ }
1189
+ }, [context.rootRef, computeSizes]);
1190
+ const VideoPrimitive = asChild ? Slot : "video";
1191
+ return /* @__PURE__ */ jsx(
1192
+ VideoPrimitive,
1193
+ {
1194
+ "data-slot": "cropper-video",
1195
+ autoPlay: true,
1196
+ playsInline: true,
1197
+ loop: true,
1198
+ muted: true,
1199
+ controls: false,
1200
+ ...videoProps,
1201
+ ref: composedRef,
1202
+ className: cn(
1203
+ cropperMediaVariants({
1204
+ objectFit: objectFit ?? context.objectFit,
1205
+ className
1206
+ })
1207
+ ),
1208
+ style: {
1209
+ transform: snapPixels ? `translate(${snapToDevicePixel(crop.x, dpr)}px, ${snapToDevicePixel(crop.y, dpr)}px) rotate(${rotation}deg) scale(${zoom})` : `translate(${crop.x}px, ${crop.y}px) rotate(${rotation}deg) scale(${zoom})`,
1210
+ ...style
1211
+ },
1212
+ onLoadedMetadata: onMediaLoad
1213
+ }
1214
+ );
1215
+ }
1216
+ const cropperAreaVariants = cva(
1217
+ "absolute top-1/2 left-1/2 box-border -translate-x-1/2 -translate-y-1/2 overflow-hidden border border-[2.5px] border-paper shadow-[0_0_0_9999em] shadow-ink/60 dark:shadow-ink/80",
1218
+ {
1219
+ variants: {
1220
+ shape: {
1221
+ rectangle: "",
1222
+ circle: "rounded-full"
1223
+ },
1224
+ withGrid: {
1225
+ true: "before:absolute before:top-0 before:right-1/3 before:bottom-0 before:left-1/3 before:box-border before:border before:border-t-0 before:border-b-0 before:border-paper/50 before:content-[''] after:absolute after:top-1/3 after:right-0 after:bottom-1/3 after:left-0 after:box-border after:border after:border-r-0 after:border-l-0 after:border-paper/50 after:content-['']",
1226
+ false: ""
1227
+ }
1228
+ },
1229
+ defaultVariants: {
1230
+ shape: "rectangle",
1231
+ withGrid: false
1232
+ }
1233
+ }
1234
+ );
1235
+ function CropperArea(props) {
1236
+ const { className, style, asChild, ref, snapPixels = false, shape, withGrid, ...areaProps } = props;
1237
+ const context = useCropperContext(AREA_NAME);
1238
+ const cropSize = useStore((state) => state.cropSize);
1239
+ if (!cropSize) return null;
1240
+ const AreaPrimitive = asChild ? Slot : "div";
1241
+ return /* @__PURE__ */ jsx(
1242
+ AreaPrimitive,
1243
+ {
1244
+ "data-slot": "cropper-area",
1245
+ ...areaProps,
1246
+ ref,
1247
+ className: cn(
1248
+ cropperAreaVariants({
1249
+ shape: shape ?? context.shape,
1250
+ withGrid: withGrid ?? context.withGrid,
1251
+ className
1252
+ })
1253
+ ),
1254
+ style: {
1255
+ width: snapPixels ? Math.round(cropSize.width) : cropSize.width,
1256
+ height: snapPixels ? Math.round(cropSize.height) : cropSize.height,
1257
+ ...style
1258
+ }
1259
+ }
1260
+ );
1261
+ }
1262
+ export {
1263
+ Cropper,
1264
+ CropperArea,
1265
+ CropperImage,
1266
+ CropperVideo,
1267
+ useStore as useCropper
1268
+ };
1269
+ //# sourceMappingURL=cropper.js.map