@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.
package/dist/index.d.mts CHANGED
@@ -5050,14 +5050,37 @@ interface RandomPlayerPickerProps {
5050
5050
  renderWinner?: (winner: RandomPlayerParticipant) => ReactNode;
5051
5051
  /** Imperative handle. `spinRef.current?.()` triggers a spin. */
5052
5052
  spinRef?: MutableRefObject<(() => void) | null>;
5053
+ /**
5054
+ * Imperative reset handle. `resetRef.current?.()` restores the pool to
5055
+ * the full `participants` list and clears history/winner — the escape
5056
+ * hatch for consumers that hide the built-in controls
5057
+ * (`showControls={false}`) but still need a "reset draft" affordance
5058
+ * once draft-mode pops drain the pool.
5059
+ */
5060
+ resetRef?: MutableRefObject<(() => void) | null>;
5061
+ /**
5062
+ * Fired whenever the internal pool changes (mount, structural sync,
5063
+ * draft-mode pop, spin-again flush, reset). Draft-mode consumers use
5064
+ * this to mirror the live pool — e.g. to disable an external spin
5065
+ * trigger when the pool drains below 2 instead of presenting a dead
5066
+ * button, and to offer a reset once the draft completes.
5067
+ */
5068
+ onPoolChange?: (pool: RandomPlayerParticipant[]) => void;
5069
+ /**
5070
+ * Caption shown in place of `placeholderCaption` when draft-mode pops
5071
+ * drain the pool below 2 while the roster itself still has 2+ members.
5072
+ * Default: "Draft complete — reset to spin again".
5073
+ */
5074
+ draftCompleteCaption?: string;
5053
5075
  /**
5054
5076
  * Visual intensity preset. Default "normal".
5055
- * - "subtle" / "normal": current behavior — shorter spins, standard
5056
- * ease-out curve.
5057
- * - "dramatic": physics-like spin 2.5× duration, inertia-style
5058
- * curve so motion continues through the full duration with a slow
5059
- * winding-down tail, +12 extra revolutions, plus more
5060
- * particles/glow/shake on reveal.
5077
+ * - "subtle" / "normal": shorter spins, standard CSS ease-out curve.
5078
+ * - "dramatic": physics-driven spin — 2.5× duration, rAF-driven
5079
+ * exponential velocity decay (v ∝ e^(-4t)) that keeps the wheel
5080
+ * visibly winding down through the FULL duration and lands exactly
5081
+ * at the end, +12 extra revolutions, ticks fired on actual
5082
+ * wedge/row crossings, plus more particles/glow/shake on reveal.
5083
+ * Falls back to a 250ms CSS settle under prefers-reduced-motion.
5061
5084
  */
5062
5085
  intensity?: RandomPlayerIntensity;
5063
5086
  /**
@@ -5100,7 +5123,7 @@ interface RandomPlayerPickerProps {
5100
5123
  /** Accessible label for the wheel root. */
5101
5124
  ariaLabel?: string;
5102
5125
  }
5103
- 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;
5126
+ 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;
5104
5127
 
5105
5128
  type HeroBannerTransition = "fade" | "slide" | "zoom" | "blur" | "reveal" | "push";
5106
5129
  type HeroBannerImageFilter = "none" | "cinematic" | "dramatic" | "cool" | "warm" | "desaturated" | "noir" | "vibrant";
@@ -5666,10 +5689,20 @@ interface TeamGeneratorProps {
5666
5689
  maxPlayers?: number;
5667
5690
  /** Initial roster. Uncontrolled — the component manages names internally. */
5668
5691
  defaultNames?: string[];
5692
+ /** Initial generated teams (e.g. restored from a saved session).
5693
+ * Uncontrolled — the component takes over after mount. An empty array
5694
+ * is treated as "no teams". */
5695
+ defaultTeams?: string[][];
5669
5696
  /** Called whenever the name pool changes. */
5670
5697
  onNamesChange?: (names: string[]) => void;
5671
5698
  /** Called whenever teams are (re)generated. */
5672
5699
  onGenerate?: (teams: string[][]) => void;
5700
+ /** Called whenever the generated teams change for ANY reason — a
5701
+ * (re)generation, or invalidation to `null` when a player is removed,
5702
+ * the pool is cleared, or the team count changes. Unlike `onGenerate`,
5703
+ * this mirrors the full lifecycle of the on-screen split so hosts can
5704
+ * persist it and restore via `defaultTeams`. */
5705
+ onTeamsChange?: (teams: string[][] | null) => void;
5673
5706
  /** Called whenever the team count selection changes. */
5674
5707
  onTeamCountChange?: (count: number) => void;
5675
5708
  /** Localized / customized labels. */
@@ -5682,7 +5715,7 @@ interface TeamGeneratorProps {
5682
5715
  * CSV / TSV roster from a spreadsheet. Default `true`. */
5683
5716
  enableImport?: boolean;
5684
5717
  }
5685
- declare function TeamGenerator({ theme, darkMode, teamCountOptions, defaultTeamCount, minPlayersPerTeam, maxPlayers, defaultNames, onNamesChange, onGenerate, onTeamCountChange, labels, ariaLabel, className, enableImport, }: TeamGeneratorProps): react_jsx_runtime.JSX.Element;
5718
+ 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;
5686
5719
  declare namespace TeamGenerator {
5687
5720
  var displayName: string;
5688
5721
  }
package/dist/index.d.ts CHANGED
@@ -5050,14 +5050,37 @@ interface RandomPlayerPickerProps {
5050
5050
  renderWinner?: (winner: RandomPlayerParticipant) => ReactNode;
5051
5051
  /** Imperative handle. `spinRef.current?.()` triggers a spin. */
5052
5052
  spinRef?: MutableRefObject<(() => void) | null>;
5053
+ /**
5054
+ * Imperative reset handle. `resetRef.current?.()` restores the pool to
5055
+ * the full `participants` list and clears history/winner — the escape
5056
+ * hatch for consumers that hide the built-in controls
5057
+ * (`showControls={false}`) but still need a "reset draft" affordance
5058
+ * once draft-mode pops drain the pool.
5059
+ */
5060
+ resetRef?: MutableRefObject<(() => void) | null>;
5061
+ /**
5062
+ * Fired whenever the internal pool changes (mount, structural sync,
5063
+ * draft-mode pop, spin-again flush, reset). Draft-mode consumers use
5064
+ * this to mirror the live pool — e.g. to disable an external spin
5065
+ * trigger when the pool drains below 2 instead of presenting a dead
5066
+ * button, and to offer a reset once the draft completes.
5067
+ */
5068
+ onPoolChange?: (pool: RandomPlayerParticipant[]) => void;
5069
+ /**
5070
+ * Caption shown in place of `placeholderCaption` when draft-mode pops
5071
+ * drain the pool below 2 while the roster itself still has 2+ members.
5072
+ * Default: "Draft complete — reset to spin again".
5073
+ */
5074
+ draftCompleteCaption?: string;
5053
5075
  /**
5054
5076
  * Visual intensity preset. Default "normal".
5055
- * - "subtle" / "normal": current behavior — shorter spins, standard
5056
- * ease-out curve.
5057
- * - "dramatic": physics-like spin 2.5× duration, inertia-style
5058
- * curve so motion continues through the full duration with a slow
5059
- * winding-down tail, +12 extra revolutions, plus more
5060
- * particles/glow/shake on reveal.
5077
+ * - "subtle" / "normal": shorter spins, standard CSS ease-out curve.
5078
+ * - "dramatic": physics-driven spin — 2.5× duration, rAF-driven
5079
+ * exponential velocity decay (v ∝ e^(-4t)) that keeps the wheel
5080
+ * visibly winding down through the FULL duration and lands exactly
5081
+ * at the end, +12 extra revolutions, ticks fired on actual
5082
+ * wedge/row crossings, plus more particles/glow/shake on reveal.
5083
+ * Falls back to a 250ms CSS settle under prefers-reduced-motion.
5061
5084
  */
5062
5085
  intensity?: RandomPlayerIntensity;
5063
5086
  /**
@@ -5100,7 +5123,7 @@ interface RandomPlayerPickerProps {
5100
5123
  /** Accessible label for the wheel root. */
5101
5124
  ariaLabel?: string;
5102
5125
  }
5103
- 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;
5126
+ 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;
5104
5127
 
5105
5128
  type HeroBannerTransition = "fade" | "slide" | "zoom" | "blur" | "reveal" | "push";
5106
5129
  type HeroBannerImageFilter = "none" | "cinematic" | "dramatic" | "cool" | "warm" | "desaturated" | "noir" | "vibrant";
@@ -5666,10 +5689,20 @@ interface TeamGeneratorProps {
5666
5689
  maxPlayers?: number;
5667
5690
  /** Initial roster. Uncontrolled — the component manages names internally. */
5668
5691
  defaultNames?: string[];
5692
+ /** Initial generated teams (e.g. restored from a saved session).
5693
+ * Uncontrolled — the component takes over after mount. An empty array
5694
+ * is treated as "no teams". */
5695
+ defaultTeams?: string[][];
5669
5696
  /** Called whenever the name pool changes. */
5670
5697
  onNamesChange?: (names: string[]) => void;
5671
5698
  /** Called whenever teams are (re)generated. */
5672
5699
  onGenerate?: (teams: string[][]) => void;
5700
+ /** Called whenever the generated teams change for ANY reason — a
5701
+ * (re)generation, or invalidation to `null` when a player is removed,
5702
+ * the pool is cleared, or the team count changes. Unlike `onGenerate`,
5703
+ * this mirrors the full lifecycle of the on-screen split so hosts can
5704
+ * persist it and restore via `defaultTeams`. */
5705
+ onTeamsChange?: (teams: string[][] | null) => void;
5673
5706
  /** Called whenever the team count selection changes. */
5674
5707
  onTeamCountChange?: (count: number) => void;
5675
5708
  /** Localized / customized labels. */
@@ -5682,7 +5715,7 @@ interface TeamGeneratorProps {
5682
5715
  * CSV / TSV roster from a spreadsheet. Default `true`. */
5683
5716
  enableImport?: boolean;
5684
5717
  }
5685
- declare function TeamGenerator({ theme, darkMode, teamCountOptions, defaultTeamCount, minPlayersPerTeam, maxPlayers, defaultNames, onNamesChange, onGenerate, onTeamCountChange, labels, ariaLabel, className, enableImport, }: TeamGeneratorProps): react_jsx_runtime.JSX.Element;
5718
+ 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;
5686
5719
  declare namespace TeamGenerator {
5687
5720
  var displayName: string;
5688
5721
  }
package/dist/index.js CHANGED
@@ -33535,6 +33535,11 @@ var WEDGE_PALETTE = [
33535
33535
  ];
33536
33536
  var DEFAULT_EASING = "cubic-bezier(0.25, 0.46, 0.45, 0.94)";
33537
33537
  var DRAMATIC_EASING = "cubic-bezier(0.39, 0.575, 0.565, 1)";
33538
+ var PHYSICS_DECAY_K = 4;
33539
+ function physicsRampDown(t) {
33540
+ return (1 - Math.exp(-PHYSICS_DECAY_K * t)) / (1 - Math.exp(-PHYSICS_DECAY_K));
33541
+ }
33542
+ var REEL_LEAD_IN = 32;
33538
33543
  var INTENSITY_CONFIG = {
33539
33544
  subtle: {
33540
33545
  particles: 14,
@@ -33606,6 +33611,9 @@ function ReelStage({
33606
33611
  winner,
33607
33612
  spinning,
33608
33613
  reelY,
33614
+ displayY,
33615
+ cssTransition,
33616
+ tapeRef,
33609
33617
  reelVisibleRows,
33610
33618
  reelRowHeight,
33611
33619
  size,
@@ -33629,17 +33637,16 @@ function ReelStage({
33629
33637
  const stripHeight = reelVisibleRows * reelRowHeight;
33630
33638
  const centerOffsetPx = (reelVisibleRows - 1) / 2 * reelRowHeight;
33631
33639
  const P = pool.length;
33632
- const leadIn = 32;
33633
33640
  const tapeRows = Math.max(
33634
- reelVisibleRows + leadIn + 1,
33635
- Math.ceil((reelY + stripHeight) / reelRowHeight) + leadIn + 1
33641
+ reelVisibleRows + REEL_LEAD_IN + 1,
33642
+ Math.ceil((reelY + stripHeight) / reelRowHeight) + REEL_LEAD_IN + 1
33636
33643
  );
33637
33644
  const tape = Array.from({ length: tapeRows }, (_, i) => {
33638
- const poolIdx = P === 0 ? 0 : ((i - leadIn) % P + P) % P;
33645
+ const poolIdx = P === 0 ? 0 : ((i - REEL_LEAD_IN) % P + P) % P;
33639
33646
  return pool[poolIdx];
33640
33647
  });
33641
- const effectiveY = reelY + leadIn * reelRowHeight;
33642
- const transition = spinning ? `transform ${reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult)}ms ${intensityCfg.easing}` : "none";
33648
+ const effectiveY = displayY + REEL_LEAD_IN * reelRowHeight;
33649
+ const transition = spinning && cssTransition ? `transform ${reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult)}ms ${intensityCfg.easing}` : "none";
33643
33650
  return /* @__PURE__ */ (0, import_jsx_runtime117.jsxs)(
33644
33651
  "div",
33645
33652
  {
@@ -33684,6 +33691,7 @@ function ReelStage({
33684
33691
  ) : /* @__PURE__ */ (0, import_jsx_runtime117.jsx)(
33685
33692
  "div",
33686
33693
  {
33694
+ ref: tapeRef,
33687
33695
  "aria-hidden": "true",
33688
33696
  style: {
33689
33697
  position: "absolute",
@@ -33899,6 +33907,9 @@ function RandomPlayerPicker({
33899
33907
  showStats = true,
33900
33908
  renderWinner,
33901
33909
  spinRef,
33910
+ resetRef,
33911
+ onPoolChange,
33912
+ draftCompleteCaption = "Draft complete \u2014 reset to spin again",
33902
33913
  intensity = "normal",
33903
33914
  enableHaptics = false,
33904
33915
  tickSoundUrl,
@@ -33913,6 +33924,7 @@ function RandomPlayerPicker({
33913
33924
  const dark = useDarkMode(darkMode);
33914
33925
  const reducedMotion = usePrefersReducedMotion();
33915
33926
  const intensityCfg = INTENSITY_CONFIG[intensity];
33927
+ const physicsDriven = intensity === "dramatic" && !reducedMotion;
33916
33928
  const [pool, setPool] = (0, import_react71.useState)(participants);
33917
33929
  const [rotation, setRotation] = (0, import_react71.useState)(0);
33918
33930
  const [reelY, setReelY] = (0, import_react71.useState)(0);
@@ -33931,6 +33943,11 @@ function RandomPlayerPicker({
33931
33943
  const winAudioRef = (0, import_react71.useRef)(null);
33932
33944
  const containerRef = (0, import_react71.useRef)(null);
33933
33945
  const prevIdsRef = (0, import_react71.useRef)("");
33946
+ const wheelSvgRef = (0, import_react71.useRef)(null);
33947
+ const reelTapeRef = (0, import_react71.useRef)(null);
33948
+ const rafRef = (0, import_react71.useRef)(null);
33949
+ const visualRotationRef = (0, import_react71.useRef)(null);
33950
+ const visualReelYRef = (0, import_react71.useRef)(null);
33934
33951
  (0, import_react71.useEffect)(() => {
33935
33952
  if (typeof window === "undefined" || !tickSoundUrl) {
33936
33953
  tickAudioRef.current = null;
@@ -33984,6 +34001,12 @@ function RandomPlayerPicker({
33984
34001
  clearTimeout(popTimer.current);
33985
34002
  popTimer.current = null;
33986
34003
  }
34004
+ if (rafRef.current) {
34005
+ cancelAnimationFrame(rafRef.current);
34006
+ rafRef.current = null;
34007
+ }
34008
+ visualRotationRef.current = null;
34009
+ visualReelYRef.current = null;
33987
34010
  pendingPopIdxRef.current = null;
33988
34011
  setSpinning(false);
33989
34012
  setPool(participants);
@@ -34095,7 +34118,56 @@ function RandomPlayerPicker({
34095
34118
  } else {
34096
34119
  setRotation(finalRotation);
34097
34120
  }
34098
- if ((onTick || tickAudioRef.current) && !reducedMotion) {
34121
+ const fireTick = () => {
34122
+ onTick?.();
34123
+ const ta = tickAudioRef.current;
34124
+ if (ta) {
34125
+ try {
34126
+ ta.currentTime = 0;
34127
+ void ta.play().catch(() => {
34128
+ });
34129
+ } catch {
34130
+ }
34131
+ }
34132
+ };
34133
+ if (physicsDriven) {
34134
+ const from = mode === "reel" ? activeReelY : activeRotation;
34135
+ const to = mode === "reel" ? finalReelY : finalRotation;
34136
+ const crossUnit = mode === "reel" ? reelRowHeight : activeAnglePerWedge * 180 / Math.PI;
34137
+ if (mode === "reel") visualReelYRef.current = from;
34138
+ else visualRotationRef.current = from;
34139
+ let crossings = 0;
34140
+ const t0 = performance.now();
34141
+ const step = (now) => {
34142
+ const t = Math.min(1, (now - t0) / effectiveDuration);
34143
+ const value = from + (to - from) * physicsRampDown(t);
34144
+ if (mode === "reel") {
34145
+ visualReelYRef.current = value;
34146
+ const el = reelTapeRef.current;
34147
+ if (el) {
34148
+ el.style.transform = `translateY(${-(value + REEL_LEAD_IN * reelRowHeight)}px)`;
34149
+ }
34150
+ } else {
34151
+ visualRotationRef.current = value;
34152
+ const el = wheelSvgRef.current;
34153
+ if (el) el.style.transform = `rotate(${value}deg)`;
34154
+ }
34155
+ const crossed = Math.floor(Math.abs(value - from) / crossUnit);
34156
+ if (crossed > crossings) {
34157
+ crossings = crossed;
34158
+ fireTick();
34159
+ }
34160
+ if (t < 1) {
34161
+ rafRef.current = requestAnimationFrame(step);
34162
+ } else {
34163
+ rafRef.current = null;
34164
+ visualRotationRef.current = null;
34165
+ visualReelYRef.current = null;
34166
+ }
34167
+ };
34168
+ rafRef.current = requestAnimationFrame(step);
34169
+ }
34170
+ if (!physicsDriven && (onTick || tickAudioRef.current) && !reducedMotion) {
34099
34171
  const wedges = mode === "reel" ? Math.max(1, reelRevRows + targetRowOffset) : (() => {
34100
34172
  const totalDelta = deltaMod + revolutions * 360;
34101
34173
  const wedgeDeg = activeAnglePerWedge * 180 / Math.PI;
@@ -34113,16 +34185,7 @@ function RandomPlayerPicker({
34113
34185
  const eased = 1 - Math.pow(1 - progress, 3);
34114
34186
  const interval = startInterval + (endInterval - startInterval) * eased;
34115
34187
  tickInterval.current = setTimeout(() => {
34116
- onTick?.();
34117
- const ta = tickAudioRef.current;
34118
- if (ta) {
34119
- try {
34120
- ta.currentTime = 0;
34121
- void ta.play().catch(() => {
34122
- });
34123
- } catch {
34124
- }
34125
- }
34188
+ fireTick();
34126
34189
  elapsed += interval;
34127
34190
  schedule();
34128
34191
  }, interval);
@@ -34182,7 +34245,7 @@ function RandomPlayerPicker({
34182
34245
  pendingPopIdxRef.current = null;
34183
34246
  if (!removeWinnersRef.current) return;
34184
34247
  setPool((p) => p.filter((_, i) => i !== finalWinnerIdx));
34185
- setWinner(null);
34248
+ if (activePool.length - 1 >= 2) setWinner(null);
34186
34249
  setRotation(0);
34187
34250
  setReelY(0);
34188
34251
  }, 2800);
@@ -34205,6 +34268,7 @@ function RandomPlayerPicker({
34205
34268
  maxRevolutions,
34206
34269
  history,
34207
34270
  reducedMotion,
34271
+ physicsDriven,
34208
34272
  enableHaptics,
34209
34273
  intensityCfg.particles,
34210
34274
  intensityCfg.durationMult,
@@ -34216,12 +34280,20 @@ function RandomPlayerPicker({
34216
34280
  if (spinRef && spinRef.current === spin) spinRef.current = null;
34217
34281
  };
34218
34282
  }, [spin, spinRef]);
34283
+ const onPoolChangeRef = (0, import_react71.useRef)(onPoolChange);
34284
+ (0, import_react71.useEffect)(() => {
34285
+ onPoolChangeRef.current = onPoolChange;
34286
+ });
34287
+ (0, import_react71.useEffect)(() => {
34288
+ onPoolChangeRef.current?.(pool);
34289
+ }, [pool]);
34219
34290
  (0, import_react71.useEffect)(
34220
34291
  () => () => {
34221
34292
  if (winnerTimer.current) clearTimeout(winnerTimer.current);
34222
34293
  if (particleTimer.current) clearTimeout(particleTimer.current);
34223
34294
  if (popTimer.current) clearTimeout(popTimer.current);
34224
34295
  if (tickInterval.current) clearTimeout(tickInterval.current);
34296
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
34225
34297
  },
34226
34298
  []
34227
34299
  );
@@ -34251,6 +34323,12 @@ function RandomPlayerPicker({
34251
34323
  clearTimeout(popTimer.current);
34252
34324
  popTimer.current = null;
34253
34325
  }
34326
+ if (rafRef.current) {
34327
+ cancelAnimationFrame(rafRef.current);
34328
+ rafRef.current = null;
34329
+ }
34330
+ visualRotationRef.current = null;
34331
+ visualReelYRef.current = null;
34254
34332
  pendingPopIdxRef.current = null;
34255
34333
  setSpinning(false);
34256
34334
  setPool(participants);
@@ -34260,9 +34338,17 @@ function RandomPlayerPicker({
34260
34338
  setReelY(0);
34261
34339
  setParticles([]);
34262
34340
  }, [participants]);
34341
+ (0, import_react71.useEffect)(() => {
34342
+ if (resetRef) resetRef.current = reset;
34343
+ return () => {
34344
+ if (resetRef && resetRef.current === reset) resetRef.current = null;
34345
+ };
34346
+ }, [reset, resetRef]);
34263
34347
  const isIdle = n < 2;
34348
+ const drainedByDraft = isIdle && participants.length >= 2;
34349
+ const effectiveIdleCaption = drainedByDraft ? draftCompleteCaption : placeholderCaption;
34264
34350
  const placeholderDiscColor = dark ? "#2a3344" : "#e2e8f0";
34265
- if (isIdle && placeholderMode === "message") {
34351
+ if (isIdle && !drainedByDraft && placeholderMode === "message") {
34266
34352
  return /* @__PURE__ */ (0, import_jsx_runtime117.jsx)(
34267
34353
  "div",
34268
34354
  {
@@ -34284,6 +34370,8 @@ function RandomPlayerPicker({
34284
34370
  const shakeKeyframe = `rppShake_${intensity}`;
34285
34371
  const glowKeyframe = `rppGlow_${intensity}`;
34286
34372
  const pulseKeyframe = `rppPulse_${intensity}`;
34373
+ const displayRotation = visualRotationRef.current ?? rotation;
34374
+ const displayReelY = visualReelYRef.current ?? reelY;
34287
34375
  return /* @__PURE__ */ (0, import_jsx_runtime117.jsxs)(
34288
34376
  "div",
34289
34377
  {
@@ -34330,13 +34418,33 @@ function RandomPlayerPicker({
34330
34418
  [data-rpp-anim] { animation: none !important; }
34331
34419
  }
34332
34420
  ` }),
34333
- mode === "reel" ? /* @__PURE__ */ (0, import_jsx_runtime117.jsx)(
34421
+ isIdle && placeholderMode === "message" ? (
34422
+ /* Drained draft in "message" mode — compact box in the wheel's
34423
+ place; the winner card / stats / controls / history below stay
34424
+ mounted so the endgame isn't a dead end. */
34425
+ /* @__PURE__ */ (0, import_jsx_runtime117.jsx)(
34426
+ "div",
34427
+ {
34428
+ style: {
34429
+ padding: "2rem 1rem",
34430
+ textAlign: "center",
34431
+ color: c.dim,
34432
+ borderRadius: 12,
34433
+ border: `1px dashed ${c.border}`
34434
+ },
34435
+ children: effectiveIdleCaption
34436
+ }
34437
+ )
34438
+ ) : mode === "reel" ? /* @__PURE__ */ (0, import_jsx_runtime117.jsx)(
34334
34439
  ReelStage,
34335
34440
  {
34336
34441
  pool,
34337
34442
  winner,
34338
34443
  spinning,
34339
34444
  reelY,
34445
+ displayY: displayReelY,
34446
+ cssTransition: !physicsDriven,
34447
+ tapeRef: reelTapeRef,
34340
34448
  reelVisibleRows,
34341
34449
  reelRowHeight,
34342
34450
  size,
@@ -34345,7 +34453,7 @@ function RandomPlayerPicker({
34345
34453
  dark,
34346
34454
  isIdle,
34347
34455
  placeholderMode,
34348
- placeholderCaption,
34456
+ placeholderCaption: effectiveIdleCaption,
34349
34457
  placeholderDiscColor,
34350
34458
  spinDuration,
34351
34459
  intensityCfg,
@@ -34402,6 +34510,7 @@ function RandomPlayerPicker({
34402
34510
  /* @__PURE__ */ (0, import_jsx_runtime117.jsxs)(
34403
34511
  "svg",
34404
34512
  {
34513
+ ref: wheelSvgRef,
34405
34514
  width: size,
34406
34515
  height: size,
34407
34516
  "aria-hidden": "true",
@@ -34410,8 +34519,8 @@ function RandomPlayerPicker({
34410
34519
  top: 36,
34411
34520
  left: 0,
34412
34521
  transformOrigin: `${cx}px ${cy}px`,
34413
- transform: `rotate(${rotation}deg)`,
34414
- transition: spinning ? `transform ${reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult)}ms ${intensityCfg.easing}` : "none",
34522
+ transform: `rotate(${displayRotation}deg)`,
34523
+ transition: spinning && !physicsDriven ? `transform ${reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult)}ms ${intensityCfg.easing}` : "none",
34415
34524
  filter: `drop-shadow(0 8px 28px ${dark ? "rgba(0,0,0,0.55)" : "rgba(0,0,0,0.15)"})`
34416
34525
  },
34417
34526
  children: [
@@ -34564,7 +34673,7 @@ function RandomPlayerPicker({
34564
34673
  pointerEvents: "none",
34565
34674
  zIndex: 3
34566
34675
  },
34567
- children: placeholderCaption
34676
+ children: effectiveIdleCaption
34568
34677
  }
34569
34678
  ),
34570
34679
  particles.map((p) => /* @__PURE__ */ (0, import_jsx_runtime117.jsx)(
@@ -40249,8 +40358,10 @@ function TeamGenerator({
40249
40358
  minPlayersPerTeam = 2,
40250
40359
  maxPlayers = Number.POSITIVE_INFINITY,
40251
40360
  defaultNames = [],
40361
+ defaultTeams,
40252
40362
  onNamesChange,
40253
40363
  onGenerate,
40364
+ onTeamsChange,
40254
40365
  onTeamCountChange,
40255
40366
  labels,
40256
40367
  ariaLabel = "Team generator",
@@ -40265,12 +40376,17 @@ function TeamGenerator({
40265
40376
  const [names, setNames] = (0, import_react76.useState)(defaultNames);
40266
40377
  const [input, setInput] = (0, import_react76.useState)("");
40267
40378
  const [teamCount, setTeamCount] = (0, import_react76.useState)(initialCount);
40268
- const [teams, setTeams] = (0, import_react76.useState)(null);
40379
+ const [teams, setTeams] = (0, import_react76.useState)(
40380
+ defaultTeams && defaultTeams.length > 0 ? defaultTeams : null
40381
+ );
40269
40382
  const inputRef = (0, import_react76.useRef)(null);
40270
40383
  const minNames = teamCount * minPlayersPerTeam;
40271
40384
  (0, import_react76.useEffect)(() => {
40272
40385
  onNamesChange?.(names);
40273
40386
  }, [names, onNamesChange]);
40387
+ (0, import_react76.useEffect)(() => {
40388
+ onTeamsChange?.(teams);
40389
+ }, [teams, onTeamsChange]);
40274
40390
  const addName = (0, import_react76.useCallback)(() => {
40275
40391
  const trimmed = input.trim();
40276
40392
  if (!trimmed) return;