@digilogiclabs/saas-factory-ui 2.4.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.
@@ -5586,14 +5586,37 @@ interface RandomPlayerPickerProps {
5586
5586
  renderWinner?: (winner: RandomPlayerParticipant) => ReactNode;
5587
5587
  /** Imperative handle. `spinRef.current?.()` triggers a spin. */
5588
5588
  spinRef?: MutableRefObject<(() => void) | null>;
5589
+ /**
5590
+ * Imperative reset handle. `resetRef.current?.()` restores the pool to
5591
+ * the full `participants` list and clears history/winner — the escape
5592
+ * hatch for consumers that hide the built-in controls
5593
+ * (`showControls={false}`) but still need a "reset draft" affordance
5594
+ * once draft-mode pops drain the pool.
5595
+ */
5596
+ resetRef?: MutableRefObject<(() => void) | null>;
5597
+ /**
5598
+ * Fired whenever the internal pool changes (mount, structural sync,
5599
+ * draft-mode pop, spin-again flush, reset). Draft-mode consumers use
5600
+ * this to mirror the live pool — e.g. to disable an external spin
5601
+ * trigger when the pool drains below 2 instead of presenting a dead
5602
+ * button, and to offer a reset once the draft completes.
5603
+ */
5604
+ onPoolChange?: (pool: RandomPlayerParticipant[]) => void;
5605
+ /**
5606
+ * Caption shown in place of `placeholderCaption` when draft-mode pops
5607
+ * drain the pool below 2 while the roster itself still has 2+ members.
5608
+ * Default: "Draft complete — reset to spin again".
5609
+ */
5610
+ draftCompleteCaption?: string;
5589
5611
  /**
5590
5612
  * 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.
5613
+ * - "subtle" / "normal": shorter spins, standard CSS ease-out curve.
5614
+ * - "dramatic": physics-driven spin — 2.5× duration, rAF-driven
5615
+ * exponential velocity decay (v ∝ e^(-4t)) that keeps the wheel
5616
+ * visibly winding down through the FULL duration and lands exactly
5617
+ * at the end, +12 extra revolutions, ticks fired on actual
5618
+ * wedge/row crossings, plus more particles/glow/shake on reveal.
5619
+ * Falls back to a 250ms CSS settle under prefers-reduced-motion.
5597
5620
  */
5598
5621
  intensity?: RandomPlayerIntensity;
5599
5622
  /**
@@ -5636,7 +5659,7 @@ interface RandomPlayerPickerProps {
5636
5659
  /** Accessible label for the wheel root. */
5637
5660
  ariaLabel?: string;
5638
5661
  }
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;
5662
+ 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
5663
 
5641
5664
  type HeroBannerTransition = "fade" | "slide" | "zoom" | "blur" | "reveal" | "push";
5642
5665
  type HeroBannerImageFilter = "none" | "cinematic" | "dramatic" | "cool" | "warm" | "desaturated" | "noir" | "vibrant";
@@ -6202,10 +6225,20 @@ interface TeamGeneratorProps {
6202
6225
  maxPlayers?: number;
6203
6226
  /** Initial roster. Uncontrolled — the component manages names internally. */
6204
6227
  defaultNames?: string[];
6228
+ /** Initial generated teams (e.g. restored from a saved session).
6229
+ * Uncontrolled — the component takes over after mount. An empty array
6230
+ * is treated as "no teams". */
6231
+ defaultTeams?: string[][];
6205
6232
  /** Called whenever the name pool changes. */
6206
6233
  onNamesChange?: (names: string[]) => void;
6207
6234
  /** Called whenever teams are (re)generated. */
6208
6235
  onGenerate?: (teams: string[][]) => void;
6236
+ /** Called whenever the generated teams change for ANY reason — a
6237
+ * (re)generation, or invalidation to `null` when a player is removed,
6238
+ * the pool is cleared, or the team count changes. Unlike `onGenerate`,
6239
+ * this mirrors the full lifecycle of the on-screen split so hosts can
6240
+ * persist it and restore via `defaultTeams`. */
6241
+ onTeamsChange?: (teams: string[][] | null) => void;
6209
6242
  /** Called whenever the team count selection changes. */
6210
6243
  onTeamCountChange?: (count: number) => void;
6211
6244
  /** Localized / customized labels. */
@@ -6218,7 +6251,7 @@ interface TeamGeneratorProps {
6218
6251
  * CSV / TSV roster from a spreadsheet. Default `true`. */
6219
6252
  enableImport?: boolean;
6220
6253
  }
6221
- declare function TeamGenerator({ theme, darkMode, teamCountOptions, defaultTeamCount, minPlayersPerTeam, maxPlayers, defaultNames, onNamesChange, onGenerate, onTeamCountChange, labels, ariaLabel, className, enableImport, }: TeamGeneratorProps): react_jsx_runtime.JSX.Element;
6254
+ 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
6255
  declare namespace TeamGenerator {
6223
6256
  var displayName: string;
6224
6257
  }
@@ -5586,14 +5586,37 @@ interface RandomPlayerPickerProps {
5586
5586
  renderWinner?: (winner: RandomPlayerParticipant) => ReactNode;
5587
5587
  /** Imperative handle. `spinRef.current?.()` triggers a spin. */
5588
5588
  spinRef?: MutableRefObject<(() => void) | null>;
5589
+ /**
5590
+ * Imperative reset handle. `resetRef.current?.()` restores the pool to
5591
+ * the full `participants` list and clears history/winner — the escape
5592
+ * hatch for consumers that hide the built-in controls
5593
+ * (`showControls={false}`) but still need a "reset draft" affordance
5594
+ * once draft-mode pops drain the pool.
5595
+ */
5596
+ resetRef?: MutableRefObject<(() => void) | null>;
5597
+ /**
5598
+ * Fired whenever the internal pool changes (mount, structural sync,
5599
+ * draft-mode pop, spin-again flush, reset). Draft-mode consumers use
5600
+ * this to mirror the live pool — e.g. to disable an external spin
5601
+ * trigger when the pool drains below 2 instead of presenting a dead
5602
+ * button, and to offer a reset once the draft completes.
5603
+ */
5604
+ onPoolChange?: (pool: RandomPlayerParticipant[]) => void;
5605
+ /**
5606
+ * Caption shown in place of `placeholderCaption` when draft-mode pops
5607
+ * drain the pool below 2 while the roster itself still has 2+ members.
5608
+ * Default: "Draft complete — reset to spin again".
5609
+ */
5610
+ draftCompleteCaption?: string;
5589
5611
  /**
5590
5612
  * 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.
5613
+ * - "subtle" / "normal": shorter spins, standard CSS ease-out curve.
5614
+ * - "dramatic": physics-driven spin — 2.5× duration, rAF-driven
5615
+ * exponential velocity decay (v ∝ e^(-4t)) that keeps the wheel
5616
+ * visibly winding down through the FULL duration and lands exactly
5617
+ * at the end, +12 extra revolutions, ticks fired on actual
5618
+ * wedge/row crossings, plus more particles/glow/shake on reveal.
5619
+ * Falls back to a 250ms CSS settle under prefers-reduced-motion.
5597
5620
  */
5598
5621
  intensity?: RandomPlayerIntensity;
5599
5622
  /**
@@ -5636,7 +5659,7 @@ interface RandomPlayerPickerProps {
5636
5659
  /** Accessible label for the wheel root. */
5637
5660
  ariaLabel?: string;
5638
5661
  }
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;
5662
+ 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
5663
 
5641
5664
  type HeroBannerTransition = "fade" | "slide" | "zoom" | "blur" | "reveal" | "push";
5642
5665
  type HeroBannerImageFilter = "none" | "cinematic" | "dramatic" | "cool" | "warm" | "desaturated" | "noir" | "vibrant";
@@ -6202,10 +6225,20 @@ interface TeamGeneratorProps {
6202
6225
  maxPlayers?: number;
6203
6226
  /** Initial roster. Uncontrolled — the component manages names internally. */
6204
6227
  defaultNames?: string[];
6228
+ /** Initial generated teams (e.g. restored from a saved session).
6229
+ * Uncontrolled — the component takes over after mount. An empty array
6230
+ * is treated as "no teams". */
6231
+ defaultTeams?: string[][];
6205
6232
  /** Called whenever the name pool changes. */
6206
6233
  onNamesChange?: (names: string[]) => void;
6207
6234
  /** Called whenever teams are (re)generated. */
6208
6235
  onGenerate?: (teams: string[][]) => void;
6236
+ /** Called whenever the generated teams change for ANY reason — a
6237
+ * (re)generation, or invalidation to `null` when a player is removed,
6238
+ * the pool is cleared, or the team count changes. Unlike `onGenerate`,
6239
+ * this mirrors the full lifecycle of the on-screen split so hosts can
6240
+ * persist it and restore via `defaultTeams`. */
6241
+ onTeamsChange?: (teams: string[][] | null) => void;
6209
6242
  /** Called whenever the team count selection changes. */
6210
6243
  onTeamCountChange?: (count: number) => void;
6211
6244
  /** Localized / customized labels. */
@@ -6218,7 +6251,7 @@ interface TeamGeneratorProps {
6218
6251
  * CSV / TSV roster from a spreadsheet. Default `true`. */
6219
6252
  enableImport?: boolean;
6220
6253
  }
6221
- declare function TeamGenerator({ theme, darkMode, teamCountOptions, defaultTeamCount, minPlayersPerTeam, maxPlayers, defaultNames, onNamesChange, onGenerate, onTeamCountChange, labels, ariaLabel, className, enableImport, }: TeamGeneratorProps): react_jsx_runtime.JSX.Element;
6254
+ 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
6255
  declare namespace TeamGenerator {
6223
6256
  var displayName: string;
6224
6257
  }
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);
@@ -33941,7 +33964,56 @@ function RandomPlayerPicker({
33941
33964
  } else {
33942
33965
  setRotation(finalRotation);
33943
33966
  }
33944
- if ((onTick || tickAudioRef.current) && !reducedMotion) {
33967
+ const fireTick = () => {
33968
+ onTick?.();
33969
+ const ta = tickAudioRef.current;
33970
+ if (ta) {
33971
+ try {
33972
+ ta.currentTime = 0;
33973
+ void ta.play().catch(() => {
33974
+ });
33975
+ } catch {
33976
+ }
33977
+ }
33978
+ };
33979
+ if (physicsDriven) {
33980
+ const from = mode === "reel" ? activeReelY : activeRotation;
33981
+ const to = mode === "reel" ? finalReelY : finalRotation;
33982
+ const crossUnit = mode === "reel" ? reelRowHeight : activeAnglePerWedge * 180 / Math.PI;
33983
+ if (mode === "reel") visualReelYRef.current = from;
33984
+ else visualRotationRef.current = from;
33985
+ let crossings = 0;
33986
+ const t0 = performance.now();
33987
+ const step = (now) => {
33988
+ const t = Math.min(1, (now - t0) / effectiveDuration);
33989
+ const value = from + (to - from) * physicsRampDown(t);
33990
+ if (mode === "reel") {
33991
+ visualReelYRef.current = value;
33992
+ const el = reelTapeRef.current;
33993
+ if (el) {
33994
+ el.style.transform = `translateY(${-(value + REEL_LEAD_IN * reelRowHeight)}px)`;
33995
+ }
33996
+ } else {
33997
+ visualRotationRef.current = value;
33998
+ const el = wheelSvgRef.current;
33999
+ if (el) el.style.transform = `rotate(${value}deg)`;
34000
+ }
34001
+ const crossed = Math.floor(Math.abs(value - from) / crossUnit);
34002
+ if (crossed > crossings) {
34003
+ crossings = crossed;
34004
+ fireTick();
34005
+ }
34006
+ if (t < 1) {
34007
+ rafRef.current = requestAnimationFrame(step);
34008
+ } else {
34009
+ rafRef.current = null;
34010
+ visualRotationRef.current = null;
34011
+ visualReelYRef.current = null;
34012
+ }
34013
+ };
34014
+ rafRef.current = requestAnimationFrame(step);
34015
+ }
34016
+ if (!physicsDriven && (onTick || tickAudioRef.current) && !reducedMotion) {
33945
34017
  const wedges = mode === "reel" ? Math.max(1, reelRevRows + targetRowOffset) : (() => {
33946
34018
  const totalDelta = deltaMod + revolutions * 360;
33947
34019
  const wedgeDeg = activeAnglePerWedge * 180 / Math.PI;
@@ -33959,16 +34031,7 @@ function RandomPlayerPicker({
33959
34031
  const eased = 1 - Math.pow(1 - progress, 3);
33960
34032
  const interval = startInterval + (endInterval - startInterval) * eased;
33961
34033
  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
- }
34034
+ fireTick();
33972
34035
  elapsed += interval;
33973
34036
  schedule();
33974
34037
  }, interval);
@@ -34028,7 +34091,7 @@ function RandomPlayerPicker({
34028
34091
  pendingPopIdxRef.current = null;
34029
34092
  if (!removeWinnersRef.current) return;
34030
34093
  setPool((p) => p.filter((_, i) => i !== finalWinnerIdx));
34031
- setWinner(null);
34094
+ if (activePool.length - 1 >= 2) setWinner(null);
34032
34095
  setRotation(0);
34033
34096
  setReelY(0);
34034
34097
  }, 2800);
@@ -34051,6 +34114,7 @@ function RandomPlayerPicker({
34051
34114
  maxRevolutions,
34052
34115
  history,
34053
34116
  reducedMotion,
34117
+ physicsDriven,
34054
34118
  enableHaptics,
34055
34119
  intensityCfg.particles,
34056
34120
  intensityCfg.durationMult,
@@ -34062,12 +34126,20 @@ function RandomPlayerPicker({
34062
34126
  if (spinRef && spinRef.current === spin) spinRef.current = null;
34063
34127
  };
34064
34128
  }, [spin, spinRef]);
34129
+ const onPoolChangeRef = (0, import_react73.useRef)(onPoolChange);
34130
+ (0, import_react73.useEffect)(() => {
34131
+ onPoolChangeRef.current = onPoolChange;
34132
+ });
34133
+ (0, import_react73.useEffect)(() => {
34134
+ onPoolChangeRef.current?.(pool);
34135
+ }, [pool]);
34065
34136
  (0, import_react73.useEffect)(
34066
34137
  () => () => {
34067
34138
  if (winnerTimer.current) clearTimeout(winnerTimer.current);
34068
34139
  if (particleTimer.current) clearTimeout(particleTimer.current);
34069
34140
  if (popTimer.current) clearTimeout(popTimer.current);
34070
34141
  if (tickInterval.current) clearTimeout(tickInterval.current);
34142
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
34071
34143
  },
34072
34144
  []
34073
34145
  );
@@ -34097,6 +34169,12 @@ function RandomPlayerPicker({
34097
34169
  clearTimeout(popTimer.current);
34098
34170
  popTimer.current = null;
34099
34171
  }
34172
+ if (rafRef.current) {
34173
+ cancelAnimationFrame(rafRef.current);
34174
+ rafRef.current = null;
34175
+ }
34176
+ visualRotationRef.current = null;
34177
+ visualReelYRef.current = null;
34100
34178
  pendingPopIdxRef.current = null;
34101
34179
  setSpinning(false);
34102
34180
  setPool(participants);
@@ -34106,9 +34184,17 @@ function RandomPlayerPicker({
34106
34184
  setReelY(0);
34107
34185
  setParticles([]);
34108
34186
  }, [participants]);
34187
+ (0, import_react73.useEffect)(() => {
34188
+ if (resetRef) resetRef.current = reset;
34189
+ return () => {
34190
+ if (resetRef && resetRef.current === reset) resetRef.current = null;
34191
+ };
34192
+ }, [reset, resetRef]);
34109
34193
  const isIdle = n < 2;
34194
+ const drainedByDraft = isIdle && participants.length >= 2;
34195
+ const effectiveIdleCaption = drainedByDraft ? draftCompleteCaption : placeholderCaption;
34110
34196
  const placeholderDiscColor = dark ? "#2a3344" : "#e2e8f0";
34111
- if (isIdle && placeholderMode === "message") {
34197
+ if (isIdle && !drainedByDraft && placeholderMode === "message") {
34112
34198
  return /* @__PURE__ */ (0, import_jsx_runtime116.jsx)(
34113
34199
  "div",
34114
34200
  {
@@ -34130,6 +34216,8 @@ function RandomPlayerPicker({
34130
34216
  const shakeKeyframe = `rppShake_${intensity}`;
34131
34217
  const glowKeyframe = `rppGlow_${intensity}`;
34132
34218
  const pulseKeyframe = `rppPulse_${intensity}`;
34219
+ const displayRotation = visualRotationRef.current ?? rotation;
34220
+ const displayReelY = visualReelYRef.current ?? reelY;
34133
34221
  return /* @__PURE__ */ (0, import_jsx_runtime116.jsxs)(
34134
34222
  "div",
34135
34223
  {
@@ -34176,13 +34264,33 @@ function RandomPlayerPicker({
34176
34264
  [data-rpp-anim] { animation: none !important; }
34177
34265
  }
34178
34266
  ` }),
34179
- mode === "reel" ? /* @__PURE__ */ (0, import_jsx_runtime116.jsx)(
34267
+ isIdle && placeholderMode === "message" ? (
34268
+ /* Drained draft in "message" mode — compact box in the wheel's
34269
+ place; the winner card / stats / controls / history below stay
34270
+ mounted so the endgame isn't a dead end. */
34271
+ /* @__PURE__ */ (0, import_jsx_runtime116.jsx)(
34272
+ "div",
34273
+ {
34274
+ style: {
34275
+ padding: "2rem 1rem",
34276
+ textAlign: "center",
34277
+ color: c.dim,
34278
+ borderRadius: 12,
34279
+ border: `1px dashed ${c.border}`
34280
+ },
34281
+ children: effectiveIdleCaption
34282
+ }
34283
+ )
34284
+ ) : mode === "reel" ? /* @__PURE__ */ (0, import_jsx_runtime116.jsx)(
34180
34285
  ReelStage,
34181
34286
  {
34182
34287
  pool,
34183
34288
  winner,
34184
34289
  spinning,
34185
34290
  reelY,
34291
+ displayY: displayReelY,
34292
+ cssTransition: !physicsDriven,
34293
+ tapeRef: reelTapeRef,
34186
34294
  reelVisibleRows,
34187
34295
  reelRowHeight,
34188
34296
  size,
@@ -34191,7 +34299,7 @@ function RandomPlayerPicker({
34191
34299
  dark,
34192
34300
  isIdle,
34193
34301
  placeholderMode,
34194
- placeholderCaption,
34302
+ placeholderCaption: effectiveIdleCaption,
34195
34303
  placeholderDiscColor,
34196
34304
  spinDuration,
34197
34305
  intensityCfg,
@@ -34248,6 +34356,7 @@ function RandomPlayerPicker({
34248
34356
  /* @__PURE__ */ (0, import_jsx_runtime116.jsxs)(
34249
34357
  "svg",
34250
34358
  {
34359
+ ref: wheelSvgRef,
34251
34360
  width: size,
34252
34361
  height: size,
34253
34362
  "aria-hidden": "true",
@@ -34256,8 +34365,8 @@ function RandomPlayerPicker({
34256
34365
  top: 36,
34257
34366
  left: 0,
34258
34367
  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",
34368
+ transform: `rotate(${displayRotation}deg)`,
34369
+ transition: spinning && !physicsDriven ? `transform ${reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult)}ms ${intensityCfg.easing}` : "none",
34261
34370
  filter: `drop-shadow(0 8px 28px ${dark ? "rgba(0,0,0,0.55)" : "rgba(0,0,0,0.15)"})`
34262
34371
  },
34263
34372
  children: [
@@ -34410,7 +34519,7 @@ function RandomPlayerPicker({
34410
34519
  pointerEvents: "none",
34411
34520
  zIndex: 3
34412
34521
  },
34413
- children: placeholderCaption
34522
+ children: effectiveIdleCaption
34414
34523
  }
34415
34524
  ),
34416
34525
  particles.map((p) => /* @__PURE__ */ (0, import_jsx_runtime116.jsx)(
@@ -39890,8 +39999,10 @@ function TeamGenerator({
39890
39999
  minPlayersPerTeam = 2,
39891
40000
  maxPlayers = Number.POSITIVE_INFINITY,
39892
40001
  defaultNames = [],
40002
+ defaultTeams,
39893
40003
  onNamesChange,
39894
40004
  onGenerate,
40005
+ onTeamsChange,
39895
40006
  onTeamCountChange,
39896
40007
  labels,
39897
40008
  ariaLabel = "Team generator",
@@ -39906,12 +40017,17 @@ function TeamGenerator({
39906
40017
  const [names, setNames] = (0, import_react78.useState)(defaultNames);
39907
40018
  const [input, setInput] = (0, import_react78.useState)("");
39908
40019
  const [teamCount, setTeamCount] = (0, import_react78.useState)(initialCount);
39909
- const [teams, setTeams] = (0, import_react78.useState)(null);
40020
+ const [teams, setTeams] = (0, import_react78.useState)(
40021
+ defaultTeams && defaultTeams.length > 0 ? defaultTeams : null
40022
+ );
39910
40023
  const inputRef = (0, import_react78.useRef)(null);
39911
40024
  const minNames = teamCount * minPlayersPerTeam;
39912
40025
  (0, import_react78.useEffect)(() => {
39913
40026
  onNamesChange?.(names);
39914
40027
  }, [names, onNamesChange]);
40028
+ (0, import_react78.useEffect)(() => {
40029
+ onTeamsChange?.(teams);
40030
+ }, [teams, onTeamsChange]);
39915
40031
  const addName = (0, import_react78.useCallback)(() => {
39916
40032
  const trimmed = input.trim();
39917
40033
  if (!trimmed) return;