@digilogiclabs/saas-factory-ui 2.4.0 → 2.6.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.
@@ -5551,8 +5551,14 @@ interface RandomPlayerPickerProps {
5551
5551
  darkMode?: boolean;
5552
5552
  /** Wheel diameter in px. Default 380. */
5553
5553
  size?: number;
5554
- /** Called when a winner is locked in. */
5555
- onPick?: (winner: RandomPlayerParticipant, history: RandomPlayerParticipant[]) => void;
5554
+ /**
5555
+ * Called when a winner is locked in. `info.lastStanding` is true for
5556
+ * the automatic final pick of a draft — the survivor promoted when
5557
+ * pops drain the pool to one.
5558
+ */
5559
+ onPick?: (winner: RandomPlayerParticipant, history: RandomPlayerParticipant[], info?: {
5560
+ lastStanding: boolean;
5561
+ }) => void;
5556
5562
  /** Fired the moment a spin begins (before the animation). */
5557
5563
  onSpinStart?: () => void;
5558
5564
  /**
@@ -5586,14 +5592,37 @@ interface RandomPlayerPickerProps {
5586
5592
  renderWinner?: (winner: RandomPlayerParticipant) => ReactNode;
5587
5593
  /** Imperative handle. `spinRef.current?.()` triggers a spin. */
5588
5594
  spinRef?: MutableRefObject<(() => void) | null>;
5595
+ /**
5596
+ * Imperative reset handle. `resetRef.current?.()` restores the pool to
5597
+ * the full `participants` list and clears history/winner — the escape
5598
+ * hatch for consumers that hide the built-in controls
5599
+ * (`showControls={false}`) but still need a "reset draft" affordance
5600
+ * once draft-mode pops drain the pool.
5601
+ */
5602
+ resetRef?: MutableRefObject<(() => void) | null>;
5603
+ /**
5604
+ * Fired whenever the internal pool changes (mount, structural sync,
5605
+ * draft-mode pop, spin-again flush, reset). Draft-mode consumers use
5606
+ * this to mirror the live pool — e.g. to disable an external spin
5607
+ * trigger when the pool drains below 2 instead of presenting a dead
5608
+ * button, and to offer a reset once the draft completes.
5609
+ */
5610
+ onPoolChange?: (pool: RandomPlayerParticipant[]) => void;
5611
+ /**
5612
+ * Caption shown in place of `placeholderCaption` when draft-mode pops
5613
+ * drain the pool below 2 while the roster itself still has 2+ members.
5614
+ * Default: "Draft complete — reset to spin again".
5615
+ */
5616
+ draftCompleteCaption?: string;
5589
5617
  /**
5590
5618
  * Visual intensity preset. Default "normal".
5591
- * - "subtle" / "normal": current behavior — shorter spins, standard
5592
- * ease-out curve.
5593
- * - "dramatic": physics-like spin 2.5× duration, inertia-style
5594
- * curve so motion continues through the full duration with a slow
5595
- * winding-down tail, +12 extra revolutions, plus more
5596
- * particles/glow/shake on reveal.
5619
+ * - "subtle" / "normal": shorter spins, standard CSS ease-out curve.
5620
+ * - "dramatic": physics-driven spin — 2.5× duration, rAF-driven
5621
+ * exponential velocity decay (v ∝ e^(-4t)) that keeps the wheel
5622
+ * visibly winding down through the FULL duration and lands exactly
5623
+ * at the end, +12 extra revolutions, ticks fired on actual
5624
+ * wedge/row crossings, plus more particles/glow/shake on reveal.
5625
+ * Falls back to a 250ms CSS settle under prefers-reduced-motion.
5597
5626
  */
5598
5627
  intensity?: RandomPlayerIntensity;
5599
5628
  /**
@@ -5636,7 +5665,7 @@ interface RandomPlayerPickerProps {
5636
5665
  /** Accessible label for the wheel root. */
5637
5666
  ariaLabel?: string;
5638
5667
  }
5639
- declare function RandomPlayerPicker({ participants, mode, reelVisibleRows, reelRowHeight, theme, darkMode, size, onPick, onSpinStart, onTick, trigger, label, removeWinners, spinDuration, minRevolutions, maxRevolutions, showHistory, showControls, showStats, renderWinner, spinRef, intensity, enableHaptics, tickSoundUrl, winSoundUrl, soundVolume, placeholderMode, placeholderCaption, className, style, ariaLabel, }: RandomPlayerPickerProps): react_jsx_runtime.JSX.Element;
5668
+ declare function RandomPlayerPicker({ participants, mode, reelVisibleRows, reelRowHeight, theme, darkMode, size, onPick, onSpinStart, onTick, trigger, label, removeWinners, spinDuration, minRevolutions, maxRevolutions, showHistory, showControls, showStats, renderWinner, spinRef, resetRef, onPoolChange, draftCompleteCaption, intensity, enableHaptics, tickSoundUrl, winSoundUrl, soundVolume, placeholderMode, placeholderCaption, className, style, ariaLabel, }: RandomPlayerPickerProps): react_jsx_runtime.JSX.Element;
5640
5669
 
5641
5670
  type HeroBannerTransition = "fade" | "slide" | "zoom" | "blur" | "reveal" | "push";
5642
5671
  type HeroBannerImageFilter = "none" | "cinematic" | "dramatic" | "cool" | "warm" | "desaturated" | "noir" | "vibrant";
@@ -6202,10 +6231,20 @@ interface TeamGeneratorProps {
6202
6231
  maxPlayers?: number;
6203
6232
  /** Initial roster. Uncontrolled — the component manages names internally. */
6204
6233
  defaultNames?: string[];
6234
+ /** Initial generated teams (e.g. restored from a saved session).
6235
+ * Uncontrolled — the component takes over after mount. An empty array
6236
+ * is treated as "no teams". */
6237
+ defaultTeams?: string[][];
6205
6238
  /** Called whenever the name pool changes. */
6206
6239
  onNamesChange?: (names: string[]) => void;
6207
6240
  /** Called whenever teams are (re)generated. */
6208
6241
  onGenerate?: (teams: string[][]) => void;
6242
+ /** Called whenever the generated teams change for ANY reason — a
6243
+ * (re)generation, or invalidation to `null` when a player is removed,
6244
+ * the pool is cleared, or the team count changes. Unlike `onGenerate`,
6245
+ * this mirrors the full lifecycle of the on-screen split so hosts can
6246
+ * persist it and restore via `defaultTeams`. */
6247
+ onTeamsChange?: (teams: string[][] | null) => void;
6209
6248
  /** Called whenever the team count selection changes. */
6210
6249
  onTeamCountChange?: (count: number) => void;
6211
6250
  /** Localized / customized labels. */
@@ -6218,7 +6257,7 @@ interface TeamGeneratorProps {
6218
6257
  * CSV / TSV roster from a spreadsheet. Default `true`. */
6219
6258
  enableImport?: boolean;
6220
6259
  }
6221
- declare function TeamGenerator({ theme, darkMode, teamCountOptions, defaultTeamCount, minPlayersPerTeam, maxPlayers, defaultNames, onNamesChange, onGenerate, onTeamCountChange, labels, ariaLabel, className, enableImport, }: TeamGeneratorProps): react_jsx_runtime.JSX.Element;
6260
+ declare function TeamGenerator({ theme, darkMode, teamCountOptions, defaultTeamCount, minPlayersPerTeam, maxPlayers, defaultNames, defaultTeams, onNamesChange, onGenerate, onTeamsChange, onTeamCountChange, labels, ariaLabel, className, enableImport, }: TeamGeneratorProps): react_jsx_runtime.JSX.Element;
6222
6261
  declare namespace TeamGenerator {
6223
6262
  var displayName: string;
6224
6263
  }
@@ -5551,8 +5551,14 @@ interface RandomPlayerPickerProps {
5551
5551
  darkMode?: boolean;
5552
5552
  /** Wheel diameter in px. Default 380. */
5553
5553
  size?: number;
5554
- /** Called when a winner is locked in. */
5555
- onPick?: (winner: RandomPlayerParticipant, history: RandomPlayerParticipant[]) => void;
5554
+ /**
5555
+ * Called when a winner is locked in. `info.lastStanding` is true for
5556
+ * the automatic final pick of a draft — the survivor promoted when
5557
+ * pops drain the pool to one.
5558
+ */
5559
+ onPick?: (winner: RandomPlayerParticipant, history: RandomPlayerParticipant[], info?: {
5560
+ lastStanding: boolean;
5561
+ }) => void;
5556
5562
  /** Fired the moment a spin begins (before the animation). */
5557
5563
  onSpinStart?: () => void;
5558
5564
  /**
@@ -5586,14 +5592,37 @@ interface RandomPlayerPickerProps {
5586
5592
  renderWinner?: (winner: RandomPlayerParticipant) => ReactNode;
5587
5593
  /** Imperative handle. `spinRef.current?.()` triggers a spin. */
5588
5594
  spinRef?: MutableRefObject<(() => void) | null>;
5595
+ /**
5596
+ * Imperative reset handle. `resetRef.current?.()` restores the pool to
5597
+ * the full `participants` list and clears history/winner — the escape
5598
+ * hatch for consumers that hide the built-in controls
5599
+ * (`showControls={false}`) but still need a "reset draft" affordance
5600
+ * once draft-mode pops drain the pool.
5601
+ */
5602
+ resetRef?: MutableRefObject<(() => void) | null>;
5603
+ /**
5604
+ * Fired whenever the internal pool changes (mount, structural sync,
5605
+ * draft-mode pop, spin-again flush, reset). Draft-mode consumers use
5606
+ * this to mirror the live pool — e.g. to disable an external spin
5607
+ * trigger when the pool drains below 2 instead of presenting a dead
5608
+ * button, and to offer a reset once the draft completes.
5609
+ */
5610
+ onPoolChange?: (pool: RandomPlayerParticipant[]) => void;
5611
+ /**
5612
+ * Caption shown in place of `placeholderCaption` when draft-mode pops
5613
+ * drain the pool below 2 while the roster itself still has 2+ members.
5614
+ * Default: "Draft complete — reset to spin again".
5615
+ */
5616
+ draftCompleteCaption?: string;
5589
5617
  /**
5590
5618
  * Visual intensity preset. Default "normal".
5591
- * - "subtle" / "normal": current behavior — shorter spins, standard
5592
- * ease-out curve.
5593
- * - "dramatic": physics-like spin 2.5× duration, inertia-style
5594
- * curve so motion continues through the full duration with a slow
5595
- * winding-down tail, +12 extra revolutions, plus more
5596
- * particles/glow/shake on reveal.
5619
+ * - "subtle" / "normal": shorter spins, standard CSS ease-out curve.
5620
+ * - "dramatic": physics-driven spin — 2.5× duration, rAF-driven
5621
+ * exponential velocity decay (v ∝ e^(-4t)) that keeps the wheel
5622
+ * visibly winding down through the FULL duration and lands exactly
5623
+ * at the end, +12 extra revolutions, ticks fired on actual
5624
+ * wedge/row crossings, plus more particles/glow/shake on reveal.
5625
+ * Falls back to a 250ms CSS settle under prefers-reduced-motion.
5597
5626
  */
5598
5627
  intensity?: RandomPlayerIntensity;
5599
5628
  /**
@@ -5636,7 +5665,7 @@ interface RandomPlayerPickerProps {
5636
5665
  /** Accessible label for the wheel root. */
5637
5666
  ariaLabel?: string;
5638
5667
  }
5639
- declare function RandomPlayerPicker({ participants, mode, reelVisibleRows, reelRowHeight, theme, darkMode, size, onPick, onSpinStart, onTick, trigger, label, removeWinners, spinDuration, minRevolutions, maxRevolutions, showHistory, showControls, showStats, renderWinner, spinRef, intensity, enableHaptics, tickSoundUrl, winSoundUrl, soundVolume, placeholderMode, placeholderCaption, className, style, ariaLabel, }: RandomPlayerPickerProps): react_jsx_runtime.JSX.Element;
5668
+ declare function RandomPlayerPicker({ participants, mode, reelVisibleRows, reelRowHeight, theme, darkMode, size, onPick, onSpinStart, onTick, trigger, label, removeWinners, spinDuration, minRevolutions, maxRevolutions, showHistory, showControls, showStats, renderWinner, spinRef, resetRef, onPoolChange, draftCompleteCaption, intensity, enableHaptics, tickSoundUrl, winSoundUrl, soundVolume, placeholderMode, placeholderCaption, className, style, ariaLabel, }: RandomPlayerPickerProps): react_jsx_runtime.JSX.Element;
5640
5669
 
5641
5670
  type HeroBannerTransition = "fade" | "slide" | "zoom" | "blur" | "reveal" | "push";
5642
5671
  type HeroBannerImageFilter = "none" | "cinematic" | "dramatic" | "cool" | "warm" | "desaturated" | "noir" | "vibrant";
@@ -6202,10 +6231,20 @@ interface TeamGeneratorProps {
6202
6231
  maxPlayers?: number;
6203
6232
  /** Initial roster. Uncontrolled — the component manages names internally. */
6204
6233
  defaultNames?: string[];
6234
+ /** Initial generated teams (e.g. restored from a saved session).
6235
+ * Uncontrolled — the component takes over after mount. An empty array
6236
+ * is treated as "no teams". */
6237
+ defaultTeams?: string[][];
6205
6238
  /** Called whenever the name pool changes. */
6206
6239
  onNamesChange?: (names: string[]) => void;
6207
6240
  /** Called whenever teams are (re)generated. */
6208
6241
  onGenerate?: (teams: string[][]) => void;
6242
+ /** Called whenever the generated teams change for ANY reason — a
6243
+ * (re)generation, or invalidation to `null` when a player is removed,
6244
+ * the pool is cleared, or the team count changes. Unlike `onGenerate`,
6245
+ * this mirrors the full lifecycle of the on-screen split so hosts can
6246
+ * persist it and restore via `defaultTeams`. */
6247
+ onTeamsChange?: (teams: string[][] | null) => void;
6209
6248
  /** Called whenever the team count selection changes. */
6210
6249
  onTeamCountChange?: (count: number) => void;
6211
6250
  /** Localized / customized labels. */
@@ -6218,7 +6257,7 @@ interface TeamGeneratorProps {
6218
6257
  * CSV / TSV roster from a spreadsheet. Default `true`. */
6219
6258
  enableImport?: boolean;
6220
6259
  }
6221
- declare function TeamGenerator({ theme, darkMode, teamCountOptions, defaultTeamCount, minPlayersPerTeam, maxPlayers, defaultNames, onNamesChange, onGenerate, onTeamCountChange, labels, ariaLabel, className, enableImport, }: TeamGeneratorProps): react_jsx_runtime.JSX.Element;
6260
+ declare function TeamGenerator({ theme, darkMode, teamCountOptions, defaultTeamCount, minPlayersPerTeam, maxPlayers, defaultNames, defaultTeams, onNamesChange, onGenerate, onTeamsChange, onTeamCountChange, labels, ariaLabel, className, enableImport, }: TeamGeneratorProps): react_jsx_runtime.JSX.Element;
6222
6261
  declare namespace TeamGenerator {
6223
6262
  var displayName: string;
6224
6263
  }
package/dist/web/index.js CHANGED
@@ -33381,6 +33381,11 @@ var WEDGE_PALETTE = [
33381
33381
  ];
33382
33382
  var DEFAULT_EASING = "cubic-bezier(0.25, 0.46, 0.45, 0.94)";
33383
33383
  var DRAMATIC_EASING = "cubic-bezier(0.39, 0.575, 0.565, 1)";
33384
+ var PHYSICS_DECAY_K = 4;
33385
+ function physicsRampDown(t) {
33386
+ return (1 - Math.exp(-PHYSICS_DECAY_K * t)) / (1 - Math.exp(-PHYSICS_DECAY_K));
33387
+ }
33388
+ var REEL_LEAD_IN = 32;
33384
33389
  var INTENSITY_CONFIG = {
33385
33390
  subtle: {
33386
33391
  particles: 14,
@@ -33452,6 +33457,9 @@ function ReelStage({
33452
33457
  winner,
33453
33458
  spinning,
33454
33459
  reelY,
33460
+ displayY,
33461
+ cssTransition,
33462
+ tapeRef,
33455
33463
  reelVisibleRows,
33456
33464
  reelRowHeight,
33457
33465
  size,
@@ -33475,17 +33483,16 @@ function ReelStage({
33475
33483
  const stripHeight = reelVisibleRows * reelRowHeight;
33476
33484
  const centerOffsetPx = (reelVisibleRows - 1) / 2 * reelRowHeight;
33477
33485
  const P = pool.length;
33478
- const leadIn = 32;
33479
33486
  const tapeRows = Math.max(
33480
- reelVisibleRows + leadIn + 1,
33481
- Math.ceil((reelY + stripHeight) / reelRowHeight) + leadIn + 1
33487
+ reelVisibleRows + REEL_LEAD_IN + 1,
33488
+ Math.ceil((reelY + stripHeight) / reelRowHeight) + REEL_LEAD_IN + 1
33482
33489
  );
33483
33490
  const tape = Array.from({ length: tapeRows }, (_, i) => {
33484
- const poolIdx = P === 0 ? 0 : ((i - leadIn) % P + P) % P;
33491
+ const poolIdx = P === 0 ? 0 : ((i - REEL_LEAD_IN) % P + P) % P;
33485
33492
  return pool[poolIdx];
33486
33493
  });
33487
- const effectiveY = reelY + leadIn * reelRowHeight;
33488
- const transition = spinning ? `transform ${reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult)}ms ${intensityCfg.easing}` : "none";
33494
+ const effectiveY = displayY + REEL_LEAD_IN * reelRowHeight;
33495
+ const transition = spinning && cssTransition ? `transform ${reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult)}ms ${intensityCfg.easing}` : "none";
33489
33496
  return /* @__PURE__ */ (0, import_jsx_runtime116.jsxs)(
33490
33497
  "div",
33491
33498
  {
@@ -33530,6 +33537,7 @@ function ReelStage({
33530
33537
  ) : /* @__PURE__ */ (0, import_jsx_runtime116.jsx)(
33531
33538
  "div",
33532
33539
  {
33540
+ ref: tapeRef,
33533
33541
  "aria-hidden": "true",
33534
33542
  style: {
33535
33543
  position: "absolute",
@@ -33745,6 +33753,9 @@ function RandomPlayerPicker({
33745
33753
  showStats = true,
33746
33754
  renderWinner,
33747
33755
  spinRef,
33756
+ resetRef,
33757
+ onPoolChange,
33758
+ draftCompleteCaption = "Draft complete \u2014 reset to spin again",
33748
33759
  intensity = "normal",
33749
33760
  enableHaptics = false,
33750
33761
  tickSoundUrl,
@@ -33759,6 +33770,7 @@ function RandomPlayerPicker({
33759
33770
  const dark = useDarkMode(darkMode);
33760
33771
  const reducedMotion = usePrefersReducedMotion();
33761
33772
  const intensityCfg = INTENSITY_CONFIG[intensity];
33773
+ const physicsDriven = intensity === "dramatic" && !reducedMotion;
33762
33774
  const [pool, setPool] = (0, import_react73.useState)(participants);
33763
33775
  const [rotation, setRotation] = (0, import_react73.useState)(0);
33764
33776
  const [reelY, setReelY] = (0, import_react73.useState)(0);
@@ -33777,6 +33789,11 @@ function RandomPlayerPicker({
33777
33789
  const winAudioRef = (0, import_react73.useRef)(null);
33778
33790
  const containerRef = (0, import_react73.useRef)(null);
33779
33791
  const prevIdsRef = (0, import_react73.useRef)("");
33792
+ const wheelSvgRef = (0, import_react73.useRef)(null);
33793
+ const reelTapeRef = (0, import_react73.useRef)(null);
33794
+ const rafRef = (0, import_react73.useRef)(null);
33795
+ const visualRotationRef = (0, import_react73.useRef)(null);
33796
+ const visualReelYRef = (0, import_react73.useRef)(null);
33780
33797
  (0, import_react73.useEffect)(() => {
33781
33798
  if (typeof window === "undefined" || !tickSoundUrl) {
33782
33799
  tickAudioRef.current = null;
@@ -33830,6 +33847,12 @@ function RandomPlayerPicker({
33830
33847
  clearTimeout(popTimer.current);
33831
33848
  popTimer.current = null;
33832
33849
  }
33850
+ if (rafRef.current) {
33851
+ cancelAnimationFrame(rafRef.current);
33852
+ rafRef.current = null;
33853
+ }
33854
+ visualRotationRef.current = null;
33855
+ visualReelYRef.current = null;
33833
33856
  pendingPopIdxRef.current = null;
33834
33857
  setSpinning(false);
33835
33858
  setPool(participants);
@@ -33902,7 +33925,20 @@ function RandomPlayerPicker({
33902
33925
  }
33903
33926
  }
33904
33927
  const activeN = activePool.length;
33905
- if (activeN < 2) return;
33928
+ if (activeN < 2) {
33929
+ if (removeWinnersRef.current && activePool.length === 1 && history.length > 0) {
33930
+ const survivor = activePool[0];
33931
+ if (survivor) {
33932
+ const promoted = [survivor, ...history].slice(0, 20);
33933
+ setPool([]);
33934
+ setWinner(survivor);
33935
+ setRevealKey((k) => k + 1);
33936
+ setHistory(promoted);
33937
+ onPick?.(survivor, promoted, { lastStanding: true });
33938
+ }
33939
+ }
33940
+ return;
33941
+ }
33906
33942
  const activeAnglePerWedge = 2 * Math.PI / activeN;
33907
33943
  const activeRotation = rotation;
33908
33944
  const activeReelY = reelY;
@@ -33941,7 +33977,56 @@ function RandomPlayerPicker({
33941
33977
  } else {
33942
33978
  setRotation(finalRotation);
33943
33979
  }
33944
- if ((onTick || tickAudioRef.current) && !reducedMotion) {
33980
+ const fireTick = () => {
33981
+ onTick?.();
33982
+ const ta = tickAudioRef.current;
33983
+ if (ta) {
33984
+ try {
33985
+ ta.currentTime = 0;
33986
+ void ta.play().catch(() => {
33987
+ });
33988
+ } catch {
33989
+ }
33990
+ }
33991
+ };
33992
+ if (physicsDriven) {
33993
+ const from = mode === "reel" ? activeReelY : activeRotation;
33994
+ const to = mode === "reel" ? finalReelY : finalRotation;
33995
+ const crossUnit = mode === "reel" ? reelRowHeight : activeAnglePerWedge * 180 / Math.PI;
33996
+ if (mode === "reel") visualReelYRef.current = from;
33997
+ else visualRotationRef.current = from;
33998
+ let crossings = 0;
33999
+ const t0 = performance.now();
34000
+ const step = (now) => {
34001
+ const t = Math.min(1, (now - t0) / effectiveDuration);
34002
+ const value = from + (to - from) * physicsRampDown(t);
34003
+ if (mode === "reel") {
34004
+ visualReelYRef.current = value;
34005
+ const el = reelTapeRef.current;
34006
+ if (el) {
34007
+ el.style.transform = `translateY(${-(value + REEL_LEAD_IN * reelRowHeight)}px)`;
34008
+ }
34009
+ } else {
34010
+ visualRotationRef.current = value;
34011
+ const el = wheelSvgRef.current;
34012
+ if (el) el.style.transform = `rotate(${value}deg)`;
34013
+ }
34014
+ const crossed = Math.floor(Math.abs(value - from) / crossUnit);
34015
+ if (crossed > crossings) {
34016
+ crossings = crossed;
34017
+ fireTick();
34018
+ }
34019
+ if (t < 1) {
34020
+ rafRef.current = requestAnimationFrame(step);
34021
+ } else {
34022
+ rafRef.current = null;
34023
+ visualRotationRef.current = null;
34024
+ visualReelYRef.current = null;
34025
+ }
34026
+ };
34027
+ rafRef.current = requestAnimationFrame(step);
34028
+ }
34029
+ if (!physicsDriven && (onTick || tickAudioRef.current) && !reducedMotion) {
33945
34030
  const wedges = mode === "reel" ? Math.max(1, reelRevRows + targetRowOffset) : (() => {
33946
34031
  const totalDelta = deltaMod + revolutions * 360;
33947
34032
  const wedgeDeg = activeAnglePerWedge * 180 / Math.PI;
@@ -33959,16 +34044,7 @@ function RandomPlayerPicker({
33959
34044
  const eased = 1 - Math.pow(1 - progress, 3);
33960
34045
  const interval = startInterval + (endInterval - startInterval) * eased;
33961
34046
  tickInterval.current = setTimeout(() => {
33962
- onTick?.();
33963
- const ta = tickAudioRef.current;
33964
- if (ta) {
33965
- try {
33966
- ta.currentTime = 0;
33967
- void ta.play().catch(() => {
33968
- });
33969
- } catch {
33970
- }
33971
- }
34047
+ fireTick();
33972
34048
  elapsed += interval;
33973
34049
  schedule();
33974
34050
  }, interval);
@@ -34027,8 +34103,19 @@ function RandomPlayerPicker({
34027
34103
  popTimer.current = null;
34028
34104
  pendingPopIdxRef.current = null;
34029
34105
  if (!removeWinnersRef.current) return;
34030
- setPool((p) => p.filter((_, i) => i !== finalWinnerIdx));
34031
- setWinner(null);
34106
+ const survivors = activePool.filter((_, i) => i !== finalWinnerIdx);
34107
+ const survivor = survivors.length === 1 ? survivors[0] : void 0;
34108
+ if (survivor) {
34109
+ const promoted = [survivor, ...nextHistory].slice(0, 20);
34110
+ setPool([]);
34111
+ setWinner(survivor);
34112
+ setRevealKey((k) => k + 1);
34113
+ setHistory(promoted);
34114
+ onPick?.(survivor, promoted, { lastStanding: true });
34115
+ } else {
34116
+ setPool(survivors);
34117
+ setWinner(null);
34118
+ }
34032
34119
  setRotation(0);
34033
34120
  setReelY(0);
34034
34121
  }, 2800);
@@ -34051,6 +34138,7 @@ function RandomPlayerPicker({
34051
34138
  maxRevolutions,
34052
34139
  history,
34053
34140
  reducedMotion,
34141
+ physicsDriven,
34054
34142
  enableHaptics,
34055
34143
  intensityCfg.particles,
34056
34144
  intensityCfg.durationMult,
@@ -34062,12 +34150,20 @@ function RandomPlayerPicker({
34062
34150
  if (spinRef && spinRef.current === spin) spinRef.current = null;
34063
34151
  };
34064
34152
  }, [spin, spinRef]);
34153
+ const onPoolChangeRef = (0, import_react73.useRef)(onPoolChange);
34154
+ (0, import_react73.useEffect)(() => {
34155
+ onPoolChangeRef.current = onPoolChange;
34156
+ });
34157
+ (0, import_react73.useEffect)(() => {
34158
+ onPoolChangeRef.current?.(pool);
34159
+ }, [pool]);
34065
34160
  (0, import_react73.useEffect)(
34066
34161
  () => () => {
34067
34162
  if (winnerTimer.current) clearTimeout(winnerTimer.current);
34068
34163
  if (particleTimer.current) clearTimeout(particleTimer.current);
34069
34164
  if (popTimer.current) clearTimeout(popTimer.current);
34070
34165
  if (tickInterval.current) clearTimeout(tickInterval.current);
34166
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
34071
34167
  },
34072
34168
  []
34073
34169
  );
@@ -34097,6 +34193,12 @@ function RandomPlayerPicker({
34097
34193
  clearTimeout(popTimer.current);
34098
34194
  popTimer.current = null;
34099
34195
  }
34196
+ if (rafRef.current) {
34197
+ cancelAnimationFrame(rafRef.current);
34198
+ rafRef.current = null;
34199
+ }
34200
+ visualRotationRef.current = null;
34201
+ visualReelYRef.current = null;
34100
34202
  pendingPopIdxRef.current = null;
34101
34203
  setSpinning(false);
34102
34204
  setPool(participants);
@@ -34106,9 +34208,17 @@ function RandomPlayerPicker({
34106
34208
  setReelY(0);
34107
34209
  setParticles([]);
34108
34210
  }, [participants]);
34211
+ (0, import_react73.useEffect)(() => {
34212
+ if (resetRef) resetRef.current = reset;
34213
+ return () => {
34214
+ if (resetRef && resetRef.current === reset) resetRef.current = null;
34215
+ };
34216
+ }, [reset, resetRef]);
34109
34217
  const isIdle = n < 2;
34218
+ const drainedByDraft = isIdle && participants.length >= 2;
34219
+ const effectiveIdleCaption = drainedByDraft ? draftCompleteCaption : placeholderCaption;
34110
34220
  const placeholderDiscColor = dark ? "#2a3344" : "#e2e8f0";
34111
- if (isIdle && placeholderMode === "message") {
34221
+ if (isIdle && !drainedByDraft && placeholderMode === "message") {
34112
34222
  return /* @__PURE__ */ (0, import_jsx_runtime116.jsx)(
34113
34223
  "div",
34114
34224
  {
@@ -34130,6 +34240,8 @@ function RandomPlayerPicker({
34130
34240
  const shakeKeyframe = `rppShake_${intensity}`;
34131
34241
  const glowKeyframe = `rppGlow_${intensity}`;
34132
34242
  const pulseKeyframe = `rppPulse_${intensity}`;
34243
+ const displayRotation = visualRotationRef.current ?? rotation;
34244
+ const displayReelY = visualReelYRef.current ?? reelY;
34133
34245
  return /* @__PURE__ */ (0, import_jsx_runtime116.jsxs)(
34134
34246
  "div",
34135
34247
  {
@@ -34176,13 +34288,33 @@ function RandomPlayerPicker({
34176
34288
  [data-rpp-anim] { animation: none !important; }
34177
34289
  }
34178
34290
  ` }),
34179
- mode === "reel" ? /* @__PURE__ */ (0, import_jsx_runtime116.jsx)(
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__ */ (0, import_jsx_runtime116.jsx)(
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__ */ (0, import_jsx_runtime116.jsx)(
34180
34309
  ReelStage,
34181
34310
  {
34182
34311
  pool,
34183
34312
  winner,
34184
34313
  spinning,
34185
34314
  reelY,
34315
+ displayY: displayReelY,
34316
+ cssTransition: !physicsDriven,
34317
+ tapeRef: reelTapeRef,
34186
34318
  reelVisibleRows,
34187
34319
  reelRowHeight,
34188
34320
  size,
@@ -34191,7 +34323,7 @@ function RandomPlayerPicker({
34191
34323
  dark,
34192
34324
  isIdle,
34193
34325
  placeholderMode,
34194
- placeholderCaption,
34326
+ placeholderCaption: effectiveIdleCaption,
34195
34327
  placeholderDiscColor,
34196
34328
  spinDuration,
34197
34329
  intensityCfg,
@@ -34248,6 +34380,7 @@ function RandomPlayerPicker({
34248
34380
  /* @__PURE__ */ (0, import_jsx_runtime116.jsxs)(
34249
34381
  "svg",
34250
34382
  {
34383
+ ref: wheelSvgRef,
34251
34384
  width: size,
34252
34385
  height: size,
34253
34386
  "aria-hidden": "true",
@@ -34256,8 +34389,8 @@ function RandomPlayerPicker({
34256
34389
  top: 36,
34257
34390
  left: 0,
34258
34391
  transformOrigin: `${cx}px ${cy}px`,
34259
- transform: `rotate(${rotation}deg)`,
34260
- 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",
34261
34394
  filter: `drop-shadow(0 8px 28px ${dark ? "rgba(0,0,0,0.55)" : "rgba(0,0,0,0.15)"})`
34262
34395
  },
34263
34396
  children: [
@@ -34410,7 +34543,7 @@ function RandomPlayerPicker({
34410
34543
  pointerEvents: "none",
34411
34544
  zIndex: 3
34412
34545
  },
34413
- children: placeholderCaption
34546
+ children: effectiveIdleCaption
34414
34547
  }
34415
34548
  ),
34416
34549
  particles.map((p) => /* @__PURE__ */ (0, import_jsx_runtime116.jsx)(
@@ -38584,6 +38717,33 @@ function GameTimer({
38584
38717
  if (rafRef.current) cancelAnimationFrame(rafRef.current);
38585
38718
  };
38586
38719
  }, [running, tick]);
38720
+ (0, import_react76.useEffect)(() => {
38721
+ if (!running || mode !== "countdown") return;
38722
+ let timeoutId = null;
38723
+ const disarm = () => {
38724
+ if (timeoutId !== null) {
38725
+ clearTimeout(timeoutId);
38726
+ timeoutId = null;
38727
+ }
38728
+ };
38729
+ const arm = () => {
38730
+ disarm();
38731
+ const remainingUntilDone = durationMs - elapsedRef.current;
38732
+ if (remainingUntilDone > 0) {
38733
+ timeoutId = setTimeout(tick, remainingUntilDone + 50);
38734
+ }
38735
+ };
38736
+ const onVisibility = () => {
38737
+ if (document.hidden) arm();
38738
+ else disarm();
38739
+ };
38740
+ document.addEventListener("visibilitychange", onVisibility);
38741
+ if (document.hidden) arm();
38742
+ return () => {
38743
+ disarm();
38744
+ document.removeEventListener("visibilitychange", onVisibility);
38745
+ };
38746
+ }, [running, mode, durationMs, tick]);
38587
38747
  const start = (0, import_react76.useCallback)(() => {
38588
38748
  if (completed && mode === "countdown") return;
38589
38749
  setRunning(true);
@@ -39890,8 +40050,10 @@ function TeamGenerator({
39890
40050
  minPlayersPerTeam = 2,
39891
40051
  maxPlayers = Number.POSITIVE_INFINITY,
39892
40052
  defaultNames = [],
40053
+ defaultTeams,
39893
40054
  onNamesChange,
39894
40055
  onGenerate,
40056
+ onTeamsChange,
39895
40057
  onTeamCountChange,
39896
40058
  labels,
39897
40059
  ariaLabel = "Team generator",
@@ -39906,12 +40068,17 @@ function TeamGenerator({
39906
40068
  const [names, setNames] = (0, import_react78.useState)(defaultNames);
39907
40069
  const [input, setInput] = (0, import_react78.useState)("");
39908
40070
  const [teamCount, setTeamCount] = (0, import_react78.useState)(initialCount);
39909
- const [teams, setTeams] = (0, import_react78.useState)(null);
40071
+ const [teams, setTeams] = (0, import_react78.useState)(
40072
+ defaultTeams && defaultTeams.length > 0 ? defaultTeams : null
40073
+ );
39910
40074
  const inputRef = (0, import_react78.useRef)(null);
39911
40075
  const minNames = teamCount * minPlayersPerTeam;
39912
40076
  (0, import_react78.useEffect)(() => {
39913
40077
  onNamesChange?.(names);
39914
40078
  }, [names, onNamesChange]);
40079
+ (0, import_react78.useEffect)(() => {
40080
+ onTeamsChange?.(teams);
40081
+ }, [teams, onTeamsChange]);
39915
40082
  const addName = (0, import_react78.useCallback)(() => {
39916
40083
  const trimmed = input.trim();
39917
40084
  if (!trimmed) return;