@thangdevalone/meet-layout-grid-react 1.0.8 → 1.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.
package/README.md CHANGED
@@ -43,7 +43,6 @@ Wraps the grid, computes layout, provides context.
43
43
  gap={8}
44
44
  count={6}
45
45
  layoutMode="gallery"
46
- speakerIndex={0}
47
46
  pinnedIndex={0}
48
47
  sidebarPosition="right"
49
48
  sidebarRatio={0.25}
@@ -85,7 +84,8 @@ const grid = useMeetGrid({
85
84
  count: 6,
86
85
  aspectRatio: '16:9',
87
86
  gap: 8,
88
- layoutMode: 'speaker',
87
+ layoutMode: 'sidebar',
88
+ sidebarPosition: 'bottom', // speaker-like layout
89
89
  })
90
90
 
91
91
  const { top, left } = grid.getPosition(index)
@@ -94,10 +94,9 @@ const { width, height } = grid.getItemDimensions(index)
94
94
 
95
95
  ## Layout modes
96
96
 
97
- - **gallery** — Same-size tiles
98
- - **speaker** — One large tile
97
+ - **gallery** — Same-size tiles (use `pinnedIndex` to pin a participant)
99
98
  - **spotlight** — Single participant
100
- - **sidebar** — Main + thumbnails
99
+ - **sidebar** — Main + thumbnails (use `sidebarPosition: 'bottom'` for speaker-like layout)
101
100
 
102
101
  ## Animation presets
103
102
 
package/dist/index.cjs CHANGED
@@ -46,6 +46,7 @@ function useGridDimensions(ref) {
46
46
  return dimensions;
47
47
  }
48
48
  function useMeetGrid(options) {
49
+ const itemAspectRatiosKey = options.itemAspectRatios?.join(",") ?? "";
49
50
  return React.useMemo(() => {
50
51
  return meetLayoutGridCore.createMeetGrid(options);
51
52
  }, [
@@ -56,13 +57,14 @@ function useMeetGrid(options) {
56
57
  options.gap,
57
58
  options.layoutMode,
58
59
  options.pinnedIndex,
59
- options.speakerIndex,
60
60
  options.sidebarPosition,
61
61
  options.sidebarRatio,
62
62
  options.maxItemsPerPage,
63
63
  options.currentPage,
64
- options.maxVisibleOthers,
65
- options.currentOthersPage
64
+ options.maxVisible,
65
+ options.currentVisiblePage,
66
+ options.flexLayout,
67
+ itemAspectRatiosKey
66
68
  ]);
67
69
  }
68
70
  function useGridAnimation(preset = "smooth") {
@@ -77,7 +79,6 @@ const GridContainer = React.forwardRef(
77
79
  count,
78
80
  layoutMode = "gallery",
79
81
  pinnedIndex,
80
- speakerIndex,
81
82
  sidebarPosition,
82
83
  sidebarRatio,
83
84
  springPreset = "smooth",
@@ -85,8 +86,10 @@ const GridContainer = React.forwardRef(
85
86
  className,
86
87
  maxItemsPerPage,
87
88
  currentPage,
88
- maxVisibleOthers,
89
- currentOthersPage,
89
+ maxVisible,
90
+ currentVisiblePage,
91
+ itemAspectRatios,
92
+ flexLayout,
90
93
  ...props
91
94
  }, forwardedRef) {
92
95
  const internalRef = React.useRef(null);
@@ -100,13 +103,14 @@ const GridContainer = React.forwardRef(
100
103
  gap,
101
104
  layoutMode,
102
105
  pinnedIndex,
103
- speakerIndex,
104
106
  sidebarPosition,
105
107
  sidebarRatio,
106
108
  maxItemsPerPage,
107
109
  currentPage,
108
- maxVisibleOthers,
109
- currentOthersPage
110
+ maxVisible,
111
+ currentVisiblePage,
112
+ itemAspectRatios,
113
+ flexLayout
110
114
  };
111
115
  const grid = useMeetGrid(gridOptions);
112
116
  const containerStyle = {
@@ -123,6 +127,7 @@ const GridItem = React.forwardRef(
123
127
  function GridItem2({
124
128
  index,
125
129
  children,
130
+ itemAspectRatio,
126
131
  transition: customTransition,
127
132
  disableAnimation = false,
128
133
  className,
@@ -138,6 +143,7 @@ const GridItem = React.forwardRef(
138
143
  }
139
144
  const { top, left } = grid.getPosition(index);
140
145
  const { width, height } = grid.getItemDimensions(index);
146
+ const contentDimensions = grid.getItemContentDimensions(index, itemAspectRatio);
141
147
  const isMain = grid.isMainItem(index);
142
148
  if (grid.layoutMode === "spotlight" && !isMain) {
143
149
  return null;
@@ -155,6 +161,15 @@ const GridItem = React.forwardRef(
155
161
  top,
156
162
  left
157
163
  };
164
+ const lastVisibleOthersIndex = grid.getLastVisibleOthersIndex();
165
+ const isLastVisibleOther = index === lastVisibleOthersIndex;
166
+ const hiddenCount = grid.hiddenCount;
167
+ const renderChildren = () => {
168
+ if (typeof children === "function") {
169
+ return children({ contentDimensions, isLastVisibleOther, hiddenCount });
170
+ }
171
+ return children;
172
+ };
158
173
  if (disableAnimation) {
159
174
  return /* @__PURE__ */ React__default.createElement(
160
175
  "div",
@@ -166,7 +181,7 @@ const GridItem = React.forwardRef(
166
181
  "data-grid-main": isMain,
167
182
  ...props
168
183
  },
169
- children
184
+ renderChildren()
170
185
  );
171
186
  }
172
187
  return /* @__PURE__ */ React__default.createElement(
@@ -183,6 +198,125 @@ const GridItem = React.forwardRef(
183
198
  "data-grid-main": isMain,
184
199
  ...props
185
200
  },
201
+ renderChildren()
202
+ );
203
+ }
204
+ );
205
+ const FloatingGridItem = React.forwardRef(
206
+ function FloatingGridItem2({
207
+ children,
208
+ width = 120,
209
+ height = 160,
210
+ initialPosition = { x: 16, y: 16 },
211
+ anchor: initialAnchor = "bottom-right",
212
+ visible = true,
213
+ edgePadding = 12,
214
+ onAnchorChange,
215
+ transition,
216
+ borderRadius = 12,
217
+ boxShadow = "0 4px 20px rgba(0,0,0,0.3)",
218
+ className,
219
+ style,
220
+ ...props
221
+ }, ref) {
222
+ const { dimensions } = useGridContext();
223
+ const [currentAnchor, setCurrentAnchor] = React__default.useState(initialAnchor);
224
+ const x = react.useMotionValue(0);
225
+ const y = react.useMotionValue(0);
226
+ const [isInitialized, setIsInitialized] = React__default.useState(false);
227
+ const getCornerPosition = React__default.useCallback((corner) => {
228
+ const padding = edgePadding + initialPosition.x;
229
+ switch (corner) {
230
+ case "top-left":
231
+ return { x: padding, y: padding };
232
+ case "top-right":
233
+ return { x: dimensions.width - width - padding, y: padding };
234
+ case "bottom-left":
235
+ return { x: padding, y: dimensions.height - height - padding };
236
+ case "bottom-right":
237
+ default:
238
+ return { x: dimensions.width - width - padding, y: dimensions.height - height - padding };
239
+ }
240
+ }, [dimensions.width, dimensions.height, width, height, edgePadding, initialPosition.x]);
241
+ React__default.useEffect(() => {
242
+ if (dimensions.width > 0 && dimensions.height > 0 && !isInitialized) {
243
+ const pos = getCornerPosition(currentAnchor);
244
+ x.set(pos.x);
245
+ y.set(pos.y);
246
+ setIsInitialized(true);
247
+ }
248
+ }, [dimensions.width, dimensions.height, currentAnchor, getCornerPosition, isInitialized, x, y]);
249
+ React__default.useEffect(() => {
250
+ if (isInitialized && dimensions.width > 0 && dimensions.height > 0) {
251
+ const pos = getCornerPosition(currentAnchor);
252
+ const springConfig = { type: "spring", stiffness: 400, damping: 30 };
253
+ react.animate(x, pos.x, springConfig);
254
+ react.animate(y, pos.y, springConfig);
255
+ }
256
+ }, [currentAnchor, dimensions.width, dimensions.height, getCornerPosition, isInitialized, x, y]);
257
+ if (!visible || dimensions.width === 0 || dimensions.height === 0)
258
+ return null;
259
+ const findNearestCorner = (posX, posY) => {
260
+ const centerX = posX + width / 2;
261
+ const centerY = posY + height / 2;
262
+ const containerCenterX = dimensions.width / 2;
263
+ const containerCenterY = dimensions.height / 2;
264
+ const isLeft = centerX < containerCenterX;
265
+ const isTop = centerY < containerCenterY;
266
+ if (isTop && isLeft)
267
+ return "top-left";
268
+ if (isTop && !isLeft)
269
+ return "top-right";
270
+ if (!isTop && isLeft)
271
+ return "bottom-left";
272
+ return "bottom-right";
273
+ };
274
+ const dragConstraints = {
275
+ left: edgePadding,
276
+ right: dimensions.width - width - edgePadding,
277
+ top: edgePadding,
278
+ bottom: dimensions.height - height - edgePadding
279
+ };
280
+ const floatingStyle = {
281
+ position: "absolute",
282
+ width,
283
+ height,
284
+ borderRadius,
285
+ boxShadow,
286
+ overflow: "hidden",
287
+ cursor: "grab",
288
+ zIndex: 100,
289
+ touchAction: "none",
290
+ left: 0,
291
+ top: 0,
292
+ ...style
293
+ };
294
+ const handleDragEnd = () => {
295
+ const currentX = x.get();
296
+ const currentY = y.get();
297
+ const nearestCorner = findNearestCorner(currentX, currentY);
298
+ setCurrentAnchor(nearestCorner);
299
+ onAnchorChange?.(nearestCorner);
300
+ const snapPos = getCornerPosition(nearestCorner);
301
+ const springConfig = { type: "spring", stiffness: 400, damping: 30 };
302
+ react.animate(x, snapPos.x, springConfig);
303
+ react.animate(y, snapPos.y, springConfig);
304
+ };
305
+ return /* @__PURE__ */ React__default.createElement(
306
+ react.motion.div,
307
+ {
308
+ ref,
309
+ drag: true,
310
+ dragMomentum: false,
311
+ dragElastic: 0.1,
312
+ dragConstraints,
313
+ style: { ...floatingStyle, x, y },
314
+ className,
315
+ onDragEnd: handleDragEnd,
316
+ whileDrag: { cursor: "grabbing", scale: 1.05, boxShadow: "0 8px 32px rgba(0,0,0,0.4)" },
317
+ transition: transition ?? { type: "spring", stiffness: 400, damping: 30 },
318
+ ...props
319
+ },
186
320
  children
187
321
  );
188
322
  }
@@ -218,6 +352,7 @@ exports.getAspectRatio = meetLayoutGridCore.getAspectRatio;
218
352
  exports.getGridItemDimensions = meetLayoutGridCore.getGridItemDimensions;
219
353
  exports.getSpringConfig = meetLayoutGridCore.getSpringConfig;
220
354
  exports.springPresets = meetLayoutGridCore.springPresets;
355
+ exports.FloatingGridItem = FloatingGridItem;
221
356
  exports.GridContainer = GridContainer;
222
357
  exports.GridContext = GridContext;
223
358
  exports.GridItem = GridItem;
package/dist/index.d.cts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import React__default, { RefObject, HTMLAttributes, ReactNode, CSSProperties } from 'react';
3
- import { GridDimensions, MeetGridOptions, MeetGridResult, SpringPreset, LayoutMode } from '@thangdevalone/meet-layout-grid-core';
4
- export { GridDimensions, GridOptions, LayoutMode, MeetGridOptions, MeetGridResult, PaginationInfo, Position, SpringPreset, createGrid, createGridItemPositioner, createMeetGrid, getAspectRatio, getGridItemDimensions, getSpringConfig, springPresets } from '@thangdevalone/meet-layout-grid-core';
3
+ import { GridDimensions, MeetGridOptions, MeetGridResult, SpringPreset, LayoutMode, ItemAspectRatio, ContentDimensions } from '@thangdevalone/meet-layout-grid-core';
4
+ export { ContentDimensions, GridDimensions, GridOptions, ItemAspectRatio, LayoutMode, MeetGridOptions, MeetGridResult, PaginationInfo, Position, SpringPreset, createGrid, createGridItemPositioner, createMeetGrid, getAspectRatio, getGridItemDimensions, getSpringConfig, springPresets } from '@thangdevalone/meet-layout-grid-core';
5
5
  import { HTMLMotionProps, Transition } from 'motion/react';
6
6
 
7
7
  interface GridContextValue {
@@ -59,10 +59,8 @@ interface GridContainerProps extends Omit<HTMLAttributes<HTMLDivElement>, 'child
59
59
  count?: number;
60
60
  /** Layout mode */
61
61
  layoutMode?: LayoutMode;
62
- /** Index of pinned item */
62
+ /** Index of pinned/focused item (main participant for spotlight/sidebar modes) */
63
63
  pinnedIndex?: number;
64
- /** Index of active speaker */
65
- speakerIndex?: number;
66
64
  /** Sidebar position */
67
65
  sidebarPosition?: 'left' | 'right' | 'top' | 'bottom';
68
66
  /** Sidebar ratio (0-1) */
@@ -77,21 +75,71 @@ interface GridContainerProps extends Omit<HTMLAttributes<HTMLDivElement>, 'child
77
75
  maxItemsPerPage?: number;
78
76
  /** Current page index (0-based) for pagination */
79
77
  currentPage?: number;
80
- /** Maximum visible "others" in speaker/sidebar modes (0 = show all) */
81
- maxVisibleOthers?: number;
82
- /** Current page for "others" in speaker/sidebar modes (0-based) */
83
- currentOthersPage?: number;
78
+ /** Maximum visible items (0 = show all). In gallery mode: limits all items. In sidebar: limits "others". */
79
+ maxVisible?: number;
80
+ /** Current page for visible items (0-based), used when maxVisible > 0 */
81
+ currentVisiblePage?: number;
82
+ /**
83
+ * Per-item aspect ratio configurations.
84
+ * Use different ratios for mobile (9:16), desktop (16:9), or whiteboard (fill).
85
+ * @example ['16:9', '9:16', 'fill', undefined]
86
+ */
87
+ itemAspectRatios?: (ItemAspectRatio | undefined)[];
88
+ /**
89
+ * Enable flexible cell sizing based on item aspect ratios.
90
+ * When true, portrait items (9:16) get narrower cells, landscape items (16:9) get wider cells.
91
+ * Items are packed into rows intelligently.
92
+ * @default false
93
+ */
94
+ flexLayout?: boolean;
84
95
  }
85
96
  /**
86
97
  * Container component for the meet grid.
87
98
  * Provides grid context to child GridItem components.
88
99
  */
89
100
  declare const GridContainer: React__default.ForwardRefExoticComponent<GridContainerProps & React__default.RefAttributes<HTMLDivElement>>;
90
- interface GridItemProps extends Omit<HTMLMotionProps<'div'>, 'animate' | 'initial' | 'transition'> {
101
+ interface GridItemProps extends Omit<HTMLMotionProps<'div'>, 'animate' | 'initial' | 'transition' | 'children'> {
91
102
  /** Index of this item in the grid */
92
103
  index: number;
93
- /** Children to render inside the item */
94
- children: ReactNode;
104
+ /**
105
+ * Children to render inside the item.
106
+ * Can be a ReactNode or a render function that receives contentDimensions and visibility info.
107
+ * @example
108
+ * // Simple usage
109
+ * <GridItem index={0}><Video /></GridItem>
110
+ *
111
+ * // With contentDimensions for flexible aspect ratios
112
+ * <GridItem index={0}>
113
+ * {({ contentDimensions }) => (
114
+ * <Video style={{
115
+ * width: contentDimensions.width,
116
+ * height: contentDimensions.height,
117
+ * marginTop: contentDimensions.offsetTop,
118
+ * marginLeft: contentDimensions.offsetLeft
119
+ * }} />
120
+ * )}
121
+ * </GridItem>
122
+ *
123
+ * // With hidden count for '+X more' indicator
124
+ * <GridItem index={index}>
125
+ * {({ isLastVisibleOther, hiddenCount }) => (
126
+ * <div>
127
+ * {isLastVisibleOther && hiddenCount > 0 && (
128
+ * <span className="more-indicator">+{hiddenCount}</span>
129
+ * )}
130
+ * </div>
131
+ * )}
132
+ * </GridItem>
133
+ */
134
+ children: ReactNode | ((props: {
135
+ contentDimensions: ContentDimensions;
136
+ /** True if this is the last visible item in the "others" section */
137
+ isLastVisibleOther: boolean;
138
+ /** Number of hidden items (for '+X more' indicator) */
139
+ hiddenCount: number;
140
+ }) => ReactNode);
141
+ /** Optional item-specific aspect ratio (overrides itemAspectRatios from container) */
142
+ itemAspectRatio?: ItemAspectRatio;
95
143
  /** Custom transition override */
96
144
  transition?: Transition;
97
145
  /** Whether to disable animations */
@@ -106,6 +154,43 @@ interface GridItemProps extends Omit<HTMLMotionProps<'div'>, 'animate' | 'initia
106
154
  * Automatically positions itself based on index in the grid.
107
155
  */
108
156
  declare const GridItem: React__default.ForwardRefExoticComponent<Omit<GridItemProps, "ref"> & React__default.RefAttributes<HTMLDivElement>>;
157
+ interface FloatingGridItemProps extends Omit<HTMLMotionProps<'div'>, 'animate' | 'initial' | 'children'> {
158
+ /** Children to render inside the floating item */
159
+ children: ReactNode;
160
+ /** Width of the floating item */
161
+ width?: number;
162
+ /** Height of the floating item */
163
+ height?: number;
164
+ /** Initial position (x, y from container edges) */
165
+ initialPosition?: {
166
+ x: number;
167
+ y: number;
168
+ };
169
+ /** Which corner to anchor: 'top-left', 'top-right', 'bottom-left', 'bottom-right' */
170
+ anchor?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
171
+ /** Whether the item is visible */
172
+ visible?: boolean;
173
+ /** Padding from container edges */
174
+ edgePadding?: number;
175
+ /** Callback when anchor changes after snap */
176
+ onAnchorChange?: (anchor: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right') => void;
177
+ /** Custom transition */
178
+ transition?: Transition;
179
+ /** Border radius */
180
+ borderRadius?: number;
181
+ /** Box shadow */
182
+ boxShadow?: string;
183
+ /** Additional class name */
184
+ className?: string;
185
+ /** Custom style */
186
+ style?: CSSProperties;
187
+ }
188
+ /**
189
+ * Floating Grid Item component that can be dragged around the screen.
190
+ * Snaps to the nearest corner when released (like iOS/Android PiP).
191
+ * Perfect for Picture-in-Picture style floating video in zoom mode.
192
+ */
193
+ declare const FloatingGridItem: React__default.ForwardRefExoticComponent<Omit<FloatingGridItemProps, "ref"> & React__default.RefAttributes<HTMLDivElement>>;
109
194
  interface GridOverlayProps extends HTMLAttributes<HTMLDivElement> {
110
195
  /** Whether to show the overlay */
111
196
  visible?: boolean;
@@ -117,5 +202,5 @@ interface GridOverlayProps extends HTMLAttributes<HTMLDivElement> {
117
202
  */
118
203
  declare const GridOverlay: React__default.ForwardRefExoticComponent<GridOverlayProps & React__default.RefAttributes<HTMLDivElement>>;
119
204
 
120
- export { GridContainer, GridContext, GridItem, GridOverlay, useGridAnimation, useGridContext, useGridDimensions, useMeetGrid };
121
- export type { GridContainerProps, GridItemProps, GridOverlayProps };
205
+ export { FloatingGridItem, GridContainer, GridContext, GridItem, GridOverlay, useGridAnimation, useGridContext, useGridDimensions, useMeetGrid };
206
+ export type { FloatingGridItemProps, GridContainerProps, GridItemProps, GridOverlayProps };
package/dist/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import React__default, { RefObject, HTMLAttributes, ReactNode, CSSProperties } from 'react';
3
- import { GridDimensions, MeetGridOptions, MeetGridResult, SpringPreset, LayoutMode } from '@thangdevalone/meet-layout-grid-core';
4
- export { GridDimensions, GridOptions, LayoutMode, MeetGridOptions, MeetGridResult, PaginationInfo, Position, SpringPreset, createGrid, createGridItemPositioner, createMeetGrid, getAspectRatio, getGridItemDimensions, getSpringConfig, springPresets } from '@thangdevalone/meet-layout-grid-core';
3
+ import { GridDimensions, MeetGridOptions, MeetGridResult, SpringPreset, LayoutMode, ItemAspectRatio, ContentDimensions } from '@thangdevalone/meet-layout-grid-core';
4
+ export { ContentDimensions, GridDimensions, GridOptions, ItemAspectRatio, LayoutMode, MeetGridOptions, MeetGridResult, PaginationInfo, Position, SpringPreset, createGrid, createGridItemPositioner, createMeetGrid, getAspectRatio, getGridItemDimensions, getSpringConfig, springPresets } from '@thangdevalone/meet-layout-grid-core';
5
5
  import { HTMLMotionProps, Transition } from 'motion/react';
6
6
 
7
7
  interface GridContextValue {
@@ -59,10 +59,8 @@ interface GridContainerProps extends Omit<HTMLAttributes<HTMLDivElement>, 'child
59
59
  count?: number;
60
60
  /** Layout mode */
61
61
  layoutMode?: LayoutMode;
62
- /** Index of pinned item */
62
+ /** Index of pinned/focused item (main participant for spotlight/sidebar modes) */
63
63
  pinnedIndex?: number;
64
- /** Index of active speaker */
65
- speakerIndex?: number;
66
64
  /** Sidebar position */
67
65
  sidebarPosition?: 'left' | 'right' | 'top' | 'bottom';
68
66
  /** Sidebar ratio (0-1) */
@@ -77,21 +75,71 @@ interface GridContainerProps extends Omit<HTMLAttributes<HTMLDivElement>, 'child
77
75
  maxItemsPerPage?: number;
78
76
  /** Current page index (0-based) for pagination */
79
77
  currentPage?: number;
80
- /** Maximum visible "others" in speaker/sidebar modes (0 = show all) */
81
- maxVisibleOthers?: number;
82
- /** Current page for "others" in speaker/sidebar modes (0-based) */
83
- currentOthersPage?: number;
78
+ /** Maximum visible items (0 = show all). In gallery mode: limits all items. In sidebar: limits "others". */
79
+ maxVisible?: number;
80
+ /** Current page for visible items (0-based), used when maxVisible > 0 */
81
+ currentVisiblePage?: number;
82
+ /**
83
+ * Per-item aspect ratio configurations.
84
+ * Use different ratios for mobile (9:16), desktop (16:9), or whiteboard (fill).
85
+ * @example ['16:9', '9:16', 'fill', undefined]
86
+ */
87
+ itemAspectRatios?: (ItemAspectRatio | undefined)[];
88
+ /**
89
+ * Enable flexible cell sizing based on item aspect ratios.
90
+ * When true, portrait items (9:16) get narrower cells, landscape items (16:9) get wider cells.
91
+ * Items are packed into rows intelligently.
92
+ * @default false
93
+ */
94
+ flexLayout?: boolean;
84
95
  }
85
96
  /**
86
97
  * Container component for the meet grid.
87
98
  * Provides grid context to child GridItem components.
88
99
  */
89
100
  declare const GridContainer: React__default.ForwardRefExoticComponent<GridContainerProps & React__default.RefAttributes<HTMLDivElement>>;
90
- interface GridItemProps extends Omit<HTMLMotionProps<'div'>, 'animate' | 'initial' | 'transition'> {
101
+ interface GridItemProps extends Omit<HTMLMotionProps<'div'>, 'animate' | 'initial' | 'transition' | 'children'> {
91
102
  /** Index of this item in the grid */
92
103
  index: number;
93
- /** Children to render inside the item */
94
- children: ReactNode;
104
+ /**
105
+ * Children to render inside the item.
106
+ * Can be a ReactNode or a render function that receives contentDimensions and visibility info.
107
+ * @example
108
+ * // Simple usage
109
+ * <GridItem index={0}><Video /></GridItem>
110
+ *
111
+ * // With contentDimensions for flexible aspect ratios
112
+ * <GridItem index={0}>
113
+ * {({ contentDimensions }) => (
114
+ * <Video style={{
115
+ * width: contentDimensions.width,
116
+ * height: contentDimensions.height,
117
+ * marginTop: contentDimensions.offsetTop,
118
+ * marginLeft: contentDimensions.offsetLeft
119
+ * }} />
120
+ * )}
121
+ * </GridItem>
122
+ *
123
+ * // With hidden count for '+X more' indicator
124
+ * <GridItem index={index}>
125
+ * {({ isLastVisibleOther, hiddenCount }) => (
126
+ * <div>
127
+ * {isLastVisibleOther && hiddenCount > 0 && (
128
+ * <span className="more-indicator">+{hiddenCount}</span>
129
+ * )}
130
+ * </div>
131
+ * )}
132
+ * </GridItem>
133
+ */
134
+ children: ReactNode | ((props: {
135
+ contentDimensions: ContentDimensions;
136
+ /** True if this is the last visible item in the "others" section */
137
+ isLastVisibleOther: boolean;
138
+ /** Number of hidden items (for '+X more' indicator) */
139
+ hiddenCount: number;
140
+ }) => ReactNode);
141
+ /** Optional item-specific aspect ratio (overrides itemAspectRatios from container) */
142
+ itemAspectRatio?: ItemAspectRatio;
95
143
  /** Custom transition override */
96
144
  transition?: Transition;
97
145
  /** Whether to disable animations */
@@ -106,6 +154,43 @@ interface GridItemProps extends Omit<HTMLMotionProps<'div'>, 'animate' | 'initia
106
154
  * Automatically positions itself based on index in the grid.
107
155
  */
108
156
  declare const GridItem: React__default.ForwardRefExoticComponent<Omit<GridItemProps, "ref"> & React__default.RefAttributes<HTMLDivElement>>;
157
+ interface FloatingGridItemProps extends Omit<HTMLMotionProps<'div'>, 'animate' | 'initial' | 'children'> {
158
+ /** Children to render inside the floating item */
159
+ children: ReactNode;
160
+ /** Width of the floating item */
161
+ width?: number;
162
+ /** Height of the floating item */
163
+ height?: number;
164
+ /** Initial position (x, y from container edges) */
165
+ initialPosition?: {
166
+ x: number;
167
+ y: number;
168
+ };
169
+ /** Which corner to anchor: 'top-left', 'top-right', 'bottom-left', 'bottom-right' */
170
+ anchor?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
171
+ /** Whether the item is visible */
172
+ visible?: boolean;
173
+ /** Padding from container edges */
174
+ edgePadding?: number;
175
+ /** Callback when anchor changes after snap */
176
+ onAnchorChange?: (anchor: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right') => void;
177
+ /** Custom transition */
178
+ transition?: Transition;
179
+ /** Border radius */
180
+ borderRadius?: number;
181
+ /** Box shadow */
182
+ boxShadow?: string;
183
+ /** Additional class name */
184
+ className?: string;
185
+ /** Custom style */
186
+ style?: CSSProperties;
187
+ }
188
+ /**
189
+ * Floating Grid Item component that can be dragged around the screen.
190
+ * Snaps to the nearest corner when released (like iOS/Android PiP).
191
+ * Perfect for Picture-in-Picture style floating video in zoom mode.
192
+ */
193
+ declare const FloatingGridItem: React__default.ForwardRefExoticComponent<Omit<FloatingGridItemProps, "ref"> & React__default.RefAttributes<HTMLDivElement>>;
109
194
  interface GridOverlayProps extends HTMLAttributes<HTMLDivElement> {
110
195
  /** Whether to show the overlay */
111
196
  visible?: boolean;
@@ -117,5 +202,5 @@ interface GridOverlayProps extends HTMLAttributes<HTMLDivElement> {
117
202
  */
118
203
  declare const GridOverlay: React__default.ForwardRefExoticComponent<GridOverlayProps & React__default.RefAttributes<HTMLDivElement>>;
119
204
 
120
- export { GridContainer, GridContext, GridItem, GridOverlay, useGridAnimation, useGridContext, useGridDimensions, useMeetGrid };
121
- export type { GridContainerProps, GridItemProps, GridOverlayProps };
205
+ export { FloatingGridItem, GridContainer, GridContext, GridItem, GridOverlay, useGridAnimation, useGridContext, useGridDimensions, useMeetGrid };
206
+ export type { FloatingGridItemProps, GridContainerProps, GridItemProps, GridOverlayProps };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import React__default, { RefObject, HTMLAttributes, ReactNode, CSSProperties } from 'react';
3
- import { GridDimensions, MeetGridOptions, MeetGridResult, SpringPreset, LayoutMode } from '@thangdevalone/meet-layout-grid-core';
4
- export { GridDimensions, GridOptions, LayoutMode, MeetGridOptions, MeetGridResult, PaginationInfo, Position, SpringPreset, createGrid, createGridItemPositioner, createMeetGrid, getAspectRatio, getGridItemDimensions, getSpringConfig, springPresets } from '@thangdevalone/meet-layout-grid-core';
3
+ import { GridDimensions, MeetGridOptions, MeetGridResult, SpringPreset, LayoutMode, ItemAspectRatio, ContentDimensions } from '@thangdevalone/meet-layout-grid-core';
4
+ export { ContentDimensions, GridDimensions, GridOptions, ItemAspectRatio, LayoutMode, MeetGridOptions, MeetGridResult, PaginationInfo, Position, SpringPreset, createGrid, createGridItemPositioner, createMeetGrid, getAspectRatio, getGridItemDimensions, getSpringConfig, springPresets } from '@thangdevalone/meet-layout-grid-core';
5
5
  import { HTMLMotionProps, Transition } from 'motion/react';
6
6
 
7
7
  interface GridContextValue {
@@ -59,10 +59,8 @@ interface GridContainerProps extends Omit<HTMLAttributes<HTMLDivElement>, 'child
59
59
  count?: number;
60
60
  /** Layout mode */
61
61
  layoutMode?: LayoutMode;
62
- /** Index of pinned item */
62
+ /** Index of pinned/focused item (main participant for spotlight/sidebar modes) */
63
63
  pinnedIndex?: number;
64
- /** Index of active speaker */
65
- speakerIndex?: number;
66
64
  /** Sidebar position */
67
65
  sidebarPosition?: 'left' | 'right' | 'top' | 'bottom';
68
66
  /** Sidebar ratio (0-1) */
@@ -77,21 +75,71 @@ interface GridContainerProps extends Omit<HTMLAttributes<HTMLDivElement>, 'child
77
75
  maxItemsPerPage?: number;
78
76
  /** Current page index (0-based) for pagination */
79
77
  currentPage?: number;
80
- /** Maximum visible "others" in speaker/sidebar modes (0 = show all) */
81
- maxVisibleOthers?: number;
82
- /** Current page for "others" in speaker/sidebar modes (0-based) */
83
- currentOthersPage?: number;
78
+ /** Maximum visible items (0 = show all). In gallery mode: limits all items. In sidebar: limits "others". */
79
+ maxVisible?: number;
80
+ /** Current page for visible items (0-based), used when maxVisible > 0 */
81
+ currentVisiblePage?: number;
82
+ /**
83
+ * Per-item aspect ratio configurations.
84
+ * Use different ratios for mobile (9:16), desktop (16:9), or whiteboard (fill).
85
+ * @example ['16:9', '9:16', 'fill', undefined]
86
+ */
87
+ itemAspectRatios?: (ItemAspectRatio | undefined)[];
88
+ /**
89
+ * Enable flexible cell sizing based on item aspect ratios.
90
+ * When true, portrait items (9:16) get narrower cells, landscape items (16:9) get wider cells.
91
+ * Items are packed into rows intelligently.
92
+ * @default false
93
+ */
94
+ flexLayout?: boolean;
84
95
  }
85
96
  /**
86
97
  * Container component for the meet grid.
87
98
  * Provides grid context to child GridItem components.
88
99
  */
89
100
  declare const GridContainer: React__default.ForwardRefExoticComponent<GridContainerProps & React__default.RefAttributes<HTMLDivElement>>;
90
- interface GridItemProps extends Omit<HTMLMotionProps<'div'>, 'animate' | 'initial' | 'transition'> {
101
+ interface GridItemProps extends Omit<HTMLMotionProps<'div'>, 'animate' | 'initial' | 'transition' | 'children'> {
91
102
  /** Index of this item in the grid */
92
103
  index: number;
93
- /** Children to render inside the item */
94
- children: ReactNode;
104
+ /**
105
+ * Children to render inside the item.
106
+ * Can be a ReactNode or a render function that receives contentDimensions and visibility info.
107
+ * @example
108
+ * // Simple usage
109
+ * <GridItem index={0}><Video /></GridItem>
110
+ *
111
+ * // With contentDimensions for flexible aspect ratios
112
+ * <GridItem index={0}>
113
+ * {({ contentDimensions }) => (
114
+ * <Video style={{
115
+ * width: contentDimensions.width,
116
+ * height: contentDimensions.height,
117
+ * marginTop: contentDimensions.offsetTop,
118
+ * marginLeft: contentDimensions.offsetLeft
119
+ * }} />
120
+ * )}
121
+ * </GridItem>
122
+ *
123
+ * // With hidden count for '+X more' indicator
124
+ * <GridItem index={index}>
125
+ * {({ isLastVisibleOther, hiddenCount }) => (
126
+ * <div>
127
+ * {isLastVisibleOther && hiddenCount > 0 && (
128
+ * <span className="more-indicator">+{hiddenCount}</span>
129
+ * )}
130
+ * </div>
131
+ * )}
132
+ * </GridItem>
133
+ */
134
+ children: ReactNode | ((props: {
135
+ contentDimensions: ContentDimensions;
136
+ /** True if this is the last visible item in the "others" section */
137
+ isLastVisibleOther: boolean;
138
+ /** Number of hidden items (for '+X more' indicator) */
139
+ hiddenCount: number;
140
+ }) => ReactNode);
141
+ /** Optional item-specific aspect ratio (overrides itemAspectRatios from container) */
142
+ itemAspectRatio?: ItemAspectRatio;
95
143
  /** Custom transition override */
96
144
  transition?: Transition;
97
145
  /** Whether to disable animations */
@@ -106,6 +154,43 @@ interface GridItemProps extends Omit<HTMLMotionProps<'div'>, 'animate' | 'initia
106
154
  * Automatically positions itself based on index in the grid.
107
155
  */
108
156
  declare const GridItem: React__default.ForwardRefExoticComponent<Omit<GridItemProps, "ref"> & React__default.RefAttributes<HTMLDivElement>>;
157
+ interface FloatingGridItemProps extends Omit<HTMLMotionProps<'div'>, 'animate' | 'initial' | 'children'> {
158
+ /** Children to render inside the floating item */
159
+ children: ReactNode;
160
+ /** Width of the floating item */
161
+ width?: number;
162
+ /** Height of the floating item */
163
+ height?: number;
164
+ /** Initial position (x, y from container edges) */
165
+ initialPosition?: {
166
+ x: number;
167
+ y: number;
168
+ };
169
+ /** Which corner to anchor: 'top-left', 'top-right', 'bottom-left', 'bottom-right' */
170
+ anchor?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
171
+ /** Whether the item is visible */
172
+ visible?: boolean;
173
+ /** Padding from container edges */
174
+ edgePadding?: number;
175
+ /** Callback when anchor changes after snap */
176
+ onAnchorChange?: (anchor: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right') => void;
177
+ /** Custom transition */
178
+ transition?: Transition;
179
+ /** Border radius */
180
+ borderRadius?: number;
181
+ /** Box shadow */
182
+ boxShadow?: string;
183
+ /** Additional class name */
184
+ className?: string;
185
+ /** Custom style */
186
+ style?: CSSProperties;
187
+ }
188
+ /**
189
+ * Floating Grid Item component that can be dragged around the screen.
190
+ * Snaps to the nearest corner when released (like iOS/Android PiP).
191
+ * Perfect for Picture-in-Picture style floating video in zoom mode.
192
+ */
193
+ declare const FloatingGridItem: React__default.ForwardRefExoticComponent<Omit<FloatingGridItemProps, "ref"> & React__default.RefAttributes<HTMLDivElement>>;
109
194
  interface GridOverlayProps extends HTMLAttributes<HTMLDivElement> {
110
195
  /** Whether to show the overlay */
111
196
  visible?: boolean;
@@ -117,5 +202,5 @@ interface GridOverlayProps extends HTMLAttributes<HTMLDivElement> {
117
202
  */
118
203
  declare const GridOverlay: React__default.ForwardRefExoticComponent<GridOverlayProps & React__default.RefAttributes<HTMLDivElement>>;
119
204
 
120
- export { GridContainer, GridContext, GridItem, GridOverlay, useGridAnimation, useGridContext, useGridDimensions, useMeetGrid };
121
- export type { GridContainerProps, GridItemProps, GridOverlayProps };
205
+ export { FloatingGridItem, GridContainer, GridContext, GridItem, GridOverlay, useGridAnimation, useGridContext, useGridDimensions, useMeetGrid };
206
+ export type { FloatingGridItemProps, GridContainerProps, GridItemProps, GridOverlayProps };
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import React, { createContext, useContext, useState, useEffect, useMemo, forwardRef, useRef } from 'react';
2
2
  import { createMeetGrid, getSpringConfig } from '@thangdevalone/meet-layout-grid-core';
3
3
  export { createGrid, createGridItemPositioner, createMeetGrid, getAspectRatio, getGridItemDimensions, getSpringConfig, springPresets } from '@thangdevalone/meet-layout-grid-core';
4
- import { motion } from 'motion/react';
4
+ import { motion, useMotionValue, animate } from 'motion/react';
5
5
 
6
6
  const GridContext = createContext(null);
7
7
  function useGridContext() {
@@ -41,6 +41,7 @@ function useGridDimensions(ref) {
41
41
  return dimensions;
42
42
  }
43
43
  function useMeetGrid(options) {
44
+ const itemAspectRatiosKey = options.itemAspectRatios?.join(",") ?? "";
44
45
  return useMemo(() => {
45
46
  return createMeetGrid(options);
46
47
  }, [
@@ -51,13 +52,14 @@ function useMeetGrid(options) {
51
52
  options.gap,
52
53
  options.layoutMode,
53
54
  options.pinnedIndex,
54
- options.speakerIndex,
55
55
  options.sidebarPosition,
56
56
  options.sidebarRatio,
57
57
  options.maxItemsPerPage,
58
58
  options.currentPage,
59
- options.maxVisibleOthers,
60
- options.currentOthersPage
59
+ options.maxVisible,
60
+ options.currentVisiblePage,
61
+ options.flexLayout,
62
+ itemAspectRatiosKey
61
63
  ]);
62
64
  }
63
65
  function useGridAnimation(preset = "smooth") {
@@ -72,7 +74,6 @@ const GridContainer = forwardRef(
72
74
  count,
73
75
  layoutMode = "gallery",
74
76
  pinnedIndex,
75
- speakerIndex,
76
77
  sidebarPosition,
77
78
  sidebarRatio,
78
79
  springPreset = "smooth",
@@ -80,8 +81,10 @@ const GridContainer = forwardRef(
80
81
  className,
81
82
  maxItemsPerPage,
82
83
  currentPage,
83
- maxVisibleOthers,
84
- currentOthersPage,
84
+ maxVisible,
85
+ currentVisiblePage,
86
+ itemAspectRatios,
87
+ flexLayout,
85
88
  ...props
86
89
  }, forwardedRef) {
87
90
  const internalRef = useRef(null);
@@ -95,13 +98,14 @@ const GridContainer = forwardRef(
95
98
  gap,
96
99
  layoutMode,
97
100
  pinnedIndex,
98
- speakerIndex,
99
101
  sidebarPosition,
100
102
  sidebarRatio,
101
103
  maxItemsPerPage,
102
104
  currentPage,
103
- maxVisibleOthers,
104
- currentOthersPage
105
+ maxVisible,
106
+ currentVisiblePage,
107
+ itemAspectRatios,
108
+ flexLayout
105
109
  };
106
110
  const grid = useMeetGrid(gridOptions);
107
111
  const containerStyle = {
@@ -118,6 +122,7 @@ const GridItem = forwardRef(
118
122
  function GridItem2({
119
123
  index,
120
124
  children,
125
+ itemAspectRatio,
121
126
  transition: customTransition,
122
127
  disableAnimation = false,
123
128
  className,
@@ -133,6 +138,7 @@ const GridItem = forwardRef(
133
138
  }
134
139
  const { top, left } = grid.getPosition(index);
135
140
  const { width, height } = grid.getItemDimensions(index);
141
+ const contentDimensions = grid.getItemContentDimensions(index, itemAspectRatio);
136
142
  const isMain = grid.isMainItem(index);
137
143
  if (grid.layoutMode === "spotlight" && !isMain) {
138
144
  return null;
@@ -150,6 +156,15 @@ const GridItem = forwardRef(
150
156
  top,
151
157
  left
152
158
  };
159
+ const lastVisibleOthersIndex = grid.getLastVisibleOthersIndex();
160
+ const isLastVisibleOther = index === lastVisibleOthersIndex;
161
+ const hiddenCount = grid.hiddenCount;
162
+ const renderChildren = () => {
163
+ if (typeof children === "function") {
164
+ return children({ contentDimensions, isLastVisibleOther, hiddenCount });
165
+ }
166
+ return children;
167
+ };
153
168
  if (disableAnimation) {
154
169
  return /* @__PURE__ */ React.createElement(
155
170
  "div",
@@ -161,7 +176,7 @@ const GridItem = forwardRef(
161
176
  "data-grid-main": isMain,
162
177
  ...props
163
178
  },
164
- children
179
+ renderChildren()
165
180
  );
166
181
  }
167
182
  return /* @__PURE__ */ React.createElement(
@@ -178,6 +193,125 @@ const GridItem = forwardRef(
178
193
  "data-grid-main": isMain,
179
194
  ...props
180
195
  },
196
+ renderChildren()
197
+ );
198
+ }
199
+ );
200
+ const FloatingGridItem = forwardRef(
201
+ function FloatingGridItem2({
202
+ children,
203
+ width = 120,
204
+ height = 160,
205
+ initialPosition = { x: 16, y: 16 },
206
+ anchor: initialAnchor = "bottom-right",
207
+ visible = true,
208
+ edgePadding = 12,
209
+ onAnchorChange,
210
+ transition,
211
+ borderRadius = 12,
212
+ boxShadow = "0 4px 20px rgba(0,0,0,0.3)",
213
+ className,
214
+ style,
215
+ ...props
216
+ }, ref) {
217
+ const { dimensions } = useGridContext();
218
+ const [currentAnchor, setCurrentAnchor] = React.useState(initialAnchor);
219
+ const x = useMotionValue(0);
220
+ const y = useMotionValue(0);
221
+ const [isInitialized, setIsInitialized] = React.useState(false);
222
+ const getCornerPosition = React.useCallback((corner) => {
223
+ const padding = edgePadding + initialPosition.x;
224
+ switch (corner) {
225
+ case "top-left":
226
+ return { x: padding, y: padding };
227
+ case "top-right":
228
+ return { x: dimensions.width - width - padding, y: padding };
229
+ case "bottom-left":
230
+ return { x: padding, y: dimensions.height - height - padding };
231
+ case "bottom-right":
232
+ default:
233
+ return { x: dimensions.width - width - padding, y: dimensions.height - height - padding };
234
+ }
235
+ }, [dimensions.width, dimensions.height, width, height, edgePadding, initialPosition.x]);
236
+ React.useEffect(() => {
237
+ if (dimensions.width > 0 && dimensions.height > 0 && !isInitialized) {
238
+ const pos = getCornerPosition(currentAnchor);
239
+ x.set(pos.x);
240
+ y.set(pos.y);
241
+ setIsInitialized(true);
242
+ }
243
+ }, [dimensions.width, dimensions.height, currentAnchor, getCornerPosition, isInitialized, x, y]);
244
+ React.useEffect(() => {
245
+ if (isInitialized && dimensions.width > 0 && dimensions.height > 0) {
246
+ const pos = getCornerPosition(currentAnchor);
247
+ const springConfig = { type: "spring", stiffness: 400, damping: 30 };
248
+ animate(x, pos.x, springConfig);
249
+ animate(y, pos.y, springConfig);
250
+ }
251
+ }, [currentAnchor, dimensions.width, dimensions.height, getCornerPosition, isInitialized, x, y]);
252
+ if (!visible || dimensions.width === 0 || dimensions.height === 0)
253
+ return null;
254
+ const findNearestCorner = (posX, posY) => {
255
+ const centerX = posX + width / 2;
256
+ const centerY = posY + height / 2;
257
+ const containerCenterX = dimensions.width / 2;
258
+ const containerCenterY = dimensions.height / 2;
259
+ const isLeft = centerX < containerCenterX;
260
+ const isTop = centerY < containerCenterY;
261
+ if (isTop && isLeft)
262
+ return "top-left";
263
+ if (isTop && !isLeft)
264
+ return "top-right";
265
+ if (!isTop && isLeft)
266
+ return "bottom-left";
267
+ return "bottom-right";
268
+ };
269
+ const dragConstraints = {
270
+ left: edgePadding,
271
+ right: dimensions.width - width - edgePadding,
272
+ top: edgePadding,
273
+ bottom: dimensions.height - height - edgePadding
274
+ };
275
+ const floatingStyle = {
276
+ position: "absolute",
277
+ width,
278
+ height,
279
+ borderRadius,
280
+ boxShadow,
281
+ overflow: "hidden",
282
+ cursor: "grab",
283
+ zIndex: 100,
284
+ touchAction: "none",
285
+ left: 0,
286
+ top: 0,
287
+ ...style
288
+ };
289
+ const handleDragEnd = () => {
290
+ const currentX = x.get();
291
+ const currentY = y.get();
292
+ const nearestCorner = findNearestCorner(currentX, currentY);
293
+ setCurrentAnchor(nearestCorner);
294
+ onAnchorChange?.(nearestCorner);
295
+ const snapPos = getCornerPosition(nearestCorner);
296
+ const springConfig = { type: "spring", stiffness: 400, damping: 30 };
297
+ animate(x, snapPos.x, springConfig);
298
+ animate(y, snapPos.y, springConfig);
299
+ };
300
+ return /* @__PURE__ */ React.createElement(
301
+ motion.div,
302
+ {
303
+ ref,
304
+ drag: true,
305
+ dragMomentum: false,
306
+ dragElastic: 0.1,
307
+ dragConstraints,
308
+ style: { ...floatingStyle, x, y },
309
+ className,
310
+ onDragEnd: handleDragEnd,
311
+ whileDrag: { cursor: "grabbing", scale: 1.05, boxShadow: "0 8px 32px rgba(0,0,0,0.4)" },
312
+ transition: transition ?? { type: "spring", stiffness: 400, damping: 30 },
313
+ ...props
314
+ },
181
315
  children
182
316
  );
183
317
  }
@@ -206,4 +340,4 @@ const GridOverlay = forwardRef(
206
340
  }
207
341
  );
208
342
 
209
- export { GridContainer, GridContext, GridItem, GridOverlay, useGridAnimation, useGridContext, useGridDimensions, useMeetGrid };
343
+ export { FloatingGridItem, GridContainer, GridContext, GridItem, GridOverlay, useGridAnimation, useGridContext, useGridDimensions, useMeetGrid };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thangdevalone/meet-layout-grid-react",
3
- "version": "1.0.8",
3
+ "version": "1.1.0",
4
4
  "description": "React integration for meet-layout-grid with Motion animations",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -42,7 +42,7 @@
42
42
  },
43
43
  "dependencies": {
44
44
  "motion": "^11.15.0",
45
- "@thangdevalone/meet-layout-grid-core": "1.0.8"
45
+ "@thangdevalone/meet-layout-grid-core": "1.1.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/react": "^18.2.0",