@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.
@@ -30226,10 +30226,10 @@ function TiltCard({
30226
30226
  // src/components/ui/dice-roller/index.tsx
30227
30227
  import {
30228
30228
  useCallback as useCallback41,
30229
- useEffect as useEffect52,
30229
+ useEffect as useEffect51,
30230
30230
  useMemo as useMemo26,
30231
30231
  useRef as useRef40,
30232
- useState as useState67
30232
+ useState as useState66
30233
30233
  } from "react";
30234
30234
 
30235
30235
  // src/hooks/web/use-dark-mode.ts
@@ -30279,21 +30279,6 @@ function useDarkMode(forced) {
30279
30279
  return dark;
30280
30280
  }
30281
30281
 
30282
- // src/hooks/web/use-prefers-reduced-motion.ts
30283
- import { useEffect as useEffect51, useState as useState66 } from "react";
30284
- function usePrefersReducedMotion() {
30285
- const [reduced, setReduced] = useState66(false);
30286
- useEffect51(() => {
30287
- if (typeof window === "undefined" || !window.matchMedia) return;
30288
- const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
30289
- setReduced(mq.matches);
30290
- const read = () => setReduced(mq.matches);
30291
- mq.addEventListener("change", read);
30292
- return () => mq.removeEventListener("change", read);
30293
- }, []);
30294
- return reduced;
30295
- }
30296
-
30297
30282
  // src/components/ui/dice-roller/index.tsx
30298
30283
  import { Fragment as Fragment30, jsx as jsx113, jsxs as jsxs86 } from "react/jsx-runtime";
30299
30284
  var PRESETS = {
@@ -30512,8 +30497,8 @@ function simulateDice(arenaW, arenaH, diceSize, starts, numDice) {
30512
30497
  return frames;
30513
30498
  }
30514
30499
  function useArenaBounds(ref) {
30515
- const [bounds, setBounds] = useState67({ w: 400, h: 260 });
30516
- useEffect52(() => {
30500
+ const [bounds, setBounds] = useState66({ w: 400, h: 260 });
30501
+ useEffect51(() => {
30517
30502
  if (!ref.current) return;
30518
30503
  const el = ref.current;
30519
30504
  const update = () => setBounds({ w: el.offsetWidth, h: el.offsetHeight });
@@ -30568,10 +30553,9 @@ function DiceRoller({
30568
30553
  }) {
30569
30554
  const clampedCount = Math.max(1, Math.min(MAX_COUNT, count2));
30570
30555
  const dark = useDarkMode(darkMode);
30571
- const reducedMotion = usePrefersReducedMotion();
30572
- const [rolling, setRolling] = useState67(false);
30573
- const [results, setResults] = useState67(null);
30574
- const [history, setHistory] = useState67([]);
30556
+ const [rolling, setRolling] = useState66(false);
30557
+ const [results, setResults] = useState66(null);
30558
+ const [history, setHistory] = useState66([]);
30575
30559
  const dieWrapperRefs = useRef40([]);
30576
30560
  const cubeRefs = useRef40([]);
30577
30561
  const shadowRefs = useRef40([]);
@@ -30704,7 +30688,15 @@ function DiceRoller({
30704
30688
  },
30705
30689
  []
30706
30690
  );
30707
- useEffect52(() => {
30691
+ useEffect51(() => {
30692
+ if (timersRef.current.length > 0) {
30693
+ timersRef.current.forEach((t) => clearTimeout(t));
30694
+ timersRef.current = [];
30695
+ }
30696
+ if (rollingRef.current) {
30697
+ rollingRef.current = false;
30698
+ setRolling(false);
30699
+ }
30708
30700
  const rawW = arenaRef.current?.offsetWidth ?? 0;
30709
30701
  const rawH = arenaRef.current?.offsetHeight ?? 0;
30710
30702
  const w = rawW > 20 ? rawW : arena.w > 20 ? arena.w : 400;
@@ -30737,7 +30729,7 @@ function DiceRoller({
30737
30729
  defaultPositions,
30738
30730
  half
30739
30731
  ]);
30740
- useEffect52(() => {
30732
+ useEffect51(() => {
30741
30733
  setHistory([]);
30742
30734
  }, [sides]);
30743
30735
  const roll = useCallback41(() => {
@@ -30861,25 +30853,24 @@ function DiceRoller({
30861
30853
  defaultPositions,
30862
30854
  effectiveSize,
30863
30855
  half,
30864
- reducedMotion,
30865
30856
  onRoll,
30866
30857
  writeCubeRotation,
30867
30858
  writeDiePosition,
30868
30859
  showHistory,
30869
30860
  historyMax
30870
30861
  ]);
30871
- useEffect52(() => {
30862
+ useEffect51(() => {
30872
30863
  if (rollRef) rollRef.current = roll;
30873
30864
  return () => {
30874
30865
  if (rollRef && rollRef.current === roll) rollRef.current = null;
30875
30866
  };
30876
30867
  }, [roll, rollRef]);
30877
- useEffect52(() => {
30868
+ useEffect51(() => {
30878
30869
  if (!autoRoll) return;
30879
30870
  const t = setTimeout(roll, 300);
30880
30871
  return () => clearTimeout(t);
30881
30872
  }, []);
30882
- useEffect52(
30873
+ useEffect51(
30883
30874
  () => () => {
30884
30875
  timersRef.current.forEach((t) => clearTimeout(t));
30885
30876
  timersRef.current = [];
@@ -31180,10 +31171,10 @@ DiceRoller.displayName = "DiceRoller";
31180
31171
 
31181
31172
  // src/components/ui/bracket-generator/index.tsx
31182
31173
  import {
31183
- useState as useState68,
31174
+ useState as useState67,
31184
31175
  useCallback as useCallback42,
31185
31176
  useRef as useRef41,
31186
- useEffect as useEffect53,
31177
+ useEffect as useEffect52,
31187
31178
  useMemo as useMemo27,
31188
31179
  useLayoutEffect as useLayoutEffect2
31189
31180
  } from "react";
@@ -31406,7 +31397,7 @@ function computeLayout(rounds, firstRoundCount) {
31406
31397
  };
31407
31398
  }
31408
31399
  function usePanZoom(viewportRef, contentSize, minZoom, maxZoom) {
31409
- const [transform, setTransform] = useState68({
31400
+ const [transform, setTransform] = useState67({
31410
31401
  x: 0,
31411
31402
  y: 0,
31412
31403
  scale: 1
@@ -31426,7 +31417,7 @@ function usePanZoom(viewportRef, contentSize, minZoom, maxZoom) {
31426
31417
  },
31427
31418
  [contentSize]
31428
31419
  );
31429
- useEffect53(() => {
31420
+ useEffect52(() => {
31430
31421
  const el = viewportRef.current;
31431
31422
  if (!el) return;
31432
31423
  const onWheel = (e) => {
@@ -31657,12 +31648,12 @@ function BracketGenerator({
31657
31648
  ariaLabel = "Tournament bracket"
31658
31649
  }) {
31659
31650
  const dark = useDarkMode(darkMode);
31660
- const [bracketState, setBracketState] = useState68({
31651
+ const [bracketState, setBracketState] = useState67({
31661
31652
  matches: [],
31662
31653
  rounds: 0,
31663
31654
  champion: null
31664
31655
  });
31665
- const [simulating, setSimulating] = useState68(false);
31656
+ const [simulating, setSimulating] = useState67(false);
31666
31657
  const autoRef = useRef41(null);
31667
31658
  const viewportRef = useRef41(null);
31668
31659
  const effectiveParticipants = useMemo27(() => {
@@ -31717,7 +31708,7 @@ function BracketGenerator({
31717
31708
  minZoom,
31718
31709
  maxZoom
31719
31710
  );
31720
- const [miniMapHidden, setMiniMapHidden] = useState68(false);
31711
+ const [miniMapHidden, setMiniMapHidden] = useState67(false);
31721
31712
  const miniMapDragRef = useRef41(false);
31722
31713
  const resumeRef = useRef41(resumeMatches);
31723
31714
  const hydrateFromResume = useCallback42(() => {
@@ -31851,7 +31842,7 @@ function BracketGenerator({
31851
31842
  []
31852
31843
  );
31853
31844
  const prevBracketStateRef = useRef41(bracketState);
31854
- useEffect53(() => {
31845
+ useEffect52(() => {
31855
31846
  const prev = prevBracketStateRef.current;
31856
31847
  const next = bracketState;
31857
31848
  if (prev === next) return;
@@ -31875,7 +31866,7 @@ function BracketGenerator({
31875
31866
  [effectiveParticipants, seedingStrategy]
31876
31867
  );
31877
31868
  const seededKeyRef = useRef41(null);
31878
- useEffect53(() => {
31869
+ useEffect52(() => {
31879
31870
  if (seededKeyRef.current === structuralKey) {
31880
31871
  setBracketState((prev) => {
31881
31872
  if (!prev.matches.length) return prev;
@@ -31906,7 +31897,7 @@ function BracketGenerator({
31906
31897
  seedBracket();
31907
31898
  }
31908
31899
  }, [structuralKey, effectiveParticipants, hydrateFromResume, seedBracket]);
31909
- useEffect53(() => {
31900
+ useEffect52(() => {
31910
31901
  return () => {
31911
31902
  if (autoRef.current) clearTimeout(autoRef.current);
31912
31903
  };
@@ -31917,7 +31908,7 @@ function BracketGenerator({
31917
31908
  return () => clearTimeout(timer);
31918
31909
  }
31919
31910
  }, [contentSize.w]);
31920
- useEffect53(() => {
31911
+ useEffect52(() => {
31921
31912
  if (autoSimulate && bracketState.matches.length && !bracketState.champion) {
31922
31913
  const timer = setTimeout(autoPlay, 500);
31923
31914
  return () => clearTimeout(timer);
@@ -32638,10 +32629,10 @@ function BracketGenerator({
32638
32629
  // src/components/ui/coin-flipper/index.tsx
32639
32630
  import {
32640
32631
  useCallback as useCallback43,
32641
- useEffect as useEffect54,
32632
+ useEffect as useEffect53,
32642
32633
  useMemo as useMemo28,
32643
32634
  useRef as useRef42,
32644
- useState as useState69
32635
+ useState as useState68
32645
32636
  } from "react";
32646
32637
  import { jsx as jsx115, jsxs as jsxs88 } from "react/jsx-runtime";
32647
32638
  var PRESETS3 = {
@@ -32826,10 +32817,10 @@ function CoinFlipper({
32826
32817
  }) {
32827
32818
  const dark = useDarkMode(darkMode);
32828
32819
  const tc = typeof theme5 === "string" ? PRESETS3[theme5] || PRESETS3.gold : theme5;
32829
- const [result, setResult] = useState69(null);
32830
- const [flipping, setFlipping] = useState69(false);
32831
- const [history, setHistory] = useState69([]);
32832
- const [stats, setStats] = useState69({ heads: 0, tails: 0 });
32820
+ const [result, setResult] = useState68(null);
32821
+ const [flipping, setFlipping] = useState68(false);
32822
+ const [history, setHistory] = useState68([]);
32823
+ const [stats, setStats] = useState68({ heads: 0, tails: 0 });
32833
32824
  const flipIdRef = useRef42(0);
32834
32825
  const coinRef = useRef42(null);
32835
32826
  const shadowRef = useRef42(null);
@@ -32885,16 +32876,19 @@ function CoinFlipper({
32885
32876
  }, frames.length * stepMs);
32886
32877
  timersRef.current.push(settleTimer);
32887
32878
  }, [flipping, onFlip, historyMax]);
32888
- useEffect54(() => {
32879
+ useEffect53(() => {
32889
32880
  if (flipRef) flipRef.current = flip;
32881
+ return () => {
32882
+ if (flipRef && flipRef.current === flip) flipRef.current = null;
32883
+ };
32890
32884
  }, [flip, flipRef]);
32891
- useEffect54(() => {
32885
+ useEffect53(() => {
32892
32886
  if (autoFlip) {
32893
32887
  const timer = setTimeout(flip, 300);
32894
32888
  return () => clearTimeout(timer);
32895
32889
  }
32896
32890
  }, []);
32897
- useEffect54(() => {
32891
+ useEffect53(() => {
32898
32892
  return () => {
32899
32893
  timersRef.current.forEach(clearTimeout);
32900
32894
  };
@@ -33337,6 +33331,23 @@ import {
33337
33331
  useRef as useRef43,
33338
33332
  useState as useState70
33339
33333
  } from "react";
33334
+
33335
+ // src/hooks/web/use-prefers-reduced-motion.ts
33336
+ import { useEffect as useEffect54, useState as useState69 } from "react";
33337
+ function usePrefersReducedMotion() {
33338
+ const [reduced, setReduced] = useState69(false);
33339
+ useEffect54(() => {
33340
+ if (typeof window === "undefined" || !window.matchMedia) return;
33341
+ const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
33342
+ setReduced(mq.matches);
33343
+ const read = () => setReduced(mq.matches);
33344
+ mq.addEventListener("change", read);
33345
+ return () => mq.removeEventListener("change", read);
33346
+ }, []);
33347
+ return reduced;
33348
+ }
33349
+
33350
+ // src/components/ui/random-player-picker/index.tsx
33340
33351
  import { Fragment as Fragment31, jsx as jsx116, jsxs as jsxs89 } from "react/jsx-runtime";
33341
33352
  var PRESETS4 = {
33342
33353
  blue: {
@@ -33394,6 +33405,11 @@ var WEDGE_PALETTE = [
33394
33405
  ];
33395
33406
  var DEFAULT_EASING = "cubic-bezier(0.25, 0.46, 0.45, 0.94)";
33396
33407
  var DRAMATIC_EASING = "cubic-bezier(0.39, 0.575, 0.565, 1)";
33408
+ var PHYSICS_DECAY_K = 4;
33409
+ function physicsRampDown(t) {
33410
+ return (1 - Math.exp(-PHYSICS_DECAY_K * t)) / (1 - Math.exp(-PHYSICS_DECAY_K));
33411
+ }
33412
+ var REEL_LEAD_IN = 32;
33397
33413
  var INTENSITY_CONFIG = {
33398
33414
  subtle: {
33399
33415
  particles: 14,
@@ -33465,6 +33481,9 @@ function ReelStage({
33465
33481
  winner,
33466
33482
  spinning,
33467
33483
  reelY,
33484
+ displayY,
33485
+ cssTransition,
33486
+ tapeRef,
33468
33487
  reelVisibleRows,
33469
33488
  reelRowHeight,
33470
33489
  size,
@@ -33488,17 +33507,16 @@ function ReelStage({
33488
33507
  const stripHeight = reelVisibleRows * reelRowHeight;
33489
33508
  const centerOffsetPx = (reelVisibleRows - 1) / 2 * reelRowHeight;
33490
33509
  const P = pool.length;
33491
- const leadIn = 32;
33492
33510
  const tapeRows = Math.max(
33493
- reelVisibleRows + leadIn + 1,
33494
- Math.ceil((reelY + stripHeight) / reelRowHeight) + leadIn + 1
33511
+ reelVisibleRows + REEL_LEAD_IN + 1,
33512
+ Math.ceil((reelY + stripHeight) / reelRowHeight) + REEL_LEAD_IN + 1
33495
33513
  );
33496
33514
  const tape = Array.from({ length: tapeRows }, (_, i) => {
33497
- const poolIdx = P === 0 ? 0 : ((i - leadIn) % P + P) % P;
33515
+ const poolIdx = P === 0 ? 0 : ((i - REEL_LEAD_IN) % P + P) % P;
33498
33516
  return pool[poolIdx];
33499
33517
  });
33500
- const effectiveY = reelY + leadIn * reelRowHeight;
33501
- const transition = spinning ? `transform ${reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult)}ms ${intensityCfg.easing}` : "none";
33518
+ const effectiveY = displayY + REEL_LEAD_IN * reelRowHeight;
33519
+ const transition = spinning && cssTransition ? `transform ${reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult)}ms ${intensityCfg.easing}` : "none";
33502
33520
  return /* @__PURE__ */ jsxs89(
33503
33521
  "div",
33504
33522
  {
@@ -33543,6 +33561,7 @@ function ReelStage({
33543
33561
  ) : /* @__PURE__ */ jsx116(
33544
33562
  "div",
33545
33563
  {
33564
+ ref: tapeRef,
33546
33565
  "aria-hidden": "true",
33547
33566
  style: {
33548
33567
  position: "absolute",
@@ -33758,6 +33777,9 @@ function RandomPlayerPicker({
33758
33777
  showStats = true,
33759
33778
  renderWinner,
33760
33779
  spinRef,
33780
+ resetRef,
33781
+ onPoolChange,
33782
+ draftCompleteCaption = "Draft complete \u2014 reset to spin again",
33761
33783
  intensity = "normal",
33762
33784
  enableHaptics = false,
33763
33785
  tickSoundUrl,
@@ -33772,6 +33794,7 @@ function RandomPlayerPicker({
33772
33794
  const dark = useDarkMode(darkMode);
33773
33795
  const reducedMotion = usePrefersReducedMotion();
33774
33796
  const intensityCfg = INTENSITY_CONFIG[intensity];
33797
+ const physicsDriven = intensity === "dramatic" && !reducedMotion;
33775
33798
  const [pool, setPool] = useState70(participants);
33776
33799
  const [rotation, setRotation] = useState70(0);
33777
33800
  const [reelY, setReelY] = useState70(0);
@@ -33784,10 +33807,17 @@ function RandomPlayerPicker({
33784
33807
  const particleTimer = useRef43(null);
33785
33808
  const popTimer = useRef43(null);
33786
33809
  const tickInterval = useRef43(null);
33810
+ const pendingPopIdxRef = useRef43(null);
33811
+ const removeWinnersRef = useRef43(removeWinners);
33787
33812
  const tickAudioRef = useRef43(null);
33788
33813
  const winAudioRef = useRef43(null);
33789
33814
  const containerRef = useRef43(null);
33790
33815
  const prevIdsRef = useRef43("");
33816
+ const wheelSvgRef = useRef43(null);
33817
+ const reelTapeRef = useRef43(null);
33818
+ const rafRef = useRef43(null);
33819
+ const visualRotationRef = useRef43(null);
33820
+ const visualReelYRef = useRef43(null);
33791
33821
  useEffect55(() => {
33792
33822
  if (typeof window === "undefined" || !tickSoundUrl) {
33793
33823
  tickAudioRef.current = null;
@@ -33822,10 +33852,33 @@ function RandomPlayerPicker({
33822
33852
  winAudioRef.current = null;
33823
33853
  };
33824
33854
  }, [winSoundUrl, soundVolume]);
33855
+ useEffect55(() => {
33856
+ removeWinnersRef.current = removeWinners;
33857
+ }, [removeWinners]);
33825
33858
  useEffect55(() => {
33826
33859
  const ids = participants.map((p) => p.id).join("|");
33827
33860
  const structuralChange = ids !== prevIdsRef.current;
33828
33861
  if (structuralChange) {
33862
+ if (winnerTimer.current) {
33863
+ clearTimeout(winnerTimer.current);
33864
+ winnerTimer.current = null;
33865
+ }
33866
+ if (tickInterval.current) {
33867
+ clearTimeout(tickInterval.current);
33868
+ tickInterval.current = null;
33869
+ }
33870
+ if (popTimer.current) {
33871
+ clearTimeout(popTimer.current);
33872
+ popTimer.current = null;
33873
+ }
33874
+ if (rafRef.current) {
33875
+ cancelAnimationFrame(rafRef.current);
33876
+ rafRef.current = null;
33877
+ }
33878
+ visualRotationRef.current = null;
33879
+ visualReelYRef.current = null;
33880
+ pendingPopIdxRef.current = null;
33881
+ setSpinning(false);
33829
33882
  setPool(participants);
33830
33883
  setHistory([]);
33831
33884
  setWinner(null);
@@ -33882,33 +33935,50 @@ function RandomPlayerPicker({
33882
33935
  [dark]
33883
33936
  );
33884
33937
  const spin = useCallback44(() => {
33885
- if (spinning || n < 2) return;
33938
+ if (spinning) return;
33939
+ let activePool = pool;
33940
+ if (popTimer.current) {
33941
+ clearTimeout(popTimer.current);
33942
+ popTimer.current = null;
33943
+ const pendingIdx = pendingPopIdxRef.current;
33944
+ pendingPopIdxRef.current = null;
33945
+ if (pendingIdx != null && pendingIdx >= 0 && pendingIdx < pool.length && removeWinnersRef.current) {
33946
+ activePool = pool.filter((_, i) => i !== pendingIdx);
33947
+ setPool(activePool);
33948
+ setWinner(null);
33949
+ }
33950
+ }
33951
+ const activeN = activePool.length;
33952
+ if (activeN < 2) return;
33953
+ const activeAnglePerWedge = 2 * Math.PI / activeN;
33954
+ const activeRotation = rotation;
33955
+ const activeReelY = reelY;
33886
33956
  onSpinStart?.();
33887
- const winnerIdx = weightedIndex(pool);
33957
+ const winnerIdx = weightedIndex(activePool);
33888
33958
  const minRev = minRevolutions + intensityCfg.revsBoost;
33889
33959
  const maxRev = maxRevolutions + intensityCfg.revsBoost;
33890
33960
  const minFullRevs = Math.ceil(minRev);
33891
33961
  const maxFullRevs = Math.max(minFullRevs, Math.floor(maxRev));
33892
33962
  const revolutions = minFullRevs + Math.floor(Math.random() * (maxFullRevs - minFullRevs + 1));
33893
- const wedgeMidRad = winnerIdx * anglePerWedge + anglePerWedge / 2;
33963
+ const wedgeMidRad = winnerIdx * activeAnglePerWedge + activeAnglePerWedge / 2;
33894
33964
  const wedgeMidDeg = wedgeMidRad * 180 / Math.PI;
33895
- const wheelWobble = (Math.random() - 0.5) * (anglePerWedge * 180 / Math.PI) * 0.6;
33896
- const currentMod = (rotation % 360 + 360) % 360;
33965
+ const wheelWobble = (Math.random() - 0.5) * (activeAnglePerWedge * 180 / Math.PI) * 0.6;
33966
+ const currentMod = (activeRotation % 360 + 360) % 360;
33897
33967
  const targetMod = ((-wedgeMidDeg + wheelWobble) % 360 + 360) % 360;
33898
33968
  let deltaMod = targetMod - currentMod;
33899
33969
  if (deltaMod < 0) deltaMod += 360;
33900
- const finalRotation = rotation + deltaMod + revolutions * 360;
33970
+ const finalRotation = activeRotation + deltaMod + revolutions * 360;
33901
33971
  const centerOffsetRows = (reelVisibleRows - 1) / 2;
33902
33972
  const centerOffsetPx = centerOffsetRows * reelRowHeight;
33903
33973
  const currentCenterRow = Math.round(
33904
- (reelY + centerOffsetPx) / reelRowHeight
33974
+ (activeReelY + centerOffsetPx) / reelRowHeight
33905
33975
  );
33906
- const currentCenterMod = mod(currentCenterRow, n);
33907
- const targetRowOffset = (winnerIdx - currentCenterMod + n) % n;
33908
- const reelRevRows = Math.round(revolutions * n);
33909
- const finalReelY = reelY + (reelRevRows + targetRowOffset) * reelRowHeight;
33910
- const settledWinnerIdx = mode === "reel" ? reelIndexAtCenter(finalReelY, centerOffsetPx, reelRowHeight, n) : wheelIndexAtPointer(finalRotation, n);
33911
- const finalWinnerIdx = settledWinnerIdx >= 0 && settledWinnerIdx < pool.length ? settledWinnerIdx : winnerIdx;
33976
+ const currentCenterMod = mod(currentCenterRow, activeN);
33977
+ const targetRowOffset = (winnerIdx - currentCenterMod + activeN) % activeN;
33978
+ const reelRevRows = Math.round(revolutions * activeN);
33979
+ const finalReelY = activeReelY + (reelRevRows + targetRowOffset) * reelRowHeight;
33980
+ const settledWinnerIdx = mode === "reel" ? reelIndexAtCenter(finalReelY, centerOffsetPx, reelRowHeight, activeN) : wheelIndexAtPointer(finalRotation, activeN);
33981
+ const finalWinnerIdx = settledWinnerIdx >= 0 && settledWinnerIdx < activePool.length ? settledWinnerIdx : winnerIdx;
33912
33982
  const effectiveDuration = reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult);
33913
33983
  setSpinning(true);
33914
33984
  setWinner(null);
@@ -33918,10 +33988,59 @@ function RandomPlayerPicker({
33918
33988
  } else {
33919
33989
  setRotation(finalRotation);
33920
33990
  }
33921
- if ((onTick || tickAudioRef.current) && !reducedMotion) {
33991
+ const fireTick = () => {
33992
+ onTick?.();
33993
+ const ta = tickAudioRef.current;
33994
+ if (ta) {
33995
+ try {
33996
+ ta.currentTime = 0;
33997
+ void ta.play().catch(() => {
33998
+ });
33999
+ } catch {
34000
+ }
34001
+ }
34002
+ };
34003
+ if (physicsDriven) {
34004
+ const from = mode === "reel" ? activeReelY : activeRotation;
34005
+ const to = mode === "reel" ? finalReelY : finalRotation;
34006
+ const crossUnit = mode === "reel" ? reelRowHeight : activeAnglePerWedge * 180 / Math.PI;
34007
+ if (mode === "reel") visualReelYRef.current = from;
34008
+ else visualRotationRef.current = from;
34009
+ let crossings = 0;
34010
+ const t0 = performance.now();
34011
+ const step = (now) => {
34012
+ const t = Math.min(1, (now - t0) / effectiveDuration);
34013
+ const value = from + (to - from) * physicsRampDown(t);
34014
+ if (mode === "reel") {
34015
+ visualReelYRef.current = value;
34016
+ const el = reelTapeRef.current;
34017
+ if (el) {
34018
+ el.style.transform = `translateY(${-(value + REEL_LEAD_IN * reelRowHeight)}px)`;
34019
+ }
34020
+ } else {
34021
+ visualRotationRef.current = value;
34022
+ const el = wheelSvgRef.current;
34023
+ if (el) el.style.transform = `rotate(${value}deg)`;
34024
+ }
34025
+ const crossed = Math.floor(Math.abs(value - from) / crossUnit);
34026
+ if (crossed > crossings) {
34027
+ crossings = crossed;
34028
+ fireTick();
34029
+ }
34030
+ if (t < 1) {
34031
+ rafRef.current = requestAnimationFrame(step);
34032
+ } else {
34033
+ rafRef.current = null;
34034
+ visualRotationRef.current = null;
34035
+ visualReelYRef.current = null;
34036
+ }
34037
+ };
34038
+ rafRef.current = requestAnimationFrame(step);
34039
+ }
34040
+ if (!physicsDriven && (onTick || tickAudioRef.current) && !reducedMotion) {
33922
34041
  const wedges = mode === "reel" ? Math.max(1, reelRevRows + targetRowOffset) : (() => {
33923
34042
  const totalDelta = deltaMod + revolutions * 360;
33924
- const wedgeDeg = anglePerWedge * 180 / Math.PI;
34043
+ const wedgeDeg = activeAnglePerWedge * 180 / Math.PI;
33925
34044
  return Math.max(
33926
34045
  1,
33927
34046
  Math.round(totalDelta / Math.max(wedgeDeg, 1))
@@ -33936,16 +34055,7 @@ function RandomPlayerPicker({
33936
34055
  const eased = 1 - Math.pow(1 - progress, 3);
33937
34056
  const interval = startInterval + (endInterval - startInterval) * eased;
33938
34057
  tickInterval.current = setTimeout(() => {
33939
- onTick?.();
33940
- const ta = tickAudioRef.current;
33941
- if (ta) {
33942
- try {
33943
- ta.currentTime = 0;
33944
- void ta.play().catch(() => {
33945
- });
33946
- } catch {
33947
- }
33948
- }
34058
+ fireTick();
33949
34059
  elapsed += interval;
33950
34060
  schedule();
33951
34061
  }, interval);
@@ -33953,7 +34063,7 @@ function RandomPlayerPicker({
33953
34063
  schedule();
33954
34064
  }
33955
34065
  winnerTimer.current = setTimeout(() => {
33956
- const picked = pool[finalWinnerIdx];
34066
+ const picked = activePool[finalWinnerIdx];
33957
34067
  if (!picked) {
33958
34068
  setSpinning(false);
33959
34069
  return;
@@ -33999,9 +34109,13 @@ function RandomPlayerPicker({
33999
34109
  particleTimer.current = setTimeout(() => setParticles([]), 2400);
34000
34110
  }
34001
34111
  if (removeWinners) {
34112
+ pendingPopIdxRef.current = finalWinnerIdx;
34002
34113
  popTimer.current = setTimeout(() => {
34114
+ popTimer.current = null;
34115
+ pendingPopIdxRef.current = null;
34116
+ if (!removeWinnersRef.current) return;
34003
34117
  setPool((p) => p.filter((_, i) => i !== finalWinnerIdx));
34004
- setWinner(null);
34118
+ if (activePool.length - 1 >= 2) setWinner(null);
34005
34119
  setRotation(0);
34006
34120
  setReelY(0);
34007
34121
  }, 2800);
@@ -34010,8 +34124,6 @@ function RandomPlayerPicker({
34010
34124
  }, [
34011
34125
  pool,
34012
34126
  spinning,
34013
- n,
34014
- anglePerWedge,
34015
34127
  rotation,
34016
34128
  reelY,
34017
34129
  mode,
@@ -34026,6 +34138,7 @@ function RandomPlayerPicker({
34026
34138
  maxRevolutions,
34027
34139
  history,
34028
34140
  reducedMotion,
34141
+ physicsDriven,
34029
34142
  enableHaptics,
34030
34143
  intensityCfg.particles,
34031
34144
  intensityCfg.durationMult,
@@ -34037,16 +34150,25 @@ function RandomPlayerPicker({
34037
34150
  if (spinRef && spinRef.current === spin) spinRef.current = null;
34038
34151
  };
34039
34152
  }, [spin, spinRef]);
34153
+ const onPoolChangeRef = useRef43(onPoolChange);
34154
+ useEffect55(() => {
34155
+ onPoolChangeRef.current = onPoolChange;
34156
+ });
34157
+ useEffect55(() => {
34158
+ onPoolChangeRef.current?.(pool);
34159
+ }, [pool]);
34040
34160
  useEffect55(
34041
34161
  () => () => {
34042
34162
  if (winnerTimer.current) clearTimeout(winnerTimer.current);
34043
34163
  if (particleTimer.current) clearTimeout(particleTimer.current);
34044
34164
  if (popTimer.current) clearTimeout(popTimer.current);
34045
34165
  if (tickInterval.current) clearTimeout(tickInterval.current);
34166
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
34046
34167
  },
34047
34168
  []
34048
34169
  );
34049
34170
  useEffect55(() => {
34171
+ if (trigger === "external") return;
34050
34172
  const el = containerRef.current;
34051
34173
  if (!el) return;
34052
34174
  const h = (e) => {
@@ -34057,8 +34179,28 @@ function RandomPlayerPicker({
34057
34179
  };
34058
34180
  el.addEventListener("keydown", h);
34059
34181
  return () => el.removeEventListener("keydown", h);
34060
- }, [spin]);
34182
+ }, [spin, trigger]);
34061
34183
  const reset = useCallback44(() => {
34184
+ if (winnerTimer.current) {
34185
+ clearTimeout(winnerTimer.current);
34186
+ winnerTimer.current = null;
34187
+ }
34188
+ if (tickInterval.current) {
34189
+ clearTimeout(tickInterval.current);
34190
+ tickInterval.current = null;
34191
+ }
34192
+ if (popTimer.current) {
34193
+ clearTimeout(popTimer.current);
34194
+ popTimer.current = null;
34195
+ }
34196
+ if (rafRef.current) {
34197
+ cancelAnimationFrame(rafRef.current);
34198
+ rafRef.current = null;
34199
+ }
34200
+ visualRotationRef.current = null;
34201
+ visualReelYRef.current = null;
34202
+ pendingPopIdxRef.current = null;
34203
+ setSpinning(false);
34062
34204
  setPool(participants);
34063
34205
  setHistory([]);
34064
34206
  setWinner(null);
@@ -34066,9 +34208,17 @@ function RandomPlayerPicker({
34066
34208
  setReelY(0);
34067
34209
  setParticles([]);
34068
34210
  }, [participants]);
34211
+ useEffect55(() => {
34212
+ if (resetRef) resetRef.current = reset;
34213
+ return () => {
34214
+ if (resetRef && resetRef.current === reset) resetRef.current = null;
34215
+ };
34216
+ }, [reset, resetRef]);
34069
34217
  const isIdle = n < 2;
34218
+ const drainedByDraft = isIdle && participants.length >= 2;
34219
+ const effectiveIdleCaption = drainedByDraft ? draftCompleteCaption : placeholderCaption;
34070
34220
  const placeholderDiscColor = dark ? "#2a3344" : "#e2e8f0";
34071
- if (isIdle && placeholderMode === "message") {
34221
+ if (isIdle && !drainedByDraft && placeholderMode === "message") {
34072
34222
  return /* @__PURE__ */ jsx116(
34073
34223
  "div",
34074
34224
  {
@@ -34090,6 +34240,8 @@ function RandomPlayerPicker({
34090
34240
  const shakeKeyframe = `rppShake_${intensity}`;
34091
34241
  const glowKeyframe = `rppGlow_${intensity}`;
34092
34242
  const pulseKeyframe = `rppPulse_${intensity}`;
34243
+ const displayRotation = visualRotationRef.current ?? rotation;
34244
+ const displayReelY = visualReelYRef.current ?? reelY;
34093
34245
  return /* @__PURE__ */ jsxs89(
34094
34246
  "div",
34095
34247
  {
@@ -34136,13 +34288,33 @@ function RandomPlayerPicker({
34136
34288
  [data-rpp-anim] { animation: none !important; }
34137
34289
  }
34138
34290
  ` }),
34139
- mode === "reel" ? /* @__PURE__ */ jsx116(
34291
+ isIdle && placeholderMode === "message" ? (
34292
+ /* Drained draft in "message" mode — compact box in the wheel's
34293
+ place; the winner card / stats / controls / history below stay
34294
+ mounted so the endgame isn't a dead end. */
34295
+ /* @__PURE__ */ jsx116(
34296
+ "div",
34297
+ {
34298
+ style: {
34299
+ padding: "2rem 1rem",
34300
+ textAlign: "center",
34301
+ color: c.dim,
34302
+ borderRadius: 12,
34303
+ border: `1px dashed ${c.border}`
34304
+ },
34305
+ children: effectiveIdleCaption
34306
+ }
34307
+ )
34308
+ ) : mode === "reel" ? /* @__PURE__ */ jsx116(
34140
34309
  ReelStage,
34141
34310
  {
34142
34311
  pool,
34143
34312
  winner,
34144
34313
  spinning,
34145
34314
  reelY,
34315
+ displayY: displayReelY,
34316
+ cssTransition: !physicsDriven,
34317
+ tapeRef: reelTapeRef,
34146
34318
  reelVisibleRows,
34147
34319
  reelRowHeight,
34148
34320
  size,
@@ -34151,7 +34323,7 @@ function RandomPlayerPicker({
34151
34323
  dark,
34152
34324
  isIdle,
34153
34325
  placeholderMode,
34154
- placeholderCaption,
34326
+ placeholderCaption: effectiveIdleCaption,
34155
34327
  placeholderDiscColor,
34156
34328
  spinDuration,
34157
34329
  intensityCfg,
@@ -34208,6 +34380,7 @@ function RandomPlayerPicker({
34208
34380
  /* @__PURE__ */ jsxs89(
34209
34381
  "svg",
34210
34382
  {
34383
+ ref: wheelSvgRef,
34211
34384
  width: size,
34212
34385
  height: size,
34213
34386
  "aria-hidden": "true",
@@ -34216,8 +34389,8 @@ function RandomPlayerPicker({
34216
34389
  top: 36,
34217
34390
  left: 0,
34218
34391
  transformOrigin: `${cx}px ${cy}px`,
34219
- transform: `rotate(${rotation}deg)`,
34220
- transition: spinning ? `transform ${reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult)}ms ${intensityCfg.easing}` : "none",
34392
+ transform: `rotate(${displayRotation}deg)`,
34393
+ transition: spinning && !physicsDriven ? `transform ${reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult)}ms ${intensityCfg.easing}` : "none",
34221
34394
  filter: `drop-shadow(0 8px 28px ${dark ? "rgba(0,0,0,0.55)" : "rgba(0,0,0,0.15)"})`
34222
34395
  },
34223
34396
  children: [
@@ -34370,7 +34543,7 @@ function RandomPlayerPicker({
34370
34543
  pointerEvents: "none",
34371
34544
  zIndex: 3
34372
34545
  },
34373
- children: placeholderCaption
34546
+ children: effectiveIdleCaption
34374
34547
  }
34375
34548
  ),
34376
34549
  particles.map((p) => /* @__PURE__ */ jsx116(
@@ -38531,27 +38704,28 @@ function GameTimer({
38531
38704
  const now = performance.now();
38532
38705
  const delta = now - lastTickRef.current;
38533
38706
  lastTickRef.current = now;
38534
- setElapsedMs((prev) => {
38535
- const next = prev + delta;
38536
- if (mode === "countdown" && next >= durationMs) {
38537
- setRunning(false);
38538
- setCompleted(true);
38539
- onComplete?.();
38540
- return durationMs;
38707
+ const next = elapsedRef.current + delta;
38708
+ if (mode === "countdown" && next >= durationMs) {
38709
+ elapsedRef.current = durationMs;
38710
+ setElapsedMs(durationMs);
38711
+ setRunning(false);
38712
+ setCompleted(true);
38713
+ onComplete?.();
38714
+ return;
38715
+ }
38716
+ if (mode === "countdown") {
38717
+ const secLeft = Math.ceil((durationMs - next) / 1e3);
38718
+ if (secLeft <= warningAt && !warningFiredRef.current) {
38719
+ warningFiredRef.current = true;
38720
+ onWarning?.();
38541
38721
  }
38542
- if (mode === "countdown") {
38543
- const secLeft = Math.ceil((durationMs - next) / 1e3);
38544
- if (secLeft <= warningAt && !warningFiredRef.current) {
38545
- warningFiredRef.current = true;
38546
- onWarning?.();
38547
- }
38548
- if (secLeft <= dangerAt && !dangerFiredRef.current) {
38549
- dangerFiredRef.current = true;
38550
- onDanger?.();
38551
- }
38722
+ if (secLeft <= dangerAt && !dangerFiredRef.current) {
38723
+ dangerFiredRef.current = true;
38724
+ onDanger?.();
38552
38725
  }
38553
- return next;
38554
- });
38726
+ }
38727
+ elapsedRef.current = next;
38728
+ setElapsedMs(next);
38555
38729
  rafRef.current = requestAnimationFrame(tick);
38556
38730
  }, [mode, durationMs, warningAt, dangerAt, onComplete, onWarning, onDanger]);
38557
38731
  useEffect59(() => {
@@ -38572,6 +38746,7 @@ function GameTimer({
38572
38746
  }, []);
38573
38747
  const reset = useCallback48(() => {
38574
38748
  setRunning(false);
38749
+ elapsedRef.current = 0;
38575
38750
  setElapsedMs(0);
38576
38751
  setCompleted(false);
38577
38752
  setLaps([]);
@@ -39880,8 +40055,10 @@ function TeamGenerator({
39880
40055
  minPlayersPerTeam = 2,
39881
40056
  maxPlayers = Number.POSITIVE_INFINITY,
39882
40057
  defaultNames = [],
40058
+ defaultTeams,
39883
40059
  onNamesChange,
39884
40060
  onGenerate,
40061
+ onTeamsChange,
39885
40062
  onTeamCountChange,
39886
40063
  labels,
39887
40064
  ariaLabel = "Team generator",
@@ -39896,12 +40073,17 @@ function TeamGenerator({
39896
40073
  const [names, setNames] = useState76(defaultNames);
39897
40074
  const [input, setInput] = useState76("");
39898
40075
  const [teamCount, setTeamCount] = useState76(initialCount);
39899
- const [teams, setTeams] = useState76(null);
40076
+ const [teams, setTeams] = useState76(
40077
+ defaultTeams && defaultTeams.length > 0 ? defaultTeams : null
40078
+ );
39900
40079
  const inputRef = useRef49(null);
39901
40080
  const minNames = teamCount * minPlayersPerTeam;
39902
40081
  useEffect61(() => {
39903
40082
  onNamesChange?.(names);
39904
40083
  }, [names, onNamesChange]);
40084
+ useEffect61(() => {
40085
+ onTeamsChange?.(teams);
40086
+ }, [teams, onTeamsChange]);
39905
40087
  const addName = useCallback50(() => {
39906
40088
  const trimmed = input.trim();
39907
40089
  if (!trimmed) return;
@@ -40528,6 +40710,8 @@ function RoundRobinScheduler({
40528
40710
  theme: theme5 = "slate",
40529
40711
  darkMode,
40530
40712
  minTeams = 3,
40713
+ maxTeams,
40714
+ nameMaxLength,
40531
40715
  defaultTeams = [],
40532
40716
  resumeRounds,
40533
40717
  onTeamsChange,
@@ -40550,39 +40734,41 @@ function RoundRobinScheduler({
40550
40734
  useEffect62(() => {
40551
40735
  onTeamsChange?.(teams);
40552
40736
  }, [teams, onTeamsChange]);
40553
- const addTeams = useCallback51((raw) => {
40554
- const names = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
40555
- if (names.length === 0) return;
40556
- setTeams((prev) => {
40737
+ const mergeNames = useCallback51(
40738
+ (prev, incoming) => {
40557
40739
  const existing = new Set(prev.map((t) => t.toLowerCase()));
40558
40740
  const next = [...prev];
40559
- for (const name of names) {
40560
- if (!existing.has(name.toLowerCase())) {
40561
- existing.add(name.toLowerCase());
40562
- next.push(name);
40563
- }
40564
- }
40565
- return next;
40566
- });
40567
- setInput("");
40568
- setRounds([]);
40569
- }, []);
40570
- const importTeams = useCallback51((incoming) => {
40571
- if (incoming.length === 0) return;
40572
- setTeams((prev) => {
40573
- const existing = new Set(prev.map((t) => t.toLowerCase()));
40574
- const next = [...prev];
40575
- for (const name of incoming) {
40741
+ for (const rawName of incoming) {
40742
+ if (maxTeams != null && next.length >= maxTeams) break;
40743
+ const name = nameMaxLength != null ? rawName.slice(0, nameMaxLength) : rawName;
40576
40744
  const key = name.toLowerCase();
40577
- if (!existing.has(key)) {
40745
+ if (name.length > 0 && !existing.has(key)) {
40578
40746
  existing.add(key);
40579
40747
  next.push(name);
40580
40748
  }
40581
40749
  }
40582
40750
  return next;
40583
- });
40584
- setRounds([]);
40585
- }, []);
40751
+ },
40752
+ [maxTeams, nameMaxLength]
40753
+ );
40754
+ const addTeams = useCallback51(
40755
+ (raw) => {
40756
+ const names = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
40757
+ if (names.length === 0) return;
40758
+ setTeams((prev) => mergeNames(prev, names));
40759
+ setInput("");
40760
+ setRounds([]);
40761
+ },
40762
+ [mergeNames]
40763
+ );
40764
+ const importTeams = useCallback51(
40765
+ (incoming) => {
40766
+ if (incoming.length === 0) return;
40767
+ setTeams((prev) => mergeNames(prev, incoming));
40768
+ setRounds([]);
40769
+ },
40770
+ [mergeNames]
40771
+ );
40586
40772
  const handleKeyDown = useCallback51(
40587
40773
  (e) => {
40588
40774
  if (e.key === "Enter") {
@@ -41076,6 +41262,8 @@ function Scoreboard({
41076
41262
  onTeamsChange,
41077
41263
  onRoundAdvance,
41078
41264
  onNewGame,
41265
+ onReset,
41266
+ nameMaxLength,
41079
41267
  labels,
41080
41268
  ariaLabel = "Scoreboard",
41081
41269
  className
@@ -41150,7 +41338,8 @@ function Scoreboard({
41150
41338
  setRound(1);
41151
41339
  setHistory([]);
41152
41340
  setHistoryOpen(false);
41153
- }, []);
41341
+ onReset?.();
41342
+ }, [onReset]);
41154
41343
  const newGame = useCallback52(() => {
41155
41344
  setTeams(createDefaultTeams(seedCount, L.teamNamePrefix));
41156
41345
  setRound(1);
@@ -41372,6 +41561,7 @@ function Scoreboard({
41372
41561
  {
41373
41562
  type: "text",
41374
41563
  value: team.name,
41564
+ maxLength: nameMaxLength,
41375
41565
  onChange: (e) => updateName(index, e.target.value),
41376
41566
  style: nameInputStyle,
41377
41567
  "aria-label": `Team ${index + 1} name`