@thangdevalone/meeting-grid-layout-react 1.4.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.
package/dist/index.mjs ADDED
@@ -0,0 +1,511 @@
1
+ import React, { createContext, useContext, useState, useEffect, useMemo, forwardRef, useRef } from 'react';
2
+ import { createMeetGrid, getSpringConfig } from '@thangdevalone/meeting-grid-layout-core';
3
+ export { createGrid, createGridItemPositioner, createMeetGrid, getAspectRatio, getGridItemDimensions, getSpringConfig, springPresets } from '@thangdevalone/meeting-grid-layout-core';
4
+ import { useMotionValue, animate, motion } from 'motion/react';
5
+
6
+ const GridContext = createContext(null);
7
+ function useGridContext() {
8
+ const context = useContext(GridContext);
9
+ if (!context) {
10
+ throw new Error("useGridContext must be used within a GridContainer");
11
+ }
12
+ return context;
13
+ }
14
+ function useGridDimensions(ref) {
15
+ const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
16
+ useEffect(() => {
17
+ const element = ref.current;
18
+ if (!element) {
19
+ return;
20
+ }
21
+ const observer = new ResizeObserver((entries) => {
22
+ for (const entry of entries) {
23
+ const { clientWidth: width, clientHeight: height } = entry.target;
24
+ setDimensions((prev) => {
25
+ if (prev.width === width && prev.height === height) {
26
+ return prev;
27
+ }
28
+ return { width, height };
29
+ });
30
+ }
31
+ });
32
+ observer.observe(element);
33
+ setDimensions({
34
+ width: element.clientWidth,
35
+ height: element.clientHeight
36
+ });
37
+ return () => {
38
+ observer.disconnect();
39
+ };
40
+ }, [ref]);
41
+ return dimensions;
42
+ }
43
+ function useMeetGrid(options) {
44
+ const itemAspectRatiosKey = options.itemAspectRatios?.join(",") ?? "";
45
+ return useMemo(() => {
46
+ return createMeetGrid(options);
47
+ }, [
48
+ options.dimensions.width,
49
+ options.dimensions.height,
50
+ options.count,
51
+ options.aspectRatio,
52
+ options.gap,
53
+ options.layoutMode,
54
+ options.pinnedIndex,
55
+ options.othersPosition,
56
+ options.maxItemsPerPage,
57
+ options.currentPage,
58
+ options.maxVisible,
59
+ options.currentVisiblePage,
60
+ itemAspectRatiosKey
61
+ ]);
62
+ }
63
+ function useGridAnimation(preset = "smooth") {
64
+ return useMemo(() => getSpringConfig(preset), [preset]);
65
+ }
66
+
67
+ const GridContainer = forwardRef(function GridContainer2({
68
+ children,
69
+ aspectRatio = "16:9",
70
+ gap = 8,
71
+ count,
72
+ layoutMode = "gallery",
73
+ pinnedIndex,
74
+ othersPosition,
75
+ springPreset = "smooth",
76
+ style,
77
+ className,
78
+ maxItemsPerPage,
79
+ currentPage,
80
+ maxVisible,
81
+ currentVisiblePage,
82
+ itemAspectRatios,
83
+ floatWidth,
84
+ floatHeight,
85
+ floatBreakpoints,
86
+ ...props
87
+ }, forwardedRef) {
88
+ const internalRef = useRef(null);
89
+ const ref = forwardedRef || internalRef;
90
+ const dimensions = useGridDimensions(ref);
91
+ const childCount = count ?? React.Children.count(children);
92
+ const gridOptions = {
93
+ dimensions,
94
+ count: childCount,
95
+ aspectRatio,
96
+ gap,
97
+ layoutMode,
98
+ pinnedIndex,
99
+ othersPosition,
100
+ maxItemsPerPage,
101
+ currentPage,
102
+ maxVisible,
103
+ currentVisiblePage,
104
+ itemAspectRatios,
105
+ floatWidth,
106
+ floatHeight,
107
+ floatBreakpoints
108
+ };
109
+ const grid = useMeetGrid(gridOptions);
110
+ const containerStyle = {
111
+ position: "relative",
112
+ width: "100%",
113
+ height: "100%",
114
+ overflow: "hidden",
115
+ ...style
116
+ };
117
+ return /* @__PURE__ */ React.createElement(GridContext.Provider, { value: { dimensions, grid, springPreset } }, /* @__PURE__ */ React.createElement("div", { ref, style: containerStyle, className, ...props }, children));
118
+ });
119
+ const GridItem = forwardRef(function GridItem2({
120
+ index,
121
+ children,
122
+ itemAspectRatio,
123
+ transition: customTransition,
124
+ disableAnimation = false,
125
+ className,
126
+ style,
127
+ ...props
128
+ }, ref) {
129
+ const { grid, springPreset, dimensions: containerDimensions } = useGridContext();
130
+ const isFloat = grid ? grid.floatIndex === index : false;
131
+ const isVisible = grid ? grid.isItemVisible(index) : false;
132
+ const isMain = grid ? grid.isMainItem(index) : false;
133
+ const isHidden = !grid || !isVisible || grid.layoutMode === "spotlight" && !isMain;
134
+ const position = grid && !isHidden ? grid.getPosition(index) : { top: 0, left: 0 };
135
+ const itemDims = grid && !isHidden ? grid.getItemDimensions(index) : { width: 0, height: 0 };
136
+ const floatDims = grid?.floatDimensions ?? { width: 120, height: 160 };
137
+ const [floatAnchor, setFloatAnchor] = React.useState("bottom-right");
138
+ const floatX = useMotionValue(0);
139
+ const floatY = useMotionValue(0);
140
+ const [floatInitialized, setFloatInitialized] = React.useState(false);
141
+ const getFloatCornerPos = React.useCallback(
142
+ (corner) => {
143
+ const padding = 12;
144
+ const fw = floatDims.width;
145
+ const fh = floatDims.height;
146
+ switch (corner) {
147
+ case "top-left":
148
+ return { x: padding, y: padding };
149
+ case "top-right":
150
+ return { x: containerDimensions.width - fw - padding, y: padding };
151
+ case "bottom-left":
152
+ return { x: padding, y: containerDimensions.height - fh - padding };
153
+ case "bottom-right":
154
+ default:
155
+ return {
156
+ x: containerDimensions.width - fw - padding,
157
+ y: containerDimensions.height - fh - padding
158
+ };
159
+ }
160
+ },
161
+ [containerDimensions.width, containerDimensions.height, floatDims.width, floatDims.height]
162
+ );
163
+ React.useEffect(() => {
164
+ if (!isFloat) {
165
+ setFloatInitialized(false);
166
+ }
167
+ }, [isFloat]);
168
+ React.useEffect(() => {
169
+ if (isFloat && containerDimensions.width > 0 && containerDimensions.height > 0 && !floatInitialized) {
170
+ const pos = getFloatCornerPos(floatAnchor);
171
+ floatX.set(pos.x);
172
+ floatY.set(pos.y);
173
+ setFloatInitialized(true);
174
+ }
175
+ }, [
176
+ isFloat,
177
+ containerDimensions.width,
178
+ containerDimensions.height,
179
+ floatAnchor,
180
+ getFloatCornerPos,
181
+ floatInitialized,
182
+ floatX,
183
+ floatY
184
+ ]);
185
+ React.useEffect(() => {
186
+ if (isFloat && floatInitialized && containerDimensions.width > 0 && containerDimensions.height > 0) {
187
+ const pos = getFloatCornerPos(floatAnchor);
188
+ const cfg = { type: "spring", stiffness: 400, damping: 30 };
189
+ animate(floatX, pos.x, cfg);
190
+ animate(floatY, pos.y, cfg);
191
+ }
192
+ }, [
193
+ isFloat,
194
+ floatAnchor,
195
+ containerDimensions.width,
196
+ containerDimensions.height,
197
+ getFloatCornerPos,
198
+ floatInitialized,
199
+ floatX,
200
+ floatY
201
+ ]);
202
+ const gridX = useMotionValue(0);
203
+ const gridY = useMotionValue(0);
204
+ const gridAnimReady = useRef(false);
205
+ const springConfig = getSpringConfig(springPreset);
206
+ React.useEffect(() => {
207
+ if (isFloat || isHidden) {
208
+ gridAnimReady.current = false;
209
+ return;
210
+ }
211
+ if (!gridAnimReady.current) {
212
+ gridX.set(position.left);
213
+ gridY.set(position.top);
214
+ gridAnimReady.current = true;
215
+ } else {
216
+ const cfg = {
217
+ type: "spring",
218
+ stiffness: springConfig.stiffness,
219
+ damping: springConfig.damping
220
+ };
221
+ animate(gridX, position.left, cfg);
222
+ animate(gridY, position.top, cfg);
223
+ }
224
+ }, [
225
+ position.top,
226
+ position.left,
227
+ isFloat,
228
+ isHidden,
229
+ gridX,
230
+ gridY,
231
+ springConfig.stiffness,
232
+ springConfig.damping
233
+ ]);
234
+ if (isHidden) {
235
+ return null;
236
+ }
237
+ const contentDimensions = grid.getItemContentDimensions(index, itemAspectRatio);
238
+ const transition = customTransition ?? {
239
+ type: springConfig.type,
240
+ stiffness: springConfig.stiffness,
241
+ damping: springConfig.damping
242
+ };
243
+ const lastVisibleOthersIndex = grid.getLastVisibleOthersIndex();
244
+ const isLastVisibleOther = index === lastVisibleOthersIndex;
245
+ const hiddenCount = grid.hiddenCount;
246
+ const renderChildren = () => {
247
+ if (typeof children === "function") {
248
+ return children({ contentDimensions, isLastVisibleOther, hiddenCount, isFloat });
249
+ }
250
+ return children;
251
+ };
252
+ if (isFloat) {
253
+ if (containerDimensions.width === 0 || containerDimensions.height === 0)
254
+ return null;
255
+ const findNearestCorner = (posX, posY) => {
256
+ const centerX = posX + floatDims.width / 2;
257
+ const centerY = posY + floatDims.height / 2;
258
+ const isLeft = centerX < containerDimensions.width / 2;
259
+ const isTop = centerY < containerDimensions.height / 2;
260
+ if (isTop && isLeft)
261
+ return "top-left";
262
+ if (isTop && !isLeft)
263
+ return "top-right";
264
+ if (!isTop && isLeft)
265
+ return "bottom-left";
266
+ return "bottom-right";
267
+ };
268
+ const dragConstraints = {
269
+ left: 12,
270
+ right: containerDimensions.width - floatDims.width - 12,
271
+ top: 12,
272
+ bottom: containerDimensions.height - floatDims.height - 12
273
+ };
274
+ const handleDragEnd = () => {
275
+ const currentX = floatX.get();
276
+ const currentY = floatY.get();
277
+ const nearestCorner = findNearestCorner(currentX, currentY);
278
+ setFloatAnchor(nearestCorner);
279
+ const snapPos = getFloatCornerPos(nearestCorner);
280
+ const springCfg = { type: "spring", stiffness: 400, damping: 30 };
281
+ animate(floatX, snapPos.x, springCfg);
282
+ animate(floatY, snapPos.y, springCfg);
283
+ };
284
+ const floatingStyle = {
285
+ position: "absolute",
286
+ width: floatDims.width,
287
+ height: floatDims.height,
288
+ borderRadius: 12,
289
+ boxShadow: "0 4px 20px rgba(0,0,0,0.3)",
290
+ overflow: "hidden",
291
+ cursor: "grab",
292
+ zIndex: 100,
293
+ touchAction: "none",
294
+ left: 0,
295
+ top: 0,
296
+ ...style
297
+ };
298
+ return /* @__PURE__ */ React.createElement(
299
+ motion.div,
300
+ {
301
+ ref,
302
+ drag: true,
303
+ dragMomentum: false,
304
+ dragElastic: 0.1,
305
+ dragConstraints,
306
+ style: { ...floatingStyle, x: floatX, y: floatY },
307
+ className,
308
+ onDragEnd: handleDragEnd,
309
+ whileDrag: { cursor: "grabbing", scale: 1.05, boxShadow: "0 8px 32px rgba(0,0,0,0.4)" },
310
+ transition: { type: "spring", stiffness: 400, damping: 30 },
311
+ "data-grid-index": index,
312
+ "data-grid-float": true,
313
+ ...props
314
+ },
315
+ renderChildren()
316
+ );
317
+ }
318
+ if (disableAnimation) {
319
+ return /* @__PURE__ */ React.createElement(
320
+ "div",
321
+ {
322
+ ref,
323
+ style: {
324
+ position: "absolute",
325
+ width: itemDims.width,
326
+ height: itemDims.height,
327
+ top: position.top,
328
+ left: position.left,
329
+ ...style
330
+ },
331
+ className,
332
+ "data-grid-index": index,
333
+ "data-grid-main": isMain,
334
+ ...props
335
+ },
336
+ renderChildren()
337
+ );
338
+ }
339
+ return /* @__PURE__ */ React.createElement(
340
+ motion.div,
341
+ {
342
+ ref,
343
+ initial: { width: itemDims.width, height: itemDims.height },
344
+ animate: { width: itemDims.width, height: itemDims.height },
345
+ transition,
346
+ style: { position: "absolute", top: 0, left: 0, x: gridX, y: gridY, ...style },
347
+ className,
348
+ "data-grid-index": index,
349
+ "data-grid-main": isMain,
350
+ ...props
351
+ },
352
+ renderChildren()
353
+ );
354
+ });
355
+ const FloatingGridItem = forwardRef(
356
+ function FloatingGridItem2({
357
+ children,
358
+ width = 120,
359
+ height = 160,
360
+ breakpoints,
361
+ initialPosition = { x: 16, y: 16 },
362
+ anchor: initialAnchor = "bottom-right",
363
+ visible = true,
364
+ edgePadding = 12,
365
+ onAnchorChange,
366
+ transition,
367
+ borderRadius = 12,
368
+ boxShadow = "0 4px 20px rgba(0,0,0,0.3)",
369
+ className,
370
+ style,
371
+ ...props
372
+ }, ref) {
373
+ const { dimensions } = useGridContext();
374
+ const [currentAnchor, setCurrentAnchor] = React.useState(initialAnchor);
375
+ const resolvedSize = React.useMemo(() => {
376
+ if (breakpoints && breakpoints.length > 0 && dimensions.width > 0) {
377
+ return resolveFloatSize(dimensions.width, breakpoints);
378
+ }
379
+ return null;
380
+ }, [breakpoints, dimensions.width]);
381
+ const effectiveWidth = resolvedSize?.width ?? width;
382
+ const effectiveHeight = resolvedSize?.height ?? height;
383
+ const x = useMotionValue(0);
384
+ const y = useMotionValue(0);
385
+ const [isInitialized, setIsInitialized] = React.useState(false);
386
+ const getCornerPosition = React.useCallback(
387
+ (corner) => {
388
+ const padding = edgePadding + initialPosition.x;
389
+ switch (corner) {
390
+ case "top-left":
391
+ return { x: padding, y: padding };
392
+ case "top-right":
393
+ return { x: dimensions.width - effectiveWidth - padding, y: padding };
394
+ case "bottom-left":
395
+ return { x: padding, y: dimensions.height - effectiveHeight - padding };
396
+ case "bottom-right":
397
+ default:
398
+ return {
399
+ x: dimensions.width - effectiveWidth - padding,
400
+ y: dimensions.height - effectiveHeight - padding
401
+ };
402
+ }
403
+ },
404
+ [dimensions.width, dimensions.height, effectiveWidth, effectiveHeight, edgePadding, initialPosition.x]
405
+ );
406
+ React.useEffect(() => {
407
+ if (dimensions.width > 0 && dimensions.height > 0 && !isInitialized) {
408
+ const pos = getCornerPosition(currentAnchor);
409
+ x.set(pos.x);
410
+ y.set(pos.y);
411
+ setIsInitialized(true);
412
+ }
413
+ }, [dimensions.width, dimensions.height, currentAnchor, getCornerPosition, isInitialized, x, y]);
414
+ React.useEffect(() => {
415
+ if (isInitialized && dimensions.width > 0 && dimensions.height > 0) {
416
+ const pos = getCornerPosition(currentAnchor);
417
+ const springConfig = { type: "spring", stiffness: 400, damping: 30 };
418
+ animate(x, pos.x, springConfig);
419
+ animate(y, pos.y, springConfig);
420
+ }
421
+ }, [currentAnchor, dimensions.width, dimensions.height, getCornerPosition, isInitialized, x, y]);
422
+ if (!visible || dimensions.width === 0 || dimensions.height === 0)
423
+ return null;
424
+ const findNearestCorner = (posX, posY) => {
425
+ const centerX = posX + effectiveWidth / 2;
426
+ const centerY = posY + effectiveHeight / 2;
427
+ const containerCenterX = dimensions.width / 2;
428
+ const containerCenterY = dimensions.height / 2;
429
+ const isLeft = centerX < containerCenterX;
430
+ const isTop = centerY < containerCenterY;
431
+ if (isTop && isLeft)
432
+ return "top-left";
433
+ if (isTop && !isLeft)
434
+ return "top-right";
435
+ if (!isTop && isLeft)
436
+ return "bottom-left";
437
+ return "bottom-right";
438
+ };
439
+ const dragConstraints = {
440
+ left: edgePadding,
441
+ right: dimensions.width - effectiveWidth - edgePadding,
442
+ top: edgePadding,
443
+ bottom: dimensions.height - effectiveHeight - edgePadding
444
+ };
445
+ const floatingStyle = {
446
+ position: "absolute",
447
+ width: effectiveWidth,
448
+ height: effectiveHeight,
449
+ borderRadius,
450
+ boxShadow,
451
+ overflow: "hidden",
452
+ cursor: "grab",
453
+ zIndex: 100,
454
+ touchAction: "none",
455
+ left: 0,
456
+ top: 0,
457
+ ...style
458
+ };
459
+ const handleDragEnd = () => {
460
+ const currentX = x.get();
461
+ const currentY = y.get();
462
+ const nearestCorner = findNearestCorner(currentX, currentY);
463
+ setCurrentAnchor(nearestCorner);
464
+ onAnchorChange?.(nearestCorner);
465
+ const snapPos = getCornerPosition(nearestCorner);
466
+ const springConfig = { type: "spring", stiffness: 400, damping: 30 };
467
+ animate(x, snapPos.x, springConfig);
468
+ animate(y, snapPos.y, springConfig);
469
+ };
470
+ return /* @__PURE__ */ React.createElement(
471
+ motion.div,
472
+ {
473
+ ref,
474
+ drag: true,
475
+ dragMomentum: false,
476
+ dragElastic: 0.1,
477
+ dragConstraints,
478
+ style: { ...floatingStyle, x, y },
479
+ className,
480
+ onDragEnd: handleDragEnd,
481
+ whileDrag: { cursor: "grabbing", scale: 1.05, boxShadow: "0 8px 32px rgba(0,0,0,0.4)" },
482
+ transition: transition ?? { type: "spring", stiffness: 400, damping: 30 },
483
+ ...props
484
+ },
485
+ children
486
+ );
487
+ }
488
+ );
489
+ const GridOverlay = forwardRef(function GridOverlay2({ visible = true, backgroundColor = "rgba(0,0,0,0.5)", children, style, ...props }, ref) {
490
+ if (!visible)
491
+ return null;
492
+ return /* @__PURE__ */ React.createElement(
493
+ "div",
494
+ {
495
+ ref,
496
+ style: {
497
+ position: "absolute",
498
+ inset: 0,
499
+ display: "flex",
500
+ alignItems: "center",
501
+ justifyContent: "center",
502
+ backgroundColor,
503
+ ...style
504
+ },
505
+ ...props
506
+ },
507
+ children
508
+ );
509
+ });
510
+
511
+ export { FloatingGridItem, GridContainer, GridContext, GridItem, GridOverlay, useGridAnimation, useGridContext, useGridDimensions, useMeetGrid };
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@thangdevalone/meeting-grid-layout-react",
3
+ "version": "1.4.1",
4
+ "description": "React integration for meeting-grid-layout with Motion animations",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.mjs",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.mts",
13
+ "default": "./dist/index.mjs"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "keywords": [
25
+ "react",
26
+ "grid",
27
+ "meeting",
28
+ "video",
29
+ "motion",
30
+ "animation"
31
+ ],
32
+ "author": "ThangDevAlone",
33
+ "license": "MIT",
34
+ "homepage": "https://github.com/thangdevalone/meeting-grid-layout#readme",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/thangdevalone/meeting-grid-layout"
38
+ },
39
+ "peerDependencies": {
40
+ "react": ">=18.0.0",
41
+ "react-dom": ">=18.0.0"
42
+ },
43
+ "dependencies": {
44
+ "motion": "^11.15.0",
45
+ "@thangdevalone/meeting-grid-layout-core": "1.4.1"
46
+ },
47
+ "devDependencies": {
48
+ "@types/react": "^18.2.0",
49
+ "@types/react-dom": "^18.2.0",
50
+ "react": "^18.2.0",
51
+ "react-dom": "^18.2.0",
52
+ "unbuild": "^2.0.0",
53
+ "vitest": "^1.0.0",
54
+ "rimraf": "^5.0.0"
55
+ },
56
+ "scripts": {
57
+ "build": "unbuild",
58
+ "dev": "unbuild --watch",
59
+ "clean": "rimraf dist",
60
+ "test": "vitest run",
61
+ "test:watch": "vitest"
62
+ }
63
+ }