concertina 0.7.1 → 0.8.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.cjs CHANGED
@@ -45,13 +45,16 @@ __export(index_exports, {
45
45
  pinToScrollTop: () => pinToScrollTop,
46
46
  useConcertina: () => useConcertina,
47
47
  useExpanded: () => useExpanded,
48
+ usePresence: () => usePresence2,
49
+ useScrollPin: () => useScrollPin,
50
+ useSize: () => useSize,
48
51
  useStableSlot: () => useStableSlot,
49
52
  useTransitionLock: () => useTransitionLock
50
53
  });
51
54
  module.exports = __toCommonJS(index_exports);
52
55
 
53
- // src/root.tsx
54
- var import_react6 = require("react");
56
+ // src/accordion/root.tsx
57
+ var import_react7 = require("react");
55
58
 
56
59
  // node_modules/@radix-ui/react-accordion/dist/index.mjs
57
60
  var import_react3 = __toESM(require("react"), 1);
@@ -1012,7 +1015,7 @@ var Header = AccordionHeader;
1012
1015
  var Trigger2 = AccordionTrigger;
1013
1016
  var Content2 = AccordionContent;
1014
1017
 
1015
- // src/store.ts
1018
+ // src/accordion/store.ts
1016
1019
  var import_react4 = require("react");
1017
1020
  var ConcertinaStore = class {
1018
1021
  constructor() {
@@ -1041,7 +1044,10 @@ var ConcertinaStore = class {
1041
1044
  };
1042
1045
  var ConcertinaContext = (0, import_react4.createContext)(null);
1043
1046
 
1044
- // src/pin-to-scroll-top.ts
1047
+ // src/primitives/use-scroll-pin.ts
1048
+ var import_react5 = require("react");
1049
+
1050
+ // src/primitives/pin-to-scroll-top.ts
1045
1051
  function pinToScrollTop(el) {
1046
1052
  if (!el) return;
1047
1053
  let parent = el.parentElement;
@@ -1071,33 +1077,42 @@ function pinToScrollTop(el) {
1071
1077
  }
1072
1078
  }
1073
1079
 
1074
- // src/use-transition-lock.ts
1075
- var import_react5 = require("react");
1080
+ // src/primitives/use-scroll-pin.ts
1081
+ function useScrollPin(getElement, deps) {
1082
+ (0, import_react5.useLayoutEffect)(() => {
1083
+ const el = getElement();
1084
+ if (!el) return;
1085
+ pinToScrollTop(el);
1086
+ }, deps);
1087
+ }
1088
+
1089
+ // src/primitives/use-transition-lock.ts
1090
+ var import_react6 = require("react");
1076
1091
  function useTransitionLock() {
1077
- const [locked, setLocked] = (0, import_react5.useState)(false);
1078
- const lock = (0, import_react5.useCallback)(() => setLocked(true), []);
1079
- (0, import_react5.useEffect)(() => {
1092
+ const [locked, setLocked] = (0, import_react6.useState)(false);
1093
+ const lock = (0, import_react6.useCallback)(() => setLocked(true), []);
1094
+ (0, import_react6.useEffect)(() => {
1080
1095
  if (locked) setLocked(false);
1081
1096
  }, [locked]);
1082
1097
  return { locked, lock };
1083
1098
  }
1084
1099
 
1085
- // src/root.tsx
1100
+ // src/accordion/root.tsx
1086
1101
  var import_jsx_runtime9 = require("react/jsx-runtime");
1087
- var Root3 = (0, import_react6.forwardRef)(
1102
+ var Root3 = (0, import_react7.forwardRef)(
1088
1103
  function Root4({ collapsible = true, children, ...props }, forwardedRef) {
1089
- const storeRef = (0, import_react6.useRef)(null);
1104
+ const storeRef = (0, import_react7.useRef)(null);
1090
1105
  if (!storeRef.current) {
1091
1106
  storeRef.current = new ConcertinaStore();
1092
1107
  }
1093
1108
  const store = storeRef.current;
1094
- const value = (0, import_react6.useSyncExternalStore)(
1109
+ const value = (0, import_react7.useSyncExternalStore)(
1095
1110
  store.subscribe,
1096
1111
  store.getValue,
1097
1112
  store.getValue
1098
1113
  );
1099
1114
  const { locked, lock } = useTransitionLock();
1100
- const onValueChange = (0, import_react6.useCallback)(
1115
+ const onValueChange = (0, import_react7.useCallback)(
1101
1116
  (newValue) => {
1102
1117
  const isSwitching = !!store.getValue() && store.getValue() !== newValue && !!newValue;
1103
1118
  if (isSwitching) lock();
@@ -1105,10 +1120,10 @@ var Root3 = (0, import_react6.forwardRef)(
1105
1120
  },
1106
1121
  [store, lock]
1107
1122
  );
1108
- (0, import_react6.useLayoutEffect)(() => {
1109
- if (!value) return;
1110
- pinToScrollTop(store.getItemRef(value));
1111
- }, [value, store]);
1123
+ useScrollPin(
1124
+ () => value ? store.getItemRef(value) : null,
1125
+ [value, store]
1126
+ );
1112
1127
  return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ConcertinaContext.Provider, { value: store, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1113
1128
  Root2,
1114
1129
  {
@@ -1125,12 +1140,12 @@ var Root3 = (0, import_react6.forwardRef)(
1125
1140
  }
1126
1141
  );
1127
1142
 
1128
- // src/item.tsx
1129
- var import_react7 = require("react");
1143
+ // src/accordion/item.tsx
1144
+ var import_react8 = require("react");
1130
1145
  var import_jsx_runtime10 = require("react/jsx-runtime");
1131
- var Item2 = (0, import_react7.forwardRef)(function Item3({ value, ...props }, forwardedRef) {
1132
- const store = (0, import_react7.useContext)(ConcertinaContext);
1133
- const mergedRef = (0, import_react7.useCallback)(
1146
+ var Item2 = (0, import_react8.forwardRef)(function Item3({ value, ...props }, forwardedRef) {
1147
+ const store = (0, import_react8.useContext)(ConcertinaContext);
1148
+ const mergedRef = (0, import_react8.useCallback)(
1134
1149
  (el) => {
1135
1150
  if (typeof forwardedRef === "function") {
1136
1151
  forwardedRef(el);
@@ -1144,18 +1159,18 @@ var Item2 = (0, import_react7.forwardRef)(function Item3({ value, ...props }, fo
1144
1159
  return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Item, { ref: mergedRef, value, ...props });
1145
1160
  });
1146
1161
 
1147
- // src/content.tsx
1148
- var import_react8 = require("react");
1162
+ // src/accordion/content.tsx
1163
+ var import_react9 = require("react");
1149
1164
  var import_jsx_runtime11 = require("react/jsx-runtime");
1150
- var Content3 = (0, import_react8.forwardRef)(function Content4({ className, ...props }, ref) {
1165
+ var Content3 = (0, import_react9.forwardRef)(function Content4({ className, ...props }, ref) {
1151
1166
  const merged = className ? `concertina-content ${className}` : "concertina-content";
1152
1167
  return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Content2, { ref, className: merged, ...props });
1153
1168
  });
1154
1169
 
1155
- // src/use-expanded.ts
1156
- var import_react9 = require("react");
1170
+ // src/accordion/use-expanded.ts
1171
+ var import_react10 = require("react");
1157
1172
  function useStore() {
1158
- const store = (0, import_react9.useContext)(ConcertinaContext);
1173
+ const store = (0, import_react10.useContext)(ConcertinaContext);
1159
1174
  if (!store) {
1160
1175
  throw new Error("useExpanded must be used inside <Concertina.Root>");
1161
1176
  }
@@ -1163,7 +1178,7 @@ function useStore() {
1163
1178
  }
1164
1179
  function useExpanded(id) {
1165
1180
  const store = useStore();
1166
- return (0, import_react9.useSyncExternalStore)(
1181
+ return (0, import_react10.useSyncExternalStore)(
1167
1182
  store.subscribe,
1168
1183
  () => store.getValue() === id,
1169
1184
  () => false
@@ -1171,26 +1186,26 @@ function useExpanded(id) {
1171
1186
  );
1172
1187
  }
1173
1188
 
1174
- // src/stable-slot.tsx
1175
- var import_react10 = require("react");
1189
+ // src/components/stable-slot.tsx
1190
+ var import_react11 = require("react");
1176
1191
  var import_jsx_runtime12 = require("react/jsx-runtime");
1177
- var AxisContext = (0, import_react10.createContext)("both");
1178
- var StableSlot = (0, import_react10.forwardRef)(
1192
+ var AxisContext = (0, import_react11.createContext)("both");
1193
+ var StableSlot = (0, import_react11.forwardRef)(
1179
1194
  function StableSlot2({ axis = "both", as: Tag = "div", className, style, children, ...props }, ref) {
1180
1195
  const merged = className ? `concertina-stable-slot ${className}` : "concertina-stable-slot";
1181
1196
  return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(AxisContext.Provider, { value: axis, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Tag, { ref, className: merged, style, ...props, children }) });
1182
1197
  }
1183
1198
  );
1184
1199
 
1185
- // src/slot.tsx
1186
- var import_react11 = require("react");
1200
+ // src/components/slot.tsx
1201
+ var import_react12 = require("react");
1187
1202
  var import_jsx_runtime13 = require("react/jsx-runtime");
1188
1203
  function inactiveStyle(_axis) {
1189
1204
  return { visibility: "hidden" };
1190
1205
  }
1191
- var Slot = (0, import_react11.forwardRef)(
1206
+ var Slot = (0, import_react12.forwardRef)(
1192
1207
  function Slot2({ active, as: Tag = "div", style, children, ...props }, ref) {
1193
- const axis = (0, import_react11.useContext)(AxisContext);
1208
+ const axis = (0, import_react12.useContext)(AxisContext);
1194
1209
  const merged = active ? { ...style } : { ...inactiveStyle(axis), ...style };
1195
1210
  return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1196
1211
  Tag,
@@ -1205,14 +1220,17 @@ var Slot = (0, import_react11.forwardRef)(
1205
1220
  }
1206
1221
  );
1207
1222
 
1208
- // src/use-stable-slot.ts
1209
- var import_react12 = require("react");
1223
+ // src/components/gigbag.tsx
1224
+ var import_react14 = require("react");
1225
+
1226
+ // src/primitives/use-stable-slot.ts
1227
+ var import_react13 = require("react");
1210
1228
  function useStableSlot(options = {}) {
1211
1229
  const { axis = "both" } = options;
1212
- const [style, setStyle] = (0, import_react12.useState)({});
1213
- const maxRef = (0, import_react12.useRef)({ w: 0, h: 0 });
1214
- const observerRef = (0, import_react12.useRef)(null);
1215
- const ref = (0, import_react12.useCallback)(
1230
+ const [style, setStyle] = (0, import_react13.useState)({});
1231
+ const maxRef = (0, import_react13.useRef)({ w: 0, h: 0 });
1232
+ const observerRef = (0, import_react13.useRef)(null);
1233
+ const ref = (0, import_react13.useCallback)(
1216
1234
  (el) => {
1217
1235
  if (observerRef.current) {
1218
1236
  observerRef.current.disconnect();
@@ -1258,10 +1276,9 @@ function useStableSlot(options = {}) {
1258
1276
  return { ref, style };
1259
1277
  }
1260
1278
 
1261
- // src/gigbag.tsx
1262
- var import_react13 = require("react");
1279
+ // src/components/gigbag.tsx
1263
1280
  var import_jsx_runtime14 = require("react/jsx-runtime");
1264
- var Gigbag = (0, import_react13.forwardRef)(
1281
+ var Gigbag = (0, import_react14.forwardRef)(
1265
1282
  function Gigbag2({ axis = "height", as: Tag = "div", className, style, children, ...props }, fwdRef) {
1266
1283
  const { ref: ratchetRef, style: ratchetStyle } = useStableSlot({ axis });
1267
1284
  const merged = className ? `concertina-gigbag ${className}` : "concertina-gigbag";
@@ -1282,52 +1299,64 @@ var Gigbag = (0, import_react13.forwardRef)(
1282
1299
  }
1283
1300
  );
1284
1301
 
1285
- // src/warmup.tsx
1286
- var import_react14 = require("react");
1302
+ // src/components/warmup.tsx
1303
+ var import_react15 = require("react");
1287
1304
  var import_jsx_runtime15 = require("react/jsx-runtime");
1288
- var Warmup = (0, import_react14.forwardRef)(
1305
+ var Warmup = (0, import_react15.forwardRef)(
1289
1306
  function Warmup2({ rows = 3, columns = 1, as: Tag = "div", className, children, ...props }, ref) {
1290
1307
  const merged = className ? `concertina-warmup ${className}` : "concertina-warmup";
1291
1308
  const cells = Array.from({ length: rows * columns }, (_, i) => /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "concertina-warmup-bone", children: [
1292
1309
  /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "concertina-warmup-line concertina-warmup-line-short" }),
1293
1310
  /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "concertina-warmup-line concertina-warmup-line-long" })
1294
1311
  ] }, i));
1295
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
1312
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
1296
1313
  Tag,
1297
1314
  {
1298
1315
  ref,
1299
1316
  className: merged,
1300
1317
  style: { gridTemplateColumns: `repeat(${columns}, 1fr)` },
1301
1318
  ...props,
1302
- children: cells
1319
+ children: [
1320
+ children,
1321
+ cells
1322
+ ]
1303
1323
  }
1304
1324
  );
1305
1325
  }
1306
1326
  );
1307
1327
 
1308
- // src/glide.tsx
1309
- var import_react15 = require("react");
1328
+ // src/components/glide.tsx
1329
+ var import_react17 = require("react");
1330
+
1331
+ // src/primitives/use-presence.ts
1332
+ var import_react16 = require("react");
1333
+ function usePresence2(show) {
1334
+ const [mounted, setMounted] = (0, import_react16.useState)(show);
1335
+ const [phase, setPhase] = (0, import_react16.useState)(show ? "entered" : "exiting");
1336
+ (0, import_react16.useEffect)(() => {
1337
+ if (show) {
1338
+ setMounted(true);
1339
+ setPhase("entering");
1340
+ } else if (mounted) {
1341
+ setPhase("exiting");
1342
+ }
1343
+ }, [show]);
1344
+ const onAnimationEnd = (0, import_react16.useCallback)(
1345
+ (e) => {
1346
+ if (e.target !== e.currentTarget) return;
1347
+ if (phase === "entering") setPhase("entered");
1348
+ if (phase === "exiting") setMounted(false);
1349
+ },
1350
+ [phase]
1351
+ );
1352
+ return { mounted, phase, onAnimationEnd };
1353
+ }
1354
+
1355
+ // src/components/glide.tsx
1310
1356
  var import_jsx_runtime16 = require("react/jsx-runtime");
1311
- var Glide = (0, import_react15.forwardRef)(
1357
+ var Glide = (0, import_react17.forwardRef)(
1312
1358
  function Glide2({ show, as: Tag = "div", className, children, ...props }, ref) {
1313
- const [mounted, setMounted] = (0, import_react15.useState)(show);
1314
- const [phase, setPhase] = (0, import_react15.useState)(show ? "entered" : "exiting");
1315
- (0, import_react15.useEffect)(() => {
1316
- if (show) {
1317
- setMounted(true);
1318
- setPhase("entering");
1319
- } else if (mounted) {
1320
- setPhase("exiting");
1321
- }
1322
- }, [show]);
1323
- const onAnimationEnd = (0, import_react15.useCallback)(
1324
- (e) => {
1325
- if (e.target !== e.currentTarget) return;
1326
- if (phase === "entering") setPhase("entered");
1327
- if (phase === "exiting") setMounted(false);
1328
- },
1329
- [phase]
1330
- );
1359
+ const { mounted, phase, onAnimationEnd } = usePresence2(show);
1331
1360
  if (!mounted) return null;
1332
1361
  const phaseClass = phase === "entering" ? "concertina-glide-entering" : phase === "exiting" ? "concertina-glide-exiting" : "";
1333
1362
  const merged = ["concertina-glide", phaseClass, className].filter(Boolean).join(" ");
@@ -1344,13 +1373,46 @@ var Glide = (0, import_react15.forwardRef)(
1344
1373
  }
1345
1374
  );
1346
1375
 
1347
- // src/use-concertina.ts
1348
- var import_react16 = require("react");
1376
+ // src/primitives/use-size.ts
1377
+ var import_react18 = require("react");
1378
+ function useSize() {
1379
+ const [size, setSize] = (0, import_react18.useState)({ width: 0, height: 0 });
1380
+ const observerRef = (0, import_react18.useRef)(null);
1381
+ const ref = (0, import_react18.useCallback)((el) => {
1382
+ if (observerRef.current) {
1383
+ observerRef.current.disconnect();
1384
+ observerRef.current = null;
1385
+ }
1386
+ if (!el || typeof ResizeObserver === "undefined") return;
1387
+ const observer = new ResizeObserver((entries) => {
1388
+ for (const entry of entries) {
1389
+ let w;
1390
+ let h;
1391
+ if (entry.borderBoxSize?.length) {
1392
+ const box = entry.borderBoxSize[0];
1393
+ w = box.inlineSize;
1394
+ h = box.blockSize;
1395
+ } else {
1396
+ const rect = entry.target.getBoundingClientRect();
1397
+ w = rect.width;
1398
+ h = rect.height;
1399
+ }
1400
+ setSize({ width: w, height: h });
1401
+ }
1402
+ });
1403
+ observer.observe(el, { box: "border-box" });
1404
+ observerRef.current = observer;
1405
+ }, []);
1406
+ return { ref, size };
1407
+ }
1408
+
1409
+ // src/accordion/use-concertina.ts
1410
+ var import_react19 = require("react");
1349
1411
  function useConcertina() {
1350
- const [value, setValue] = (0, import_react16.useState)("");
1351
- const [switching, setSwitching] = (0, import_react16.useState)(false);
1352
- const itemRefs = (0, import_react16.useRef)({});
1353
- const onValueChange = (0, import_react16.useCallback)(
1412
+ const [value, setValue] = (0, import_react19.useState)("");
1413
+ const [switching, setSwitching] = (0, import_react19.useState)(false);
1414
+ const itemRefs = (0, import_react19.useRef)({});
1415
+ const onValueChange = (0, import_react19.useCallback)(
1354
1416
  (newValue) => {
1355
1417
  if (!newValue) {
1356
1418
  setSwitching(false);
@@ -1362,14 +1424,14 @@ function useConcertina() {
1362
1424
  },
1363
1425
  [value]
1364
1426
  );
1365
- (0, import_react16.useLayoutEffect)(() => {
1427
+ (0, import_react19.useLayoutEffect)(() => {
1366
1428
  if (!value) return;
1367
1429
  pinToScrollTop(itemRefs.current[value]);
1368
1430
  }, [value]);
1369
- (0, import_react16.useEffect)(() => {
1431
+ (0, import_react19.useEffect)(() => {
1370
1432
  if (switching) setSwitching(false);
1371
1433
  }, [switching]);
1372
- const getItemRef = (0, import_react16.useCallback)(
1434
+ const getItemRef = (0, import_react19.useCallback)(
1373
1435
  (id) => (el) => {
1374
1436
  itemRefs.current[id] = el;
1375
1437
  },
@@ -1399,6 +1461,9 @@ function useConcertina() {
1399
1461
  pinToScrollTop,
1400
1462
  useConcertina,
1401
1463
  useExpanded,
1464
+ usePresence,
1465
+ useScrollPin,
1466
+ useSize,
1402
1467
  useStableSlot,
1403
1468
  useTransitionLock
1404
1469
  });
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { HTMLAttributes, ElementType, CSSProperties } from 'react';
2
+ import { HTMLAttributes, ElementType, AnimationEvent, DependencyList, CSSProperties } from 'react';
3
3
  import * as Accordion from '@radix-ui/react-accordion';
4
4
  export { Header, Trigger } from '@radix-ui/react-accordion';
5
5
 
@@ -73,48 +73,6 @@ interface SlotProps extends HTMLAttributes<HTMLElement> {
73
73
  */
74
74
  declare const Slot: react.ForwardRefExoticComponent<SlotProps & react.RefAttributes<HTMLElement>>;
75
75
 
76
- interface UseStableSlotOptions {
77
- /** Which axis to ratchet. Default: "both". */
78
- axis?: Axis;
79
- }
80
- interface UseStableSlotReturn {
81
- /** RefCallback — attach to the container element. */
82
- ref: (el: HTMLElement | null) => void;
83
- /** Spread onto the element: { minWidth?, minHeight? } */
84
- style: CSSProperties;
85
- }
86
- /**
87
- * ResizeObserver ratchet for dynamic content.
88
- *
89
- * Watches the element, tracks maximum width/height ever observed,
90
- * applies min-width/min-height that only ratchets up.
91
- *
92
- * Five things work together:
93
- * 1. ResizeObserver uses borderBoxSize — includes padding/border
94
- * 2. Ratchet is one-way — max only increases, never resets
95
- * 3. setStyle only called when ratchet grows — no infinite loops
96
- * 4. RefCallback disconnects observer on unmount — no leak
97
- * 5. SSR graceful no-op — typeof ResizeObserver guard
98
- */
99
- declare function useStableSlot(options?: UseStableSlotOptions): UseStableSlotReturn;
100
-
101
- /**
102
- * Suppress CSS transitions during batched state changes.
103
- *
104
- * Three things work together:
105
- * 1. lock() sets the flag synchronously — batched with state changes in React 18
106
- * 2. After DOM commit (useLayoutEffect window) — consumer does measurement/scroll/pin work
107
- * 3. useEffect auto-clears the flag after paint — transitions re-enable
108
- *
109
- * Usage:
110
- * const { locked, lock } = useTransitionLock();
111
- * <div data-locked={locked || undefined}>...</div>
112
- */
113
- declare function useTransitionLock(): {
114
- readonly locked: boolean;
115
- readonly lock: () => void;
116
- };
117
-
118
76
  interface GigbagProps extends HTMLAttributes<HTMLElement> {
119
77
  /** Which axis to ratchet. Default: "height". */
120
78
  axis?: Axis;
@@ -147,6 +105,9 @@ interface WarmupProps extends HTMLAttributes<HTMLElement> {
147
105
  * dimensions of the real content. Pair with <Gigbag> so the
148
106
  * container ratchets to the larger of placeholder vs real content.
149
107
  *
108
+ * Pass children to inject structure before the generated bones
109
+ * (e.g. a toolbar placeholder that spans all grid columns).
110
+ *
150
111
  * All dimensions are CSS custom properties — consuming apps theme
151
112
  * without forking.
152
113
  */
@@ -161,14 +122,105 @@ interface GlideProps extends HTMLAttributes<HTMLElement> {
161
122
  /**
162
123
  * Enter/exit animation wrapper.
163
124
  *
164
- * State machine:
165
- * show=true -> mount + "entering" -> animationEnd -> "entered"
166
- * show=false -> "exiting" -> animationEnd -> unmount
167
- *
168
- * CSS classes: concertina-glide-entering, concertina-glide-exiting
125
+ * Thin composition over usePresence. Adds CSS class names:
126
+ * concertina-glide-entering, concertina-glide-exiting
169
127
  */
170
128
  declare const Glide: react.ForwardRefExoticComponent<GlideProps & react.RefAttributes<HTMLElement>>;
171
129
 
130
+ interface Size {
131
+ width: number;
132
+ height: number;
133
+ }
134
+ interface UseSizeReturn {
135
+ /** RefCallback — attach to the element to observe. */
136
+ ref: (el: HTMLElement | null) => void;
137
+ /** Current border-box size. Starts at { width: 0, height: 0 }. */
138
+ size: Size;
139
+ }
140
+ /**
141
+ * Raw border-box size observation via ResizeObserver.
142
+ *
143
+ * Reports every resize — no ratchet, no policy. Use this when you
144
+ * need the actual current size for your own logic (e.g. breakpoints,
145
+ * conditional rendering, animations).
146
+ *
147
+ * For a ratcheting min-size that only grows, use useStableSlot instead.
148
+ */
149
+ declare function useSize(): UseSizeReturn;
150
+
151
+ type Phase = "entering" | "entered" | "exiting";
152
+ interface UsePresenceReturn {
153
+ /** Whether the element should be in the DOM. */
154
+ mounted: boolean;
155
+ /** Current animation phase. */
156
+ phase: Phase;
157
+ /** Attach to the animating element's onAnimationEnd. */
158
+ onAnimationEnd: (e: AnimationEvent) => void;
159
+ }
160
+ /**
161
+ * Mount/unmount state machine for enter/exit animations.
162
+ *
163
+ * State transitions:
164
+ * show=true → mount + "entering" → animationEnd → "entered"
165
+ * show=false → "exiting" → animationEnd → unmount
166
+ *
167
+ * Extracted from Glide so any component can use animated presence.
168
+ */
169
+ declare function usePresence(show: boolean): UsePresenceReturn;
170
+
171
+ /**
172
+ * Pin an element to the top of its scroll container after layout changes.
173
+ *
174
+ * Runs pinToScrollTop inside useLayoutEffect — after React commits
175
+ * the DOM but before the browser paints. This ensures scroll
176
+ * correction happens synchronously with layout changes.
177
+ *
178
+ * Extracted from accordion Root so any component can do scroll pinning.
179
+ */
180
+ declare function useScrollPin(getElement: () => HTMLElement | null, deps: DependencyList): void;
181
+
182
+ interface UseStableSlotOptions {
183
+ /** Which axis to ratchet. Default: "both". */
184
+ axis?: Axis;
185
+ }
186
+ interface UseStableSlotReturn {
187
+ /** RefCallback — attach to the container element. */
188
+ ref: (el: HTMLElement | null) => void;
189
+ /** Spread onto the element: { minWidth?, minHeight? } */
190
+ style: CSSProperties;
191
+ }
192
+ /**
193
+ * ResizeObserver ratchet for dynamic content.
194
+ *
195
+ * Watches the element, tracks maximum width/height ever observed,
196
+ * applies min-width/min-height that only ratchets up.
197
+ *
198
+ * Five things work together:
199
+ * 1. ResizeObserver uses borderBoxSize — includes padding/border
200
+ * 2. Ratchet is one-way — max only increases, never resets
201
+ * 3. setStyle only called when ratchet grows — no infinite loops
202
+ * 4. RefCallback disconnects observer on unmount — no leak
203
+ * 5. SSR graceful no-op — typeof ResizeObserver guard
204
+ */
205
+ declare function useStableSlot(options?: UseStableSlotOptions): UseStableSlotReturn;
206
+
207
+ /**
208
+ * Suppress CSS transitions during batched state changes.
209
+ *
210
+ * Three things work together:
211
+ * 1. lock() sets the flag synchronously — batched with state changes in React 18
212
+ * 2. After DOM commit (useLayoutEffect window) — consumer does measurement/scroll/pin work
213
+ * 3. useEffect auto-clears the flag after paint — transitions re-enable
214
+ *
215
+ * Usage:
216
+ * const { locked, lock } = useTransitionLock();
217
+ * <div data-locked={locked || undefined}>...</div>
218
+ */
219
+ declare function useTransitionLock(): {
220
+ readonly locked: boolean;
221
+ readonly lock: () => void;
222
+ };
223
+
172
224
  /**
173
225
  * Scroll `el` to the top of its nearest scrollable ancestor,
174
226
  * clearing any sticky headers. Only adjusts one container's
@@ -211,4 +263,4 @@ interface UseConcertinaReturn {
211
263
  */
212
264
  declare function useConcertina(): UseConcertinaReturn;
213
265
 
214
- export { type Axis, ConcertinaContext, type ConcertinaRootProps, ConcertinaStore, Content, Gigbag, type GigbagProps, Glide, type GlideProps, Item, Root, Slot, type SlotProps, StableSlot, type StableSlotProps, type UseConcertinaReturn, Warmup, type WarmupProps, pinToScrollTop, useConcertina, useExpanded, useStableSlot, useTransitionLock };
266
+ export { type Axis, ConcertinaContext, type ConcertinaRootProps, ConcertinaStore, Content, Gigbag, type GigbagProps, Glide, type GlideProps, Item, type Phase, Root, type Size, Slot, type SlotProps, StableSlot, type StableSlotProps, type UseConcertinaReturn, type UsePresenceReturn, Warmup, type WarmupProps, pinToScrollTop, useConcertina, useExpanded, usePresence, useScrollPin, useSize, useStableSlot, useTransitionLock };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { HTMLAttributes, ElementType, CSSProperties } from 'react';
2
+ import { HTMLAttributes, ElementType, AnimationEvent, DependencyList, CSSProperties } from 'react';
3
3
  import * as Accordion from '@radix-ui/react-accordion';
4
4
  export { Header, Trigger } from '@radix-ui/react-accordion';
5
5
 
@@ -73,48 +73,6 @@ interface SlotProps extends HTMLAttributes<HTMLElement> {
73
73
  */
74
74
  declare const Slot: react.ForwardRefExoticComponent<SlotProps & react.RefAttributes<HTMLElement>>;
75
75
 
76
- interface UseStableSlotOptions {
77
- /** Which axis to ratchet. Default: "both". */
78
- axis?: Axis;
79
- }
80
- interface UseStableSlotReturn {
81
- /** RefCallback — attach to the container element. */
82
- ref: (el: HTMLElement | null) => void;
83
- /** Spread onto the element: { minWidth?, minHeight? } */
84
- style: CSSProperties;
85
- }
86
- /**
87
- * ResizeObserver ratchet for dynamic content.
88
- *
89
- * Watches the element, tracks maximum width/height ever observed,
90
- * applies min-width/min-height that only ratchets up.
91
- *
92
- * Five things work together:
93
- * 1. ResizeObserver uses borderBoxSize — includes padding/border
94
- * 2. Ratchet is one-way — max only increases, never resets
95
- * 3. setStyle only called when ratchet grows — no infinite loops
96
- * 4. RefCallback disconnects observer on unmount — no leak
97
- * 5. SSR graceful no-op — typeof ResizeObserver guard
98
- */
99
- declare function useStableSlot(options?: UseStableSlotOptions): UseStableSlotReturn;
100
-
101
- /**
102
- * Suppress CSS transitions during batched state changes.
103
- *
104
- * Three things work together:
105
- * 1. lock() sets the flag synchronously — batched with state changes in React 18
106
- * 2. After DOM commit (useLayoutEffect window) — consumer does measurement/scroll/pin work
107
- * 3. useEffect auto-clears the flag after paint — transitions re-enable
108
- *
109
- * Usage:
110
- * const { locked, lock } = useTransitionLock();
111
- * <div data-locked={locked || undefined}>...</div>
112
- */
113
- declare function useTransitionLock(): {
114
- readonly locked: boolean;
115
- readonly lock: () => void;
116
- };
117
-
118
76
  interface GigbagProps extends HTMLAttributes<HTMLElement> {
119
77
  /** Which axis to ratchet. Default: "height". */
120
78
  axis?: Axis;
@@ -147,6 +105,9 @@ interface WarmupProps extends HTMLAttributes<HTMLElement> {
147
105
  * dimensions of the real content. Pair with <Gigbag> so the
148
106
  * container ratchets to the larger of placeholder vs real content.
149
107
  *
108
+ * Pass children to inject structure before the generated bones
109
+ * (e.g. a toolbar placeholder that spans all grid columns).
110
+ *
150
111
  * All dimensions are CSS custom properties — consuming apps theme
151
112
  * without forking.
152
113
  */
@@ -161,14 +122,105 @@ interface GlideProps extends HTMLAttributes<HTMLElement> {
161
122
  /**
162
123
  * Enter/exit animation wrapper.
163
124
  *
164
- * State machine:
165
- * show=true -> mount + "entering" -> animationEnd -> "entered"
166
- * show=false -> "exiting" -> animationEnd -> unmount
167
- *
168
- * CSS classes: concertina-glide-entering, concertina-glide-exiting
125
+ * Thin composition over usePresence. Adds CSS class names:
126
+ * concertina-glide-entering, concertina-glide-exiting
169
127
  */
170
128
  declare const Glide: react.ForwardRefExoticComponent<GlideProps & react.RefAttributes<HTMLElement>>;
171
129
 
130
+ interface Size {
131
+ width: number;
132
+ height: number;
133
+ }
134
+ interface UseSizeReturn {
135
+ /** RefCallback — attach to the element to observe. */
136
+ ref: (el: HTMLElement | null) => void;
137
+ /** Current border-box size. Starts at { width: 0, height: 0 }. */
138
+ size: Size;
139
+ }
140
+ /**
141
+ * Raw border-box size observation via ResizeObserver.
142
+ *
143
+ * Reports every resize — no ratchet, no policy. Use this when you
144
+ * need the actual current size for your own logic (e.g. breakpoints,
145
+ * conditional rendering, animations).
146
+ *
147
+ * For a ratcheting min-size that only grows, use useStableSlot instead.
148
+ */
149
+ declare function useSize(): UseSizeReturn;
150
+
151
+ type Phase = "entering" | "entered" | "exiting";
152
+ interface UsePresenceReturn {
153
+ /** Whether the element should be in the DOM. */
154
+ mounted: boolean;
155
+ /** Current animation phase. */
156
+ phase: Phase;
157
+ /** Attach to the animating element's onAnimationEnd. */
158
+ onAnimationEnd: (e: AnimationEvent) => void;
159
+ }
160
+ /**
161
+ * Mount/unmount state machine for enter/exit animations.
162
+ *
163
+ * State transitions:
164
+ * show=true → mount + "entering" → animationEnd → "entered"
165
+ * show=false → "exiting" → animationEnd → unmount
166
+ *
167
+ * Extracted from Glide so any component can use animated presence.
168
+ */
169
+ declare function usePresence(show: boolean): UsePresenceReturn;
170
+
171
+ /**
172
+ * Pin an element to the top of its scroll container after layout changes.
173
+ *
174
+ * Runs pinToScrollTop inside useLayoutEffect — after React commits
175
+ * the DOM but before the browser paints. This ensures scroll
176
+ * correction happens synchronously with layout changes.
177
+ *
178
+ * Extracted from accordion Root so any component can do scroll pinning.
179
+ */
180
+ declare function useScrollPin(getElement: () => HTMLElement | null, deps: DependencyList): void;
181
+
182
+ interface UseStableSlotOptions {
183
+ /** Which axis to ratchet. Default: "both". */
184
+ axis?: Axis;
185
+ }
186
+ interface UseStableSlotReturn {
187
+ /** RefCallback — attach to the container element. */
188
+ ref: (el: HTMLElement | null) => void;
189
+ /** Spread onto the element: { minWidth?, minHeight? } */
190
+ style: CSSProperties;
191
+ }
192
+ /**
193
+ * ResizeObserver ratchet for dynamic content.
194
+ *
195
+ * Watches the element, tracks maximum width/height ever observed,
196
+ * applies min-width/min-height that only ratchets up.
197
+ *
198
+ * Five things work together:
199
+ * 1. ResizeObserver uses borderBoxSize — includes padding/border
200
+ * 2. Ratchet is one-way — max only increases, never resets
201
+ * 3. setStyle only called when ratchet grows — no infinite loops
202
+ * 4. RefCallback disconnects observer on unmount — no leak
203
+ * 5. SSR graceful no-op — typeof ResizeObserver guard
204
+ */
205
+ declare function useStableSlot(options?: UseStableSlotOptions): UseStableSlotReturn;
206
+
207
+ /**
208
+ * Suppress CSS transitions during batched state changes.
209
+ *
210
+ * Three things work together:
211
+ * 1. lock() sets the flag synchronously — batched with state changes in React 18
212
+ * 2. After DOM commit (useLayoutEffect window) — consumer does measurement/scroll/pin work
213
+ * 3. useEffect auto-clears the flag after paint — transitions re-enable
214
+ *
215
+ * Usage:
216
+ * const { locked, lock } = useTransitionLock();
217
+ * <div data-locked={locked || undefined}>...</div>
218
+ */
219
+ declare function useTransitionLock(): {
220
+ readonly locked: boolean;
221
+ readonly lock: () => void;
222
+ };
223
+
172
224
  /**
173
225
  * Scroll `el` to the top of its nearest scrollable ancestor,
174
226
  * clearing any sticky headers. Only adjusts one container's
@@ -211,4 +263,4 @@ interface UseConcertinaReturn {
211
263
  */
212
264
  declare function useConcertina(): UseConcertinaReturn;
213
265
 
214
- export { type Axis, ConcertinaContext, type ConcertinaRootProps, ConcertinaStore, Content, Gigbag, type GigbagProps, Glide, type GlideProps, Item, Root, Slot, type SlotProps, StableSlot, type StableSlotProps, type UseConcertinaReturn, Warmup, type WarmupProps, pinToScrollTop, useConcertina, useExpanded, useStableSlot, useTransitionLock };
266
+ export { type Axis, ConcertinaContext, type ConcertinaRootProps, ConcertinaStore, Content, Gigbag, type GigbagProps, Glide, type GlideProps, Item, type Phase, Root, type Size, Slot, type SlotProps, StableSlot, type StableSlotProps, type UseConcertinaReturn, type UsePresenceReturn, Warmup, type WarmupProps, pinToScrollTop, useConcertina, useExpanded, usePresence, useScrollPin, useSize, useStableSlot, useTransitionLock };
package/dist/index.js CHANGED
@@ -1,8 +1,7 @@
1
- // src/root.tsx
1
+ // src/accordion/root.tsx
2
2
  import {
3
3
  forwardRef as forwardRef4,
4
4
  useRef as useRef5,
5
- useLayoutEffect as useLayoutEffect3,
6
5
  useSyncExternalStore,
7
6
  useCallback as useCallback6
8
7
  } from "react";
@@ -966,7 +965,7 @@ var Header = AccordionHeader;
966
965
  var Trigger2 = AccordionTrigger;
967
966
  var Content2 = AccordionContent;
968
967
 
969
- // src/store.ts
968
+ // src/accordion/store.ts
970
969
  import { createContext as createContext3 } from "react";
971
970
  var ConcertinaStore = class {
972
971
  constructor() {
@@ -995,7 +994,10 @@ var ConcertinaStore = class {
995
994
  };
996
995
  var ConcertinaContext = createContext3(null);
997
996
 
998
- // src/pin-to-scroll-top.ts
997
+ // src/primitives/use-scroll-pin.ts
998
+ import { useLayoutEffect as useLayoutEffect3 } from "react";
999
+
1000
+ // src/primitives/pin-to-scroll-top.ts
999
1001
  function pinToScrollTop(el) {
1000
1002
  if (!el) return;
1001
1003
  let parent = el.parentElement;
@@ -1025,7 +1027,16 @@ function pinToScrollTop(el) {
1025
1027
  }
1026
1028
  }
1027
1029
 
1028
- // src/use-transition-lock.ts
1030
+ // src/primitives/use-scroll-pin.ts
1031
+ function useScrollPin(getElement, deps) {
1032
+ useLayoutEffect3(() => {
1033
+ const el = getElement();
1034
+ if (!el) return;
1035
+ pinToScrollTop(el);
1036
+ }, deps);
1037
+ }
1038
+
1039
+ // src/primitives/use-transition-lock.ts
1029
1040
  import { useState as useState5, useEffect as useEffect5, useCallback as useCallback5 } from "react";
1030
1041
  function useTransitionLock() {
1031
1042
  const [locked, setLocked] = useState5(false);
@@ -1036,7 +1047,7 @@ function useTransitionLock() {
1036
1047
  return { locked, lock };
1037
1048
  }
1038
1049
 
1039
- // src/root.tsx
1050
+ // src/accordion/root.tsx
1040
1051
  import { jsx as jsx8 } from "react/jsx-runtime";
1041
1052
  var Root3 = forwardRef4(
1042
1053
  function Root4({ collapsible = true, children, ...props }, forwardedRef) {
@@ -1059,10 +1070,10 @@ var Root3 = forwardRef4(
1059
1070
  },
1060
1071
  [store, lock]
1061
1072
  );
1062
- useLayoutEffect3(() => {
1063
- if (!value) return;
1064
- pinToScrollTop(store.getItemRef(value));
1065
- }, [value, store]);
1073
+ useScrollPin(
1074
+ () => value ? store.getItemRef(value) : null,
1075
+ [value, store]
1076
+ );
1066
1077
  return /* @__PURE__ */ jsx8(ConcertinaContext.Provider, { value: store, children: /* @__PURE__ */ jsx8(
1067
1078
  Root2,
1068
1079
  {
@@ -1079,7 +1090,7 @@ var Root3 = forwardRef4(
1079
1090
  }
1080
1091
  );
1081
1092
 
1082
- // src/item.tsx
1093
+ // src/accordion/item.tsx
1083
1094
  import {
1084
1095
  forwardRef as forwardRef5,
1085
1096
  useContext as useContext3,
@@ -1102,7 +1113,7 @@ var Item2 = forwardRef5(function Item3({ value, ...props }, forwardedRef) {
1102
1113
  return /* @__PURE__ */ jsx9(Item, { ref: mergedRef, value, ...props });
1103
1114
  });
1104
1115
 
1105
- // src/content.tsx
1116
+ // src/accordion/content.tsx
1106
1117
  import { forwardRef as forwardRef6 } from "react";
1107
1118
  import { jsx as jsx10 } from "react/jsx-runtime";
1108
1119
  var Content3 = forwardRef6(function Content4({ className, ...props }, ref) {
@@ -1110,7 +1121,7 @@ var Content3 = forwardRef6(function Content4({ className, ...props }, ref) {
1110
1121
  return /* @__PURE__ */ jsx10(Content2, { ref, className: merged, ...props });
1111
1122
  });
1112
1123
 
1113
- // src/use-expanded.ts
1124
+ // src/accordion/use-expanded.ts
1114
1125
  import { useContext as useContext4, useSyncExternalStore as useSyncExternalStore2 } from "react";
1115
1126
  function useStore() {
1116
1127
  const store = useContext4(ConcertinaContext);
@@ -1129,7 +1140,7 @@ function useExpanded(id) {
1129
1140
  );
1130
1141
  }
1131
1142
 
1132
- // src/stable-slot.tsx
1143
+ // src/components/stable-slot.tsx
1133
1144
  import {
1134
1145
  forwardRef as forwardRef7,
1135
1146
  createContext as createContext4
@@ -1143,7 +1154,7 @@ var StableSlot = forwardRef7(
1143
1154
  }
1144
1155
  );
1145
1156
 
1146
- // src/slot.tsx
1157
+ // src/components/slot.tsx
1147
1158
  import {
1148
1159
  forwardRef as forwardRef8,
1149
1160
  useContext as useContext5
@@ -1169,7 +1180,10 @@ var Slot = forwardRef8(
1169
1180
  }
1170
1181
  );
1171
1182
 
1172
- // src/use-stable-slot.ts
1183
+ // src/components/gigbag.tsx
1184
+ import { forwardRef as forwardRef9 } from "react";
1185
+
1186
+ // src/primitives/use-stable-slot.ts
1173
1187
  import { useState as useState6, useCallback as useCallback8, useRef as useRef6 } from "react";
1174
1188
  function useStableSlot(options = {}) {
1175
1189
  const { axis = "both" } = options;
@@ -1222,8 +1236,7 @@ function useStableSlot(options = {}) {
1222
1236
  return { ref, style };
1223
1237
  }
1224
1238
 
1225
- // src/gigbag.tsx
1226
- import { forwardRef as forwardRef9 } from "react";
1239
+ // src/components/gigbag.tsx
1227
1240
  import { jsx as jsx13 } from "react/jsx-runtime";
1228
1241
  var Gigbag = forwardRef9(
1229
1242
  function Gigbag2({ axis = "height", as: Tag = "div", className, style, children, ...props }, fwdRef) {
@@ -1246,7 +1259,7 @@ var Gigbag = forwardRef9(
1246
1259
  }
1247
1260
  );
1248
1261
 
1249
- // src/warmup.tsx
1262
+ // src/components/warmup.tsx
1250
1263
  import { forwardRef as forwardRef10 } from "react";
1251
1264
  import { jsx as jsx14, jsxs } from "react/jsx-runtime";
1252
1265
  var Warmup = forwardRef10(
@@ -1256,47 +1269,60 @@ var Warmup = forwardRef10(
1256
1269
  /* @__PURE__ */ jsx14("div", { className: "concertina-warmup-line concertina-warmup-line-short" }),
1257
1270
  /* @__PURE__ */ jsx14("div", { className: "concertina-warmup-line concertina-warmup-line-long" })
1258
1271
  ] }, i));
1259
- return /* @__PURE__ */ jsx14(
1272
+ return /* @__PURE__ */ jsxs(
1260
1273
  Tag,
1261
1274
  {
1262
1275
  ref,
1263
1276
  className: merged,
1264
1277
  style: { gridTemplateColumns: `repeat(${columns}, 1fr)` },
1265
1278
  ...props,
1266
- children: cells
1279
+ children: [
1280
+ children,
1281
+ cells
1282
+ ]
1267
1283
  }
1268
1284
  );
1269
1285
  }
1270
1286
  );
1271
1287
 
1272
- // src/glide.tsx
1288
+ // src/components/glide.tsx
1289
+ import {
1290
+ forwardRef as forwardRef11
1291
+ } from "react";
1292
+
1293
+ // src/primitives/use-presence.ts
1273
1294
  import {
1274
- forwardRef as forwardRef11,
1275
1295
  useState as useState7,
1276
1296
  useEffect as useEffect6,
1277
1297
  useCallback as useCallback9
1278
1298
  } from "react";
1299
+ function usePresence2(show) {
1300
+ const [mounted, setMounted] = useState7(show);
1301
+ const [phase, setPhase] = useState7(show ? "entered" : "exiting");
1302
+ useEffect6(() => {
1303
+ if (show) {
1304
+ setMounted(true);
1305
+ setPhase("entering");
1306
+ } else if (mounted) {
1307
+ setPhase("exiting");
1308
+ }
1309
+ }, [show]);
1310
+ const onAnimationEnd = useCallback9(
1311
+ (e) => {
1312
+ if (e.target !== e.currentTarget) return;
1313
+ if (phase === "entering") setPhase("entered");
1314
+ if (phase === "exiting") setMounted(false);
1315
+ },
1316
+ [phase]
1317
+ );
1318
+ return { mounted, phase, onAnimationEnd };
1319
+ }
1320
+
1321
+ // src/components/glide.tsx
1279
1322
  import { jsx as jsx15 } from "react/jsx-runtime";
1280
1323
  var Glide = forwardRef11(
1281
1324
  function Glide2({ show, as: Tag = "div", className, children, ...props }, ref) {
1282
- const [mounted, setMounted] = useState7(show);
1283
- const [phase, setPhase] = useState7(show ? "entered" : "exiting");
1284
- useEffect6(() => {
1285
- if (show) {
1286
- setMounted(true);
1287
- setPhase("entering");
1288
- } else if (mounted) {
1289
- setPhase("exiting");
1290
- }
1291
- }, [show]);
1292
- const onAnimationEnd = useCallback9(
1293
- (e) => {
1294
- if (e.target !== e.currentTarget) return;
1295
- if (phase === "entering") setPhase("entered");
1296
- if (phase === "exiting") setMounted(false);
1297
- },
1298
- [phase]
1299
- );
1325
+ const { mounted, phase, onAnimationEnd } = usePresence2(show);
1300
1326
  if (!mounted) return null;
1301
1327
  const phaseClass = phase === "entering" ? "concertina-glide-entering" : phase === "exiting" ? "concertina-glide-exiting" : "";
1302
1328
  const merged = ["concertina-glide", phaseClass, className].filter(Boolean).join(" ");
@@ -1313,19 +1339,52 @@ var Glide = forwardRef11(
1313
1339
  }
1314
1340
  );
1315
1341
 
1316
- // src/use-concertina.ts
1342
+ // src/primitives/use-size.ts
1343
+ import { useState as useState8, useCallback as useCallback10, useRef as useRef7 } from "react";
1344
+ function useSize() {
1345
+ const [size, setSize] = useState8({ width: 0, height: 0 });
1346
+ const observerRef = useRef7(null);
1347
+ const ref = useCallback10((el) => {
1348
+ if (observerRef.current) {
1349
+ observerRef.current.disconnect();
1350
+ observerRef.current = null;
1351
+ }
1352
+ if (!el || typeof ResizeObserver === "undefined") return;
1353
+ const observer = new ResizeObserver((entries) => {
1354
+ for (const entry of entries) {
1355
+ let w;
1356
+ let h;
1357
+ if (entry.borderBoxSize?.length) {
1358
+ const box = entry.borderBoxSize[0];
1359
+ w = box.inlineSize;
1360
+ h = box.blockSize;
1361
+ } else {
1362
+ const rect = entry.target.getBoundingClientRect();
1363
+ w = rect.width;
1364
+ h = rect.height;
1365
+ }
1366
+ setSize({ width: w, height: h });
1367
+ }
1368
+ });
1369
+ observer.observe(el, { box: "border-box" });
1370
+ observerRef.current = observer;
1371
+ }, []);
1372
+ return { ref, size };
1373
+ }
1374
+
1375
+ // src/accordion/use-concertina.ts
1317
1376
  import {
1318
- useState as useState8,
1319
- useCallback as useCallback10,
1320
- useRef as useRef7,
1377
+ useState as useState9,
1378
+ useCallback as useCallback11,
1379
+ useRef as useRef8,
1321
1380
  useLayoutEffect as useLayoutEffect4,
1322
1381
  useEffect as useEffect7
1323
1382
  } from "react";
1324
1383
  function useConcertina() {
1325
- const [value, setValue] = useState8("");
1326
- const [switching, setSwitching] = useState8(false);
1327
- const itemRefs = useRef7({});
1328
- const onValueChange = useCallback10(
1384
+ const [value, setValue] = useState9("");
1385
+ const [switching, setSwitching] = useState9(false);
1386
+ const itemRefs = useRef8({});
1387
+ const onValueChange = useCallback11(
1329
1388
  (newValue) => {
1330
1389
  if (!newValue) {
1331
1390
  setSwitching(false);
@@ -1344,7 +1403,7 @@ function useConcertina() {
1344
1403
  useEffect7(() => {
1345
1404
  if (switching) setSwitching(false);
1346
1405
  }, [switching]);
1347
- const getItemRef = useCallback10(
1406
+ const getItemRef = useCallback11(
1348
1407
  (id) => (el) => {
1349
1408
  itemRefs.current[id] = el;
1350
1409
  },
@@ -1373,6 +1432,9 @@ export {
1373
1432
  pinToScrollTop,
1374
1433
  useConcertina,
1375
1434
  useExpanded,
1435
+ usePresence2 as usePresence,
1436
+ useScrollPin,
1437
+ useSize,
1376
1438
  useStableSlot,
1377
1439
  useTransitionLock
1378
1440
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "concertina",
3
- "version": "0.7.1",
3
+ "version": "0.8.1",
4
4
  "description": "React toolkit for layout stability.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",