@digilogiclabs/saas-factory-ui 2.3.0 → 2.5.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/dist/index.js CHANGED
@@ -30719,7 +30719,6 @@ function DiceRoller({
30719
30719
  }) {
30720
30720
  const clampedCount = Math.max(1, Math.min(MAX_COUNT, count2));
30721
30721
  const dark = useDarkMode(darkMode);
30722
- const reducedMotion = usePrefersReducedMotion();
30723
30722
  const [rolling, setRolling] = (0, import_react68.useState)(false);
30724
30723
  const [results, setResults] = (0, import_react68.useState)(null);
30725
30724
  const [history, setHistory] = (0, import_react68.useState)([]);
@@ -30856,6 +30855,14 @@ function DiceRoller({
30856
30855
  []
30857
30856
  );
30858
30857
  (0, import_react68.useEffect)(() => {
30858
+ if (timersRef.current.length > 0) {
30859
+ timersRef.current.forEach((t) => clearTimeout(t));
30860
+ timersRef.current = [];
30861
+ }
30862
+ if (rollingRef.current) {
30863
+ rollingRef.current = false;
30864
+ setRolling(false);
30865
+ }
30859
30866
  const rawW = arenaRef.current?.offsetWidth ?? 0;
30860
30867
  const rawH = arenaRef.current?.offsetHeight ?? 0;
30861
30868
  const w = rawW > 20 ? rawW : arena.w > 20 ? arena.w : 400;
@@ -31012,7 +31019,6 @@ function DiceRoller({
31012
31019
  defaultPositions,
31013
31020
  effectiveSize,
31014
31021
  half,
31015
- reducedMotion,
31016
31022
  onRoll,
31017
31023
  writeCubeRotation,
31018
31024
  writeDiePosition,
@@ -33025,6 +33031,9 @@ function CoinFlipper({
33025
33031
  }, [flipping, onFlip, historyMax]);
33026
33032
  (0, import_react70.useEffect)(() => {
33027
33033
  if (flipRef) flipRef.current = flip;
33034
+ return () => {
33035
+ if (flipRef && flipRef.current === flip) flipRef.current = null;
33036
+ };
33028
33037
  }, [flip, flipRef]);
33029
33038
  (0, import_react70.useEffect)(() => {
33030
33039
  if (autoFlip) {
@@ -33526,6 +33535,11 @@ var WEDGE_PALETTE = [
33526
33535
  ];
33527
33536
  var DEFAULT_EASING = "cubic-bezier(0.25, 0.46, 0.45, 0.94)";
33528
33537
  var DRAMATIC_EASING = "cubic-bezier(0.39, 0.575, 0.565, 1)";
33538
+ var PHYSICS_DECAY_K = 4;
33539
+ function physicsRampDown(t) {
33540
+ return (1 - Math.exp(-PHYSICS_DECAY_K * t)) / (1 - Math.exp(-PHYSICS_DECAY_K));
33541
+ }
33542
+ var REEL_LEAD_IN = 32;
33529
33543
  var INTENSITY_CONFIG = {
33530
33544
  subtle: {
33531
33545
  particles: 14,
@@ -33597,6 +33611,9 @@ function ReelStage({
33597
33611
  winner,
33598
33612
  spinning,
33599
33613
  reelY,
33614
+ displayY,
33615
+ cssTransition,
33616
+ tapeRef,
33600
33617
  reelVisibleRows,
33601
33618
  reelRowHeight,
33602
33619
  size,
@@ -33620,17 +33637,16 @@ function ReelStage({
33620
33637
  const stripHeight = reelVisibleRows * reelRowHeight;
33621
33638
  const centerOffsetPx = (reelVisibleRows - 1) / 2 * reelRowHeight;
33622
33639
  const P = pool.length;
33623
- const leadIn = 32;
33624
33640
  const tapeRows = Math.max(
33625
- reelVisibleRows + leadIn + 1,
33626
- Math.ceil((reelY + stripHeight) / reelRowHeight) + leadIn + 1
33641
+ reelVisibleRows + REEL_LEAD_IN + 1,
33642
+ Math.ceil((reelY + stripHeight) / reelRowHeight) + REEL_LEAD_IN + 1
33627
33643
  );
33628
33644
  const tape = Array.from({ length: tapeRows }, (_, i) => {
33629
- const poolIdx = P === 0 ? 0 : ((i - leadIn) % P + P) % P;
33645
+ const poolIdx = P === 0 ? 0 : ((i - REEL_LEAD_IN) % P + P) % P;
33630
33646
  return pool[poolIdx];
33631
33647
  });
33632
- const effectiveY = reelY + leadIn * reelRowHeight;
33633
- const transition = spinning ? `transform ${reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult)}ms ${intensityCfg.easing}` : "none";
33648
+ const effectiveY = displayY + REEL_LEAD_IN * reelRowHeight;
33649
+ const transition = spinning && cssTransition ? `transform ${reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult)}ms ${intensityCfg.easing}` : "none";
33634
33650
  return /* @__PURE__ */ (0, import_jsx_runtime117.jsxs)(
33635
33651
  "div",
33636
33652
  {
@@ -33675,6 +33691,7 @@ function ReelStage({
33675
33691
  ) : /* @__PURE__ */ (0, import_jsx_runtime117.jsx)(
33676
33692
  "div",
33677
33693
  {
33694
+ ref: tapeRef,
33678
33695
  "aria-hidden": "true",
33679
33696
  style: {
33680
33697
  position: "absolute",
@@ -33890,6 +33907,9 @@ function RandomPlayerPicker({
33890
33907
  showStats = true,
33891
33908
  renderWinner,
33892
33909
  spinRef,
33910
+ resetRef,
33911
+ onPoolChange,
33912
+ draftCompleteCaption = "Draft complete \u2014 reset to spin again",
33893
33913
  intensity = "normal",
33894
33914
  enableHaptics = false,
33895
33915
  tickSoundUrl,
@@ -33904,6 +33924,7 @@ function RandomPlayerPicker({
33904
33924
  const dark = useDarkMode(darkMode);
33905
33925
  const reducedMotion = usePrefersReducedMotion();
33906
33926
  const intensityCfg = INTENSITY_CONFIG[intensity];
33927
+ const physicsDriven = intensity === "dramatic" && !reducedMotion;
33907
33928
  const [pool, setPool] = (0, import_react71.useState)(participants);
33908
33929
  const [rotation, setRotation] = (0, import_react71.useState)(0);
33909
33930
  const [reelY, setReelY] = (0, import_react71.useState)(0);
@@ -33916,10 +33937,17 @@ function RandomPlayerPicker({
33916
33937
  const particleTimer = (0, import_react71.useRef)(null);
33917
33938
  const popTimer = (0, import_react71.useRef)(null);
33918
33939
  const tickInterval = (0, import_react71.useRef)(null);
33940
+ const pendingPopIdxRef = (0, import_react71.useRef)(null);
33941
+ const removeWinnersRef = (0, import_react71.useRef)(removeWinners);
33919
33942
  const tickAudioRef = (0, import_react71.useRef)(null);
33920
33943
  const winAudioRef = (0, import_react71.useRef)(null);
33921
33944
  const containerRef = (0, import_react71.useRef)(null);
33922
33945
  const prevIdsRef = (0, import_react71.useRef)("");
33946
+ const wheelSvgRef = (0, import_react71.useRef)(null);
33947
+ const reelTapeRef = (0, import_react71.useRef)(null);
33948
+ const rafRef = (0, import_react71.useRef)(null);
33949
+ const visualRotationRef = (0, import_react71.useRef)(null);
33950
+ const visualReelYRef = (0, import_react71.useRef)(null);
33923
33951
  (0, import_react71.useEffect)(() => {
33924
33952
  if (typeof window === "undefined" || !tickSoundUrl) {
33925
33953
  tickAudioRef.current = null;
@@ -33954,10 +33982,33 @@ function RandomPlayerPicker({
33954
33982
  winAudioRef.current = null;
33955
33983
  };
33956
33984
  }, [winSoundUrl, soundVolume]);
33985
+ (0, import_react71.useEffect)(() => {
33986
+ removeWinnersRef.current = removeWinners;
33987
+ }, [removeWinners]);
33957
33988
  (0, import_react71.useEffect)(() => {
33958
33989
  const ids = participants.map((p) => p.id).join("|");
33959
33990
  const structuralChange = ids !== prevIdsRef.current;
33960
33991
  if (structuralChange) {
33992
+ if (winnerTimer.current) {
33993
+ clearTimeout(winnerTimer.current);
33994
+ winnerTimer.current = null;
33995
+ }
33996
+ if (tickInterval.current) {
33997
+ clearTimeout(tickInterval.current);
33998
+ tickInterval.current = null;
33999
+ }
34000
+ if (popTimer.current) {
34001
+ clearTimeout(popTimer.current);
34002
+ popTimer.current = null;
34003
+ }
34004
+ if (rafRef.current) {
34005
+ cancelAnimationFrame(rafRef.current);
34006
+ rafRef.current = null;
34007
+ }
34008
+ visualRotationRef.current = null;
34009
+ visualReelYRef.current = null;
34010
+ pendingPopIdxRef.current = null;
34011
+ setSpinning(false);
33961
34012
  setPool(participants);
33962
34013
  setHistory([]);
33963
34014
  setWinner(null);
@@ -34014,33 +34065,50 @@ function RandomPlayerPicker({
34014
34065
  [dark]
34015
34066
  );
34016
34067
  const spin = (0, import_react71.useCallback)(() => {
34017
- if (spinning || n < 2) return;
34068
+ if (spinning) return;
34069
+ let activePool = pool;
34070
+ if (popTimer.current) {
34071
+ clearTimeout(popTimer.current);
34072
+ popTimer.current = null;
34073
+ const pendingIdx = pendingPopIdxRef.current;
34074
+ pendingPopIdxRef.current = null;
34075
+ if (pendingIdx != null && pendingIdx >= 0 && pendingIdx < pool.length && removeWinnersRef.current) {
34076
+ activePool = pool.filter((_, i) => i !== pendingIdx);
34077
+ setPool(activePool);
34078
+ setWinner(null);
34079
+ }
34080
+ }
34081
+ const activeN = activePool.length;
34082
+ if (activeN < 2) return;
34083
+ const activeAnglePerWedge = 2 * Math.PI / activeN;
34084
+ const activeRotation = rotation;
34085
+ const activeReelY = reelY;
34018
34086
  onSpinStart?.();
34019
- const winnerIdx = weightedIndex(pool);
34087
+ const winnerIdx = weightedIndex(activePool);
34020
34088
  const minRev = minRevolutions + intensityCfg.revsBoost;
34021
34089
  const maxRev = maxRevolutions + intensityCfg.revsBoost;
34022
34090
  const minFullRevs = Math.ceil(minRev);
34023
34091
  const maxFullRevs = Math.max(minFullRevs, Math.floor(maxRev));
34024
34092
  const revolutions = minFullRevs + Math.floor(Math.random() * (maxFullRevs - minFullRevs + 1));
34025
- const wedgeMidRad = winnerIdx * anglePerWedge + anglePerWedge / 2;
34093
+ const wedgeMidRad = winnerIdx * activeAnglePerWedge + activeAnglePerWedge / 2;
34026
34094
  const wedgeMidDeg = wedgeMidRad * 180 / Math.PI;
34027
- const wheelWobble = (Math.random() - 0.5) * (anglePerWedge * 180 / Math.PI) * 0.6;
34028
- const currentMod = (rotation % 360 + 360) % 360;
34095
+ const wheelWobble = (Math.random() - 0.5) * (activeAnglePerWedge * 180 / Math.PI) * 0.6;
34096
+ const currentMod = (activeRotation % 360 + 360) % 360;
34029
34097
  const targetMod = ((-wedgeMidDeg + wheelWobble) % 360 + 360) % 360;
34030
34098
  let deltaMod = targetMod - currentMod;
34031
34099
  if (deltaMod < 0) deltaMod += 360;
34032
- const finalRotation = rotation + deltaMod + revolutions * 360;
34100
+ const finalRotation = activeRotation + deltaMod + revolutions * 360;
34033
34101
  const centerOffsetRows = (reelVisibleRows - 1) / 2;
34034
34102
  const centerOffsetPx = centerOffsetRows * reelRowHeight;
34035
34103
  const currentCenterRow = Math.round(
34036
- (reelY + centerOffsetPx) / reelRowHeight
34104
+ (activeReelY + centerOffsetPx) / reelRowHeight
34037
34105
  );
34038
- const currentCenterMod = mod(currentCenterRow, n);
34039
- const targetRowOffset = (winnerIdx - currentCenterMod + n) % n;
34040
- const reelRevRows = Math.round(revolutions * n);
34041
- const finalReelY = reelY + (reelRevRows + targetRowOffset) * reelRowHeight;
34042
- const settledWinnerIdx = mode === "reel" ? reelIndexAtCenter(finalReelY, centerOffsetPx, reelRowHeight, n) : wheelIndexAtPointer(finalRotation, n);
34043
- const finalWinnerIdx = settledWinnerIdx >= 0 && settledWinnerIdx < pool.length ? settledWinnerIdx : winnerIdx;
34106
+ const currentCenterMod = mod(currentCenterRow, activeN);
34107
+ const targetRowOffset = (winnerIdx - currentCenterMod + activeN) % activeN;
34108
+ const reelRevRows = Math.round(revolutions * activeN);
34109
+ const finalReelY = activeReelY + (reelRevRows + targetRowOffset) * reelRowHeight;
34110
+ const settledWinnerIdx = mode === "reel" ? reelIndexAtCenter(finalReelY, centerOffsetPx, reelRowHeight, activeN) : wheelIndexAtPointer(finalRotation, activeN);
34111
+ const finalWinnerIdx = settledWinnerIdx >= 0 && settledWinnerIdx < activePool.length ? settledWinnerIdx : winnerIdx;
34044
34112
  const effectiveDuration = reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult);
34045
34113
  setSpinning(true);
34046
34114
  setWinner(null);
@@ -34050,10 +34118,59 @@ function RandomPlayerPicker({
34050
34118
  } else {
34051
34119
  setRotation(finalRotation);
34052
34120
  }
34053
- if ((onTick || tickAudioRef.current) && !reducedMotion) {
34121
+ const fireTick = () => {
34122
+ onTick?.();
34123
+ const ta = tickAudioRef.current;
34124
+ if (ta) {
34125
+ try {
34126
+ ta.currentTime = 0;
34127
+ void ta.play().catch(() => {
34128
+ });
34129
+ } catch {
34130
+ }
34131
+ }
34132
+ };
34133
+ if (physicsDriven) {
34134
+ const from = mode === "reel" ? activeReelY : activeRotation;
34135
+ const to = mode === "reel" ? finalReelY : finalRotation;
34136
+ const crossUnit = mode === "reel" ? reelRowHeight : activeAnglePerWedge * 180 / Math.PI;
34137
+ if (mode === "reel") visualReelYRef.current = from;
34138
+ else visualRotationRef.current = from;
34139
+ let crossings = 0;
34140
+ const t0 = performance.now();
34141
+ const step = (now) => {
34142
+ const t = Math.min(1, (now - t0) / effectiveDuration);
34143
+ const value = from + (to - from) * physicsRampDown(t);
34144
+ if (mode === "reel") {
34145
+ visualReelYRef.current = value;
34146
+ const el = reelTapeRef.current;
34147
+ if (el) {
34148
+ el.style.transform = `translateY(${-(value + REEL_LEAD_IN * reelRowHeight)}px)`;
34149
+ }
34150
+ } else {
34151
+ visualRotationRef.current = value;
34152
+ const el = wheelSvgRef.current;
34153
+ if (el) el.style.transform = `rotate(${value}deg)`;
34154
+ }
34155
+ const crossed = Math.floor(Math.abs(value - from) / crossUnit);
34156
+ if (crossed > crossings) {
34157
+ crossings = crossed;
34158
+ fireTick();
34159
+ }
34160
+ if (t < 1) {
34161
+ rafRef.current = requestAnimationFrame(step);
34162
+ } else {
34163
+ rafRef.current = null;
34164
+ visualRotationRef.current = null;
34165
+ visualReelYRef.current = null;
34166
+ }
34167
+ };
34168
+ rafRef.current = requestAnimationFrame(step);
34169
+ }
34170
+ if (!physicsDriven && (onTick || tickAudioRef.current) && !reducedMotion) {
34054
34171
  const wedges = mode === "reel" ? Math.max(1, reelRevRows + targetRowOffset) : (() => {
34055
34172
  const totalDelta = deltaMod + revolutions * 360;
34056
- const wedgeDeg = anglePerWedge * 180 / Math.PI;
34173
+ const wedgeDeg = activeAnglePerWedge * 180 / Math.PI;
34057
34174
  return Math.max(
34058
34175
  1,
34059
34176
  Math.round(totalDelta / Math.max(wedgeDeg, 1))
@@ -34068,16 +34185,7 @@ function RandomPlayerPicker({
34068
34185
  const eased = 1 - Math.pow(1 - progress, 3);
34069
34186
  const interval = startInterval + (endInterval - startInterval) * eased;
34070
34187
  tickInterval.current = setTimeout(() => {
34071
- onTick?.();
34072
- const ta = tickAudioRef.current;
34073
- if (ta) {
34074
- try {
34075
- ta.currentTime = 0;
34076
- void ta.play().catch(() => {
34077
- });
34078
- } catch {
34079
- }
34080
- }
34188
+ fireTick();
34081
34189
  elapsed += interval;
34082
34190
  schedule();
34083
34191
  }, interval);
@@ -34085,7 +34193,7 @@ function RandomPlayerPicker({
34085
34193
  schedule();
34086
34194
  }
34087
34195
  winnerTimer.current = setTimeout(() => {
34088
- const picked = pool[finalWinnerIdx];
34196
+ const picked = activePool[finalWinnerIdx];
34089
34197
  if (!picked) {
34090
34198
  setSpinning(false);
34091
34199
  return;
@@ -34131,9 +34239,13 @@ function RandomPlayerPicker({
34131
34239
  particleTimer.current = setTimeout(() => setParticles([]), 2400);
34132
34240
  }
34133
34241
  if (removeWinners) {
34242
+ pendingPopIdxRef.current = finalWinnerIdx;
34134
34243
  popTimer.current = setTimeout(() => {
34244
+ popTimer.current = null;
34245
+ pendingPopIdxRef.current = null;
34246
+ if (!removeWinnersRef.current) return;
34135
34247
  setPool((p) => p.filter((_, i) => i !== finalWinnerIdx));
34136
- setWinner(null);
34248
+ if (activePool.length - 1 >= 2) setWinner(null);
34137
34249
  setRotation(0);
34138
34250
  setReelY(0);
34139
34251
  }, 2800);
@@ -34142,8 +34254,6 @@ function RandomPlayerPicker({
34142
34254
  }, [
34143
34255
  pool,
34144
34256
  spinning,
34145
- n,
34146
- anglePerWedge,
34147
34257
  rotation,
34148
34258
  reelY,
34149
34259
  mode,
@@ -34158,6 +34268,7 @@ function RandomPlayerPicker({
34158
34268
  maxRevolutions,
34159
34269
  history,
34160
34270
  reducedMotion,
34271
+ physicsDriven,
34161
34272
  enableHaptics,
34162
34273
  intensityCfg.particles,
34163
34274
  intensityCfg.durationMult,
@@ -34169,16 +34280,25 @@ function RandomPlayerPicker({
34169
34280
  if (spinRef && spinRef.current === spin) spinRef.current = null;
34170
34281
  };
34171
34282
  }, [spin, spinRef]);
34283
+ const onPoolChangeRef = (0, import_react71.useRef)(onPoolChange);
34284
+ (0, import_react71.useEffect)(() => {
34285
+ onPoolChangeRef.current = onPoolChange;
34286
+ });
34287
+ (0, import_react71.useEffect)(() => {
34288
+ onPoolChangeRef.current?.(pool);
34289
+ }, [pool]);
34172
34290
  (0, import_react71.useEffect)(
34173
34291
  () => () => {
34174
34292
  if (winnerTimer.current) clearTimeout(winnerTimer.current);
34175
34293
  if (particleTimer.current) clearTimeout(particleTimer.current);
34176
34294
  if (popTimer.current) clearTimeout(popTimer.current);
34177
34295
  if (tickInterval.current) clearTimeout(tickInterval.current);
34296
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
34178
34297
  },
34179
34298
  []
34180
34299
  );
34181
34300
  (0, import_react71.useEffect)(() => {
34301
+ if (trigger === "external") return;
34182
34302
  const el = containerRef.current;
34183
34303
  if (!el) return;
34184
34304
  const h = (e) => {
@@ -34189,8 +34309,28 @@ function RandomPlayerPicker({
34189
34309
  };
34190
34310
  el.addEventListener("keydown", h);
34191
34311
  return () => el.removeEventListener("keydown", h);
34192
- }, [spin]);
34312
+ }, [spin, trigger]);
34193
34313
  const reset = (0, import_react71.useCallback)(() => {
34314
+ if (winnerTimer.current) {
34315
+ clearTimeout(winnerTimer.current);
34316
+ winnerTimer.current = null;
34317
+ }
34318
+ if (tickInterval.current) {
34319
+ clearTimeout(tickInterval.current);
34320
+ tickInterval.current = null;
34321
+ }
34322
+ if (popTimer.current) {
34323
+ clearTimeout(popTimer.current);
34324
+ popTimer.current = null;
34325
+ }
34326
+ if (rafRef.current) {
34327
+ cancelAnimationFrame(rafRef.current);
34328
+ rafRef.current = null;
34329
+ }
34330
+ visualRotationRef.current = null;
34331
+ visualReelYRef.current = null;
34332
+ pendingPopIdxRef.current = null;
34333
+ setSpinning(false);
34194
34334
  setPool(participants);
34195
34335
  setHistory([]);
34196
34336
  setWinner(null);
@@ -34198,9 +34338,17 @@ function RandomPlayerPicker({
34198
34338
  setReelY(0);
34199
34339
  setParticles([]);
34200
34340
  }, [participants]);
34341
+ (0, import_react71.useEffect)(() => {
34342
+ if (resetRef) resetRef.current = reset;
34343
+ return () => {
34344
+ if (resetRef && resetRef.current === reset) resetRef.current = null;
34345
+ };
34346
+ }, [reset, resetRef]);
34201
34347
  const isIdle = n < 2;
34348
+ const drainedByDraft = isIdle && participants.length >= 2;
34349
+ const effectiveIdleCaption = drainedByDraft ? draftCompleteCaption : placeholderCaption;
34202
34350
  const placeholderDiscColor = dark ? "#2a3344" : "#e2e8f0";
34203
- if (isIdle && placeholderMode === "message") {
34351
+ if (isIdle && !drainedByDraft && placeholderMode === "message") {
34204
34352
  return /* @__PURE__ */ (0, import_jsx_runtime117.jsx)(
34205
34353
  "div",
34206
34354
  {
@@ -34222,6 +34370,8 @@ function RandomPlayerPicker({
34222
34370
  const shakeKeyframe = `rppShake_${intensity}`;
34223
34371
  const glowKeyframe = `rppGlow_${intensity}`;
34224
34372
  const pulseKeyframe = `rppPulse_${intensity}`;
34373
+ const displayRotation = visualRotationRef.current ?? rotation;
34374
+ const displayReelY = visualReelYRef.current ?? reelY;
34225
34375
  return /* @__PURE__ */ (0, import_jsx_runtime117.jsxs)(
34226
34376
  "div",
34227
34377
  {
@@ -34268,13 +34418,33 @@ function RandomPlayerPicker({
34268
34418
  [data-rpp-anim] { animation: none !important; }
34269
34419
  }
34270
34420
  ` }),
34271
- mode === "reel" ? /* @__PURE__ */ (0, import_jsx_runtime117.jsx)(
34421
+ isIdle && placeholderMode === "message" ? (
34422
+ /* Drained draft in "message" mode — compact box in the wheel's
34423
+ place; the winner card / stats / controls / history below stay
34424
+ mounted so the endgame isn't a dead end. */
34425
+ /* @__PURE__ */ (0, import_jsx_runtime117.jsx)(
34426
+ "div",
34427
+ {
34428
+ style: {
34429
+ padding: "2rem 1rem",
34430
+ textAlign: "center",
34431
+ color: c.dim,
34432
+ borderRadius: 12,
34433
+ border: `1px dashed ${c.border}`
34434
+ },
34435
+ children: effectiveIdleCaption
34436
+ }
34437
+ )
34438
+ ) : mode === "reel" ? /* @__PURE__ */ (0, import_jsx_runtime117.jsx)(
34272
34439
  ReelStage,
34273
34440
  {
34274
34441
  pool,
34275
34442
  winner,
34276
34443
  spinning,
34277
34444
  reelY,
34445
+ displayY: displayReelY,
34446
+ cssTransition: !physicsDriven,
34447
+ tapeRef: reelTapeRef,
34278
34448
  reelVisibleRows,
34279
34449
  reelRowHeight,
34280
34450
  size,
@@ -34283,7 +34453,7 @@ function RandomPlayerPicker({
34283
34453
  dark,
34284
34454
  isIdle,
34285
34455
  placeholderMode,
34286
- placeholderCaption,
34456
+ placeholderCaption: effectiveIdleCaption,
34287
34457
  placeholderDiscColor,
34288
34458
  spinDuration,
34289
34459
  intensityCfg,
@@ -34340,6 +34510,7 @@ function RandomPlayerPicker({
34340
34510
  /* @__PURE__ */ (0, import_jsx_runtime117.jsxs)(
34341
34511
  "svg",
34342
34512
  {
34513
+ ref: wheelSvgRef,
34343
34514
  width: size,
34344
34515
  height: size,
34345
34516
  "aria-hidden": "true",
@@ -34348,8 +34519,8 @@ function RandomPlayerPicker({
34348
34519
  top: 36,
34349
34520
  left: 0,
34350
34521
  transformOrigin: `${cx}px ${cy}px`,
34351
- transform: `rotate(${rotation}deg)`,
34352
- transition: spinning ? `transform ${reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult)}ms ${intensityCfg.easing}` : "none",
34522
+ transform: `rotate(${displayRotation}deg)`,
34523
+ transition: spinning && !physicsDriven ? `transform ${reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult)}ms ${intensityCfg.easing}` : "none",
34353
34524
  filter: `drop-shadow(0 8px 28px ${dark ? "rgba(0,0,0,0.55)" : "rgba(0,0,0,0.15)"})`
34354
34525
  },
34355
34526
  children: [
@@ -34502,7 +34673,7 @@ function RandomPlayerPicker({
34502
34673
  pointerEvents: "none",
34503
34674
  zIndex: 3
34504
34675
  },
34505
- children: placeholderCaption
34676
+ children: effectiveIdleCaption
34506
34677
  }
34507
34678
  ),
34508
34679
  particles.map((p) => /* @__PURE__ */ (0, import_jsx_runtime117.jsx)(
@@ -38643,27 +38814,28 @@ function GameTimer({
38643
38814
  const now = performance.now();
38644
38815
  const delta = now - lastTickRef.current;
38645
38816
  lastTickRef.current = now;
38646
- setElapsedMs((prev) => {
38647
- const next = prev + delta;
38648
- if (mode === "countdown" && next >= durationMs) {
38649
- setRunning(false);
38650
- setCompleted(true);
38651
- onComplete?.();
38652
- return durationMs;
38817
+ const next = elapsedRef.current + delta;
38818
+ if (mode === "countdown" && next >= durationMs) {
38819
+ elapsedRef.current = durationMs;
38820
+ setElapsedMs(durationMs);
38821
+ setRunning(false);
38822
+ setCompleted(true);
38823
+ onComplete?.();
38824
+ return;
38825
+ }
38826
+ if (mode === "countdown") {
38827
+ const secLeft = Math.ceil((durationMs - next) / 1e3);
38828
+ if (secLeft <= warningAt && !warningFiredRef.current) {
38829
+ warningFiredRef.current = true;
38830
+ onWarning?.();
38653
38831
  }
38654
- if (mode === "countdown") {
38655
- const secLeft = Math.ceil((durationMs - next) / 1e3);
38656
- if (secLeft <= warningAt && !warningFiredRef.current) {
38657
- warningFiredRef.current = true;
38658
- onWarning?.();
38659
- }
38660
- if (secLeft <= dangerAt && !dangerFiredRef.current) {
38661
- dangerFiredRef.current = true;
38662
- onDanger?.();
38663
- }
38832
+ if (secLeft <= dangerAt && !dangerFiredRef.current) {
38833
+ dangerFiredRef.current = true;
38834
+ onDanger?.();
38664
38835
  }
38665
- return next;
38666
- });
38836
+ }
38837
+ elapsedRef.current = next;
38838
+ setElapsedMs(next);
38667
38839
  rafRef.current = requestAnimationFrame(tick);
38668
38840
  }, [mode, durationMs, warningAt, dangerAt, onComplete, onWarning, onDanger]);
38669
38841
  (0, import_react74.useEffect)(() => {
@@ -38684,6 +38856,7 @@ function GameTimer({
38684
38856
  }, []);
38685
38857
  const reset = (0, import_react74.useCallback)(() => {
38686
38858
  setRunning(false);
38859
+ elapsedRef.current = 0;
38687
38860
  setElapsedMs(0);
38688
38861
  setCompleted(false);
38689
38862
  setLaps([]);
@@ -40185,8 +40358,10 @@ function TeamGenerator({
40185
40358
  minPlayersPerTeam = 2,
40186
40359
  maxPlayers = Number.POSITIVE_INFINITY,
40187
40360
  defaultNames = [],
40361
+ defaultTeams,
40188
40362
  onNamesChange,
40189
40363
  onGenerate,
40364
+ onTeamsChange,
40190
40365
  onTeamCountChange,
40191
40366
  labels,
40192
40367
  ariaLabel = "Team generator",
@@ -40201,12 +40376,17 @@ function TeamGenerator({
40201
40376
  const [names, setNames] = (0, import_react76.useState)(defaultNames);
40202
40377
  const [input, setInput] = (0, import_react76.useState)("");
40203
40378
  const [teamCount, setTeamCount] = (0, import_react76.useState)(initialCount);
40204
- const [teams, setTeams] = (0, import_react76.useState)(null);
40379
+ const [teams, setTeams] = (0, import_react76.useState)(
40380
+ defaultTeams && defaultTeams.length > 0 ? defaultTeams : null
40381
+ );
40205
40382
  const inputRef = (0, import_react76.useRef)(null);
40206
40383
  const minNames = teamCount * minPlayersPerTeam;
40207
40384
  (0, import_react76.useEffect)(() => {
40208
40385
  onNamesChange?.(names);
40209
40386
  }, [names, onNamesChange]);
40387
+ (0, import_react76.useEffect)(() => {
40388
+ onTeamsChange?.(teams);
40389
+ }, [teams, onTeamsChange]);
40210
40390
  const addName = (0, import_react76.useCallback)(() => {
40211
40391
  const trimmed = input.trim();
40212
40392
  if (!trimmed) return;
@@ -40828,6 +41008,8 @@ function RoundRobinScheduler({
40828
41008
  theme: theme5 = "slate",
40829
41009
  darkMode,
40830
41010
  minTeams = 3,
41011
+ maxTeams,
41012
+ nameMaxLength,
40831
41013
  defaultTeams = [],
40832
41014
  resumeRounds,
40833
41015
  onTeamsChange,
@@ -40850,39 +41032,41 @@ function RoundRobinScheduler({
40850
41032
  (0, import_react77.useEffect)(() => {
40851
41033
  onTeamsChange?.(teams);
40852
41034
  }, [teams, onTeamsChange]);
40853
- const addTeams = (0, import_react77.useCallback)((raw) => {
40854
- const names = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
40855
- if (names.length === 0) return;
40856
- setTeams((prev) => {
40857
- const existing = new Set(prev.map((t) => t.toLowerCase()));
40858
- const next = [...prev];
40859
- for (const name of names) {
40860
- if (!existing.has(name.toLowerCase())) {
40861
- existing.add(name.toLowerCase());
40862
- next.push(name);
40863
- }
40864
- }
40865
- return next;
40866
- });
40867
- setInput("");
40868
- setRounds([]);
40869
- }, []);
40870
- const importTeams = (0, import_react77.useCallback)((incoming) => {
40871
- if (incoming.length === 0) return;
40872
- setTeams((prev) => {
41035
+ const mergeNames = (0, import_react77.useCallback)(
41036
+ (prev, incoming) => {
40873
41037
  const existing = new Set(prev.map((t) => t.toLowerCase()));
40874
41038
  const next = [...prev];
40875
- for (const name of incoming) {
41039
+ for (const rawName of incoming) {
41040
+ if (maxTeams != null && next.length >= maxTeams) break;
41041
+ const name = nameMaxLength != null ? rawName.slice(0, nameMaxLength) : rawName;
40876
41042
  const key = name.toLowerCase();
40877
- if (!existing.has(key)) {
41043
+ if (name.length > 0 && !existing.has(key)) {
40878
41044
  existing.add(key);
40879
41045
  next.push(name);
40880
41046
  }
40881
41047
  }
40882
41048
  return next;
40883
- });
40884
- setRounds([]);
40885
- }, []);
41049
+ },
41050
+ [maxTeams, nameMaxLength]
41051
+ );
41052
+ const addTeams = (0, import_react77.useCallback)(
41053
+ (raw) => {
41054
+ const names = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
41055
+ if (names.length === 0) return;
41056
+ setTeams((prev) => mergeNames(prev, names));
41057
+ setInput("");
41058
+ setRounds([]);
41059
+ },
41060
+ [mergeNames]
41061
+ );
41062
+ const importTeams = (0, import_react77.useCallback)(
41063
+ (incoming) => {
41064
+ if (incoming.length === 0) return;
41065
+ setTeams((prev) => mergeNames(prev, incoming));
41066
+ setRounds([]);
41067
+ },
41068
+ [mergeNames]
41069
+ );
40886
41070
  const handleKeyDown = (0, import_react77.useCallback)(
40887
41071
  (e) => {
40888
41072
  if (e.key === "Enter") {
@@ -41371,6 +41555,8 @@ function Scoreboard({
41371
41555
  onTeamsChange,
41372
41556
  onRoundAdvance,
41373
41557
  onNewGame,
41558
+ onReset,
41559
+ nameMaxLength,
41374
41560
  labels,
41375
41561
  ariaLabel = "Scoreboard",
41376
41562
  className
@@ -41445,7 +41631,8 @@ function Scoreboard({
41445
41631
  setRound(1);
41446
41632
  setHistory([]);
41447
41633
  setHistoryOpen(false);
41448
- }, []);
41634
+ onReset?.();
41635
+ }, [onReset]);
41449
41636
  const newGame = (0, import_react78.useCallback)(() => {
41450
41637
  setTeams(createDefaultTeams(seedCount, L.teamNamePrefix));
41451
41638
  setRound(1);
@@ -41667,6 +41854,7 @@ function Scoreboard({
41667
41854
  {
41668
41855
  type: "text",
41669
41856
  value: team.name,
41857
+ maxLength: nameMaxLength,
41670
41858
  onChange: (e) => updateName(index, e.target.value),
41671
41859
  style: nameInputStyle,
41672
41860
  "aria-label": `Team ${index + 1} name`