@digilogiclabs/saas-factory-ui 2.2.0 → 2.4.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/web/index.js CHANGED
@@ -264,6 +264,7 @@ __export(index_exports, {
264
264
  CommentSystem: () => CommentSystem,
265
265
  Container: () => Container,
266
266
  ConversionKPI: () => ConversionKPI,
267
+ D6_FACE_PLACEMENTS: () => D6_FACE_PLACEMENTS,
267
268
  DashboardGrid: () => DashboardGrid,
268
269
  DashboardLayout: () => DashboardLayout,
269
270
  DashboardWidget: () => DashboardWidget,
@@ -504,6 +505,7 @@ __export(index_exports, {
504
505
  createFormStore: () => createFormStore,
505
506
  createOptimizedStore: () => createOptimizedStore,
506
507
  cssAnimations: () => cssAnimations,
508
+ d6LandingRotation: () => d6LandingRotation,
507
509
  dailyTrendingConfig: () => dailyTrendingConfig,
508
510
  detectAuthProvider: () => detectAuthProvider,
509
511
  detectDelimiter: () => detectDelimiter,
@@ -30223,7 +30225,7 @@ function TiltCard({
30223
30225
  }
30224
30226
 
30225
30227
  // src/components/ui/dice-roller/index.tsx
30226
- var import_react70 = require("react");
30228
+ var import_react69 = require("react");
30227
30229
 
30228
30230
  // src/hooks/web/use-dark-mode.ts
30229
30231
  var import_react68 = require("react");
@@ -30272,21 +30274,6 @@ function useDarkMode(forced) {
30272
30274
  return dark;
30273
30275
  }
30274
30276
 
30275
- // src/hooks/web/use-prefers-reduced-motion.ts
30276
- var import_react69 = require("react");
30277
- function usePrefersReducedMotion() {
30278
- const [reduced, setReduced] = (0, import_react69.useState)(false);
30279
- (0, import_react69.useEffect)(() => {
30280
- if (typeof window === "undefined" || !window.matchMedia) return;
30281
- const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
30282
- setReduced(mq.matches);
30283
- const read = () => setReduced(mq.matches);
30284
- mq.addEventListener("change", read);
30285
- return () => mq.removeEventListener("change", read);
30286
- }, []);
30287
- return reduced;
30288
- }
30289
-
30290
30277
  // src/components/ui/dice-roller/index.tsx
30291
30278
  var import_jsx_runtime113 = require("react/jsx-runtime");
30292
30279
  var PRESETS = {
@@ -30359,14 +30346,35 @@ var PIP_POS = {
30359
30346
  [75, 75]
30360
30347
  ]
30361
30348
  };
30362
- var SHOW_ROT = {
30349
+ var D6_FACE_PLACEMENTS = {
30363
30350
  1: { x: 0, y: 0 },
30364
- 2: { x: 0, y: 180 },
30365
- 3: { x: 0, y: -90 },
30366
- 4: { x: 0, y: 90 },
30367
- 5: { x: -90, y: 0 },
30368
- 6: { x: 90, y: 0 }
30351
+ 2: { x: 0, y: 90 },
30352
+ 3: { x: 90, y: 0 },
30353
+ 4: { x: -90, y: 0 },
30354
+ 5: { x: 0, y: -90 },
30355
+ 6: { x: 0, y: 180 }
30356
+ };
30357
+ function d6LandingRotation(value) {
30358
+ const p = D6_FACE_PLACEMENTS[value];
30359
+ return {
30360
+ x: Math.abs(p.x) === 180 ? 180 : -p.x || 0,
30361
+ y: Math.abs(p.y) === 180 ? 180 : -p.y || 0
30362
+ };
30363
+ }
30364
+ var POLY_SHAPES = {
30365
+ 4: { points: "50,8 93,86 7,86", textY: 62, fontScale: 0.3 },
30366
+ 8: { points: "50,4 96,50 50,96 4,50", textY: 51, fontScale: 0.33 },
30367
+ 10: { points: "50,4 93,44 50,96 7,44", textY: 47, fontScale: 0.3 },
30368
+ 12: { points: "50,4 95,37 78,93 22,93 5,37", textY: 54, fontScale: 0.33 },
30369
+ 20: {
30370
+ points: "50,3 91,26 91,74 50,97 9,74 9,26",
30371
+ textY: 51,
30372
+ fontScale: 0.33
30373
+ }
30369
30374
  };
30375
+ function formatDieValue(v, sides) {
30376
+ return sides > 8 && (v === 6 || v === 9) ? `${v}.` : String(v);
30377
+ }
30370
30378
  var MAX_COUNT = 6;
30371
30379
  var TUMBLE_MS = 950;
30372
30380
  var SETTLE_MS = 1100;
@@ -30484,8 +30492,8 @@ function simulateDice(arenaW, arenaH, diceSize, starts, numDice) {
30484
30492
  return frames;
30485
30493
  }
30486
30494
  function useArenaBounds(ref) {
30487
- const [bounds, setBounds] = (0, import_react70.useState)({ w: 400, h: 260 });
30488
- (0, import_react70.useEffect)(() => {
30495
+ const [bounds, setBounds] = (0, import_react69.useState)({ w: 400, h: 260 });
30496
+ (0, import_react69.useEffect)(() => {
30489
30497
  if (!ref.current) return;
30490
30498
  const el = ref.current;
30491
30499
  const update = () => setBounds({ w: el.offsetWidth, h: el.offsetHeight });
@@ -30518,6 +30526,7 @@ function Face({ value, pipColor, pipShadow, pipDiameter }) {
30518
30526
  }
30519
30527
  function DiceRoller({
30520
30528
  count: count2 = 2,
30529
+ sides = 6,
30521
30530
  theme: theme5 = "red",
30522
30531
  darkMode,
30523
30532
  size = 80,
@@ -30539,18 +30548,18 @@ function DiceRoller({
30539
30548
  }) {
30540
30549
  const clampedCount = Math.max(1, Math.min(MAX_COUNT, count2));
30541
30550
  const dark = useDarkMode(darkMode);
30542
- const reducedMotion = usePrefersReducedMotion();
30543
- const [rolling, setRolling] = (0, import_react70.useState)(false);
30544
- const [results, setResults] = (0, import_react70.useState)(null);
30545
- const [history, setHistory] = (0, import_react70.useState)([]);
30546
- const dieWrapperRefs = (0, import_react70.useRef)([]);
30547
- const cubeRefs = (0, import_react70.useRef)([]);
30548
- const shadowRefs = (0, import_react70.useRef)([]);
30549
- const currentPositionsRef = (0, import_react70.useRef)(null);
30550
- const rollingRef = (0, import_react70.useRef)(false);
30551
- const dieSetters = (0, import_react70.useRef)([]);
30552
- const cubeSetters = (0, import_react70.useRef)([]);
30553
- const shadowSetters = (0, import_react70.useRef)([]);
30551
+ const [rolling, setRolling] = (0, import_react69.useState)(false);
30552
+ const [results, setResults] = (0, import_react69.useState)(null);
30553
+ const [history, setHistory] = (0, import_react69.useState)([]);
30554
+ const dieWrapperRefs = (0, import_react69.useRef)([]);
30555
+ const cubeRefs = (0, import_react69.useRef)([]);
30556
+ const shadowRefs = (0, import_react69.useRef)([]);
30557
+ const numeralRefs = (0, import_react69.useRef)([]);
30558
+ const currentPositionsRef = (0, import_react69.useRef)(null);
30559
+ const rollingRef = (0, import_react69.useRef)(false);
30560
+ const dieSetters = (0, import_react69.useRef)([]);
30561
+ const cubeSetters = (0, import_react69.useRef)([]);
30562
+ const shadowSetters = (0, import_react69.useRef)([]);
30554
30563
  const getDieSetter = (i) => {
30555
30564
  if (!dieSetters.current[i]) {
30556
30565
  dieSetters.current[i] = (el) => {
@@ -30575,11 +30584,20 @@ function DiceRoller({
30575
30584
  }
30576
30585
  return shadowSetters.current[i];
30577
30586
  };
30578
- const timersRef = (0, import_react70.useRef)([]);
30579
- const rafHandleRef = (0, import_react70.useRef)(null);
30580
- const arenaRef = (0, import_react70.useRef)(null);
30587
+ const numeralSetters = (0, import_react69.useRef)([]);
30588
+ const getNumeralSetter = (i) => {
30589
+ if (!numeralSetters.current[i]) {
30590
+ numeralSetters.current[i] = (el) => {
30591
+ numeralRefs.current[i] = el;
30592
+ };
30593
+ }
30594
+ return numeralSetters.current[i];
30595
+ };
30596
+ const timersRef = (0, import_react69.useRef)([]);
30597
+ const rafHandleRef = (0, import_react69.useRef)(null);
30598
+ const arenaRef = (0, import_react69.useRef)(null);
30581
30599
  const arena = useArenaBounds(arenaRef);
30582
- const colors = (0, import_react70.useMemo)(() => {
30600
+ const colors = (0, import_react69.useMemo)(() => {
30583
30601
  if (typeof theme5 === "object") {
30584
30602
  const pipL = theme5.pip;
30585
30603
  const pipD = theme5.pipDark ?? theme5.pip;
@@ -30616,7 +30634,7 @@ function DiceRoller({
30616
30634
  chipDim: dark ? "#3a4358" : "#94a3b8"
30617
30635
  };
30618
30636
  }, [theme5, dark]);
30619
- const effectiveSize = (0, import_react70.useMemo)(() => {
30637
+ const effectiveSize = (0, import_react69.useMemo)(() => {
30620
30638
  if (!responsive2) return size;
30621
30639
  const needed = clampedCount * size * 1.6;
30622
30640
  if (arena.w >= needed || arena.w === 0) return size;
@@ -30627,25 +30645,25 @@ function DiceRoller({
30627
30645
  const pipDiameter = Math.round(effectiveSize * 0.2);
30628
30646
  const faceRadius = Math.round(effectiveSize * 0.07);
30629
30647
  const arenaHeight = arenaHeightProp ?? Math.max(effectiveSize * 3, 200);
30630
- const faceTransforms = (0, import_react70.useMemo)(
30631
- () => [
30632
- `translateZ(${half}px)`,
30633
- `rotateY(180deg) translateZ(${half}px)`,
30634
- `rotateY(-90deg) translateZ(${half}px)`,
30635
- `rotateY(90deg) translateZ(${half}px)`,
30636
- `rotateX(90deg) translateZ(${half}px)`,
30637
- `rotateX(-90deg) translateZ(${half}px)`
30638
- ],
30648
+ const faceTransformFor = (0, import_react69.useCallback)(
30649
+ (value) => {
30650
+ const p = D6_FACE_PLACEMENTS[value];
30651
+ const parts = [];
30652
+ if (p.x !== 0) parts.push(`rotateX(${p.x}deg)`);
30653
+ if (p.y !== 0) parts.push(`rotateY(${p.y}deg)`);
30654
+ parts.push(`translateZ(${half}px)`);
30655
+ return parts.join(" ");
30656
+ },
30639
30657
  [half]
30640
30658
  );
30641
- const defaultPositions = (0, import_react70.useCallback)(
30659
+ const defaultPositions = (0, import_react69.useCallback)(
30642
30660
  (w, h, n) => Array.from({ length: n }, (_, i) => ({
30643
30661
  x: (i + 1) * w / (n + 1),
30644
30662
  y: h / 2
30645
30663
  })),
30646
30664
  []
30647
30665
  );
30648
- const writeDiePosition = (0, import_react70.useCallback)(
30666
+ const writeDiePosition = (0, import_react69.useCallback)(
30649
30667
  (i, x, y, sx = 1, sy = 1) => {
30650
30668
  const el = dieWrapperRefs.current[i];
30651
30669
  if (!el) return;
@@ -30656,16 +30674,24 @@ function DiceRoller({
30656
30674
  },
30657
30675
  [half]
30658
30676
  );
30659
- const writeCubeRotation = (0, import_react70.useCallback)(
30660
- (i, rx, ry, rz, withTransition) => {
30677
+ const writeCubeRotation = (0, import_react69.useCallback)(
30678
+ (i, rx, ry, rz, withTransition, tiltZ = 0) => {
30661
30679
  const el = cubeRefs.current[i];
30662
30680
  if (!el) return;
30663
30681
  el.style.transition = withTransition ? "transform 0.95s cubic-bezier(0.12,0.8,0.22,1)" : "none";
30664
- el.style.transform = `rotateX(${rx}deg) rotateY(${ry}deg) rotateZ(${rz}deg)`;
30682
+ el.style.transform = `${tiltZ ? `rotateZ(${tiltZ}deg) ` : ""}rotateX(${rx}deg) rotateY(${ry}deg) rotateZ(${rz}deg)`;
30665
30683
  },
30666
30684
  []
30667
30685
  );
30668
- (0, import_react70.useEffect)(() => {
30686
+ (0, import_react69.useEffect)(() => {
30687
+ if (timersRef.current.length > 0) {
30688
+ timersRef.current.forEach((t) => clearTimeout(t));
30689
+ timersRef.current = [];
30690
+ }
30691
+ if (rollingRef.current) {
30692
+ rollingRef.current = false;
30693
+ setRolling(false);
30694
+ }
30669
30695
  const rawW = arenaRef.current?.offsetWidth ?? 0;
30670
30696
  const rawH = arenaRef.current?.offsetHeight ?? 0;
30671
30697
  const w = rawW > 20 ? rawW : arena.w > 20 ? arena.w : 400;
@@ -30691,19 +30717,23 @@ function DiceRoller({
30691
30717
  setResults(null);
30692
30718
  }, [
30693
30719
  clampedCount,
30720
+ sides,
30694
30721
  effectiveSize,
30695
30722
  arena.w,
30696
30723
  arenaHeight,
30697
30724
  defaultPositions,
30698
30725
  half
30699
30726
  ]);
30700
- const roll = (0, import_react70.useCallback)(() => {
30727
+ (0, import_react69.useEffect)(() => {
30728
+ setHistory([]);
30729
+ }, [sides]);
30730
+ const roll = (0, import_react69.useCallback)(() => {
30701
30731
  if (rollingRef.current) return;
30702
30732
  rollingRef.current = true;
30703
30733
  setRolling(true);
30704
30734
  const vals = Array.from(
30705
30735
  { length: clampedCount },
30706
- () => Math.floor(Math.random() * 6) + 1
30736
+ () => Math.floor(Math.random() * sides) + 1
30707
30737
  );
30708
30738
  timersRef.current.forEach((t) => clearTimeout(t));
30709
30739
  timersRef.current = [];
@@ -30718,13 +30748,17 @@ function DiceRoller({
30718
30748
  const starts = currentPositionsRef.current ?? defaultPositions(w, h, clampedCount);
30719
30749
  const frames = simulateDice(w, h, effectiveSize, starts, clampedCount);
30720
30750
  for (let i = 0; i < clampedCount; i++) {
30721
- writeCubeRotation(
30722
- i,
30723
- (Math.random() - 0.5) * 720,
30724
- (Math.random() - 0.5) * 720,
30725
- (Math.random() - 0.5) * 720,
30726
- false
30727
- );
30751
+ if (sides === 6) {
30752
+ writeCubeRotation(
30753
+ i,
30754
+ (Math.random() - 0.5) * 720,
30755
+ (Math.random() - 0.5) * 720,
30756
+ (Math.random() - 0.5) * 720,
30757
+ false
30758
+ );
30759
+ } else {
30760
+ writeCubeRotation(i, 0, 0, (Math.random() - 0.5) * 720, false);
30761
+ }
30728
30762
  const el = dieWrapperRefs.current[i];
30729
30763
  if (el) el.style.transition = "none";
30730
30764
  const shadow = shadowRefs.current[i];
@@ -30757,18 +30791,47 @@ function DiceRoller({
30757
30791
  }, capturedStep * stepMs);
30758
30792
  timersRef.current.push(timer);
30759
30793
  }
30794
+ if (sides !== 6) {
30795
+ const flickerTicks = 8;
30796
+ const flickerStep = Math.max(60, Math.floor(TUMBLE_MS / flickerTicks));
30797
+ for (let f = 1; f <= flickerTicks; f++) {
30798
+ const t = setTimeout(() => {
30799
+ for (let i = 0; i < clampedCount; i++) {
30800
+ const el = numeralRefs.current[i];
30801
+ if (el)
30802
+ el.textContent = formatDieValue(
30803
+ Math.floor(Math.random() * sides) + 1,
30804
+ sides
30805
+ );
30806
+ }
30807
+ }, f * flickerStep);
30808
+ timersRef.current.push(t);
30809
+ }
30810
+ }
30760
30811
  requestAnimationFrame(() => {
30761
30812
  requestAnimationFrame(() => {
30762
30813
  for (let i = 0; i < clampedCount; i++) {
30763
- const tgt = SHOW_ROT[vals[i]];
30764
- const lx = tgt.x + (Math.random() > 0.5 ? 720 : -720);
30765
- const ly = tgt.y;
30766
- const lz = (Math.random() > 0.5 ? 1 : -1) * 360;
30767
- writeCubeRotation(i, lx, ly, lz, true);
30814
+ const tilt = (Math.random() - 0.5) * 18;
30815
+ if (sides === 6) {
30816
+ const tgt = d6LandingRotation(vals[i]);
30817
+ const lx = tgt.x + (Math.random() > 0.5 ? 720 : -720);
30818
+ const ly = tgt.y;
30819
+ const lz = (Math.random() > 0.5 ? 1 : -1) * 360;
30820
+ writeCubeRotation(i, lx, ly, lz, true, tilt);
30821
+ } else {
30822
+ const lz = (Math.random() > 0.5 ? 1 : -1) * 720 + tilt;
30823
+ writeCubeRotation(i, 0, 0, lz, true);
30824
+ }
30768
30825
  }
30769
30826
  });
30770
30827
  });
30771
30828
  const settleTimer = setTimeout(() => {
30829
+ if (sides !== 6) {
30830
+ for (let i = 0; i < clampedCount; i++) {
30831
+ const el = numeralRefs.current[i];
30832
+ if (el) el.textContent = formatDieValue(vals[i], sides);
30833
+ }
30834
+ }
30772
30835
  setResults(vals);
30773
30836
  setRolling(false);
30774
30837
  rollingRef.current = false;
@@ -30781,28 +30844,28 @@ function DiceRoller({
30781
30844
  timersRef.current.push(settleTimer);
30782
30845
  }, [
30783
30846
  clampedCount,
30847
+ sides,
30784
30848
  defaultPositions,
30785
30849
  effectiveSize,
30786
30850
  half,
30787
- reducedMotion,
30788
30851
  onRoll,
30789
30852
  writeCubeRotation,
30790
30853
  writeDiePosition,
30791
30854
  showHistory,
30792
30855
  historyMax
30793
30856
  ]);
30794
- (0, import_react70.useEffect)(() => {
30857
+ (0, import_react69.useEffect)(() => {
30795
30858
  if (rollRef) rollRef.current = roll;
30796
30859
  return () => {
30797
30860
  if (rollRef && rollRef.current === roll) rollRef.current = null;
30798
30861
  };
30799
30862
  }, [roll, rollRef]);
30800
- (0, import_react70.useEffect)(() => {
30863
+ (0, import_react69.useEffect)(() => {
30801
30864
  if (!autoRoll) return;
30802
30865
  const t = setTimeout(roll, 300);
30803
30866
  return () => clearTimeout(t);
30804
30867
  }, []);
30805
- (0, import_react70.useEffect)(
30868
+ (0, import_react69.useEffect)(
30806
30869
  () => () => {
30807
30870
  timersRef.current.forEach((t) => clearTimeout(t));
30808
30871
  timersRef.current = [];
@@ -30826,7 +30889,7 @@ function DiceRoller({
30826
30889
  {
30827
30890
  ref: arenaRef,
30828
30891
  role: "img",
30829
- "aria-label": ariaLabel ?? `${clampedCount} dice`,
30892
+ "aria-label": ariaLabel ?? `${clampedCount} ${sides === 6 ? "dice" : `${sides}-sided dice`}`,
30830
30893
  "aria-live": "polite",
30831
30894
  style: {
30832
30895
  position: "relative",
@@ -30889,7 +30952,7 @@ function DiceRoller({
30889
30952
  transformStyle: "preserve-3d",
30890
30953
  willChange: "transform"
30891
30954
  },
30892
- children: [1, 2, 3, 4, 5, 6].map((val, fi) => /* @__PURE__ */ (0, import_jsx_runtime113.jsx)(
30955
+ children: sides === 6 ? [1, 2, 3, 4, 5, 6].map((val) => /* @__PURE__ */ (0, import_jsx_runtime113.jsx)(
30893
30956
  "div",
30894
30957
  {
30895
30958
  style: {
@@ -30899,7 +30962,7 @@ function DiceRoller({
30899
30962
  background: colors.face,
30900
30963
  border: `1px solid ${colors.faceBorder}`,
30901
30964
  borderRadius: faceRadius,
30902
- transform: faceTransforms[fi],
30965
+ transform: faceTransformFor(val),
30903
30966
  transition: "background 0.3s, border-color 0.3s"
30904
30967
  },
30905
30968
  children: /* @__PURE__ */ (0, import_jsx_runtime113.jsx)(
@@ -30913,7 +30976,64 @@ function DiceRoller({
30913
30976
  )
30914
30977
  },
30915
30978
  val
30916
- ))
30979
+ )) : (() => {
30980
+ const shape = POLY_SHAPES[sides];
30981
+ const shown = results?.[d] ?? sides;
30982
+ return /* @__PURE__ */ (0, import_jsx_runtime113.jsxs)(
30983
+ "svg",
30984
+ {
30985
+ viewBox: "0 0 100 100",
30986
+ width: "100%",
30987
+ height: "100%",
30988
+ style: { display: "block", overflow: "visible" },
30989
+ "aria-hidden": "true",
30990
+ children: [
30991
+ /* @__PURE__ */ (0, import_jsx_runtime113.jsx)(
30992
+ "polygon",
30993
+ {
30994
+ points: shape.points,
30995
+ fill: colors.face,
30996
+ stroke: colors.faceBorder,
30997
+ strokeWidth: 2.5,
30998
+ strokeLinejoin: "round",
30999
+ style: { transition: "fill 0.3s, stroke 0.3s" }
31000
+ }
31001
+ ),
31002
+ /* @__PURE__ */ (0, import_jsx_runtime113.jsx)(
31003
+ "polygon",
31004
+ {
31005
+ points: shape.points,
31006
+ fill: "none",
31007
+ stroke: colors.faceBorder,
31008
+ strokeWidth: 1.5,
31009
+ strokeLinejoin: "round",
31010
+ opacity: 0.55,
31011
+ transform: "translate(50 50) scale(0.76) translate(-50 -50)"
31012
+ }
31013
+ ),
31014
+ /* @__PURE__ */ (0, import_jsx_runtime113.jsx)(
31015
+ "text",
31016
+ {
31017
+ ref: getNumeralSetter(d),
31018
+ x: 50,
31019
+ y: shape.textY,
31020
+ textAnchor: "middle",
31021
+ dominantBaseline: "central",
31022
+ fontSize: 100 * shape.fontScale,
31023
+ fontWeight: 800,
31024
+ fill: colors.pip,
31025
+ style: {
31026
+ fontVariantNumeric: "tabular-nums",
31027
+ transition: "fill 0.3s",
31028
+ userSelect: "none"
31029
+ },
31030
+ children: formatDieValue(shown, sides)
31031
+ }
31032
+ )
31033
+ ]
31034
+ }
31035
+ );
31036
+ })()
30917
31037
  }
30918
31038
  )
30919
31039
  ] }, d);
@@ -31045,7 +31165,7 @@ function DiceRoller({
31045
31165
  DiceRoller.displayName = "DiceRoller";
31046
31166
 
31047
31167
  // src/components/ui/bracket-generator/index.tsx
31048
- var import_react71 = require("react");
31168
+ var import_react70 = require("react");
31049
31169
 
31050
31170
  // src/utils/array.ts
31051
31171
  function shuffle(arr) {
@@ -31265,15 +31385,15 @@ function computeLayout(rounds, firstRoundCount) {
31265
31385
  };
31266
31386
  }
31267
31387
  function usePanZoom(viewportRef, contentSize, minZoom, maxZoom) {
31268
- const [transform, setTransform] = (0, import_react71.useState)({
31388
+ const [transform, setTransform] = (0, import_react70.useState)({
31269
31389
  x: 0,
31270
31390
  y: 0,
31271
31391
  scale: 1
31272
31392
  });
31273
- const isDragging = (0, import_react71.useRef)(false);
31274
- const dragStart = (0, import_react71.useRef)({ x: 0, y: 0 });
31275
- const pinchRef = (0, import_react71.useRef)(null);
31276
- const clampTransform = (0, import_react71.useCallback)(
31393
+ const isDragging = (0, import_react70.useRef)(false);
31394
+ const dragStart = (0, import_react70.useRef)({ x: 0, y: 0 });
31395
+ const pinchRef = (0, import_react70.useRef)(null);
31396
+ const clampTransform = (0, import_react70.useCallback)(
31277
31397
  (t, vw, vh) => {
31278
31398
  const sw = contentSize.w * t.scale;
31279
31399
  const sh = contentSize.h * t.scale;
@@ -31285,7 +31405,7 @@ function usePanZoom(viewportRef, contentSize, minZoom, maxZoom) {
31285
31405
  },
31286
31406
  [contentSize]
31287
31407
  );
31288
- (0, import_react71.useEffect)(() => {
31408
+ (0, import_react70.useEffect)(() => {
31289
31409
  const el = viewportRef.current;
31290
31410
  if (!el) return;
31291
31411
  const onWheel = (e) => {
@@ -31310,7 +31430,7 @@ function usePanZoom(viewportRef, contentSize, minZoom, maxZoom) {
31310
31430
  el.addEventListener("wheel", onWheel, { passive: false });
31311
31431
  return () => el.removeEventListener("wheel", onWheel);
31312
31432
  }, [viewportRef, minZoom, maxZoom, clampTransform]);
31313
- const onPointerDown = (0, import_react71.useCallback)(
31433
+ const onPointerDown = (0, import_react70.useCallback)(
31314
31434
  (e) => {
31315
31435
  if (e.target.closest("[data-no-pan]")) return;
31316
31436
  isDragging.current = true;
@@ -31323,7 +31443,7 @@ function usePanZoom(viewportRef, contentSize, minZoom, maxZoom) {
31323
31443
  },
31324
31444
  [transform]
31325
31445
  );
31326
- const onPointerMove = (0, import_react71.useCallback)(
31446
+ const onPointerMove = (0, import_react70.useCallback)(
31327
31447
  (e) => {
31328
31448
  if (!isDragging.current) return;
31329
31449
  const el = viewportRef.current;
@@ -31343,11 +31463,11 @@ function usePanZoom(viewportRef, contentSize, minZoom, maxZoom) {
31343
31463
  },
31344
31464
  [viewportRef, clampTransform]
31345
31465
  );
31346
- const onPointerUp = (0, import_react71.useCallback)((e) => {
31466
+ const onPointerUp = (0, import_react70.useCallback)((e) => {
31347
31467
  isDragging.current = false;
31348
31468
  e.currentTarget.style.cursor = "grab";
31349
31469
  }, []);
31350
- const onTouchStart = (0, import_react71.useCallback)(
31470
+ const onTouchStart = (0, import_react70.useCallback)(
31351
31471
  (e) => {
31352
31472
  if (e.touches.length === 2) {
31353
31473
  const [t1, t2] = [e.touches[0], e.touches[1]];
@@ -31367,7 +31487,7 @@ function usePanZoom(viewportRef, contentSize, minZoom, maxZoom) {
31367
31487
  },
31368
31488
  [transform]
31369
31489
  );
31370
- const onTouchMove = (0, import_react71.useCallback)(
31490
+ const onTouchMove = (0, import_react70.useCallback)(
31371
31491
  (e) => {
31372
31492
  if (e.touches.length === 2 && pinchRef.current) {
31373
31493
  e.preventDefault();
@@ -31404,10 +31524,10 @@ function usePanZoom(viewportRef, contentSize, minZoom, maxZoom) {
31404
31524
  },
31405
31525
  [viewportRef, minZoom, maxZoom, clampTransform]
31406
31526
  );
31407
- const onTouchEnd = (0, import_react71.useCallback)(() => {
31527
+ const onTouchEnd = (0, import_react70.useCallback)(() => {
31408
31528
  pinchRef.current = null;
31409
31529
  }, []);
31410
- const zoomBy = (0, import_react71.useCallback)(
31530
+ const zoomBy = (0, import_react70.useCallback)(
31411
31531
  (factor) => {
31412
31532
  const el = viewportRef.current;
31413
31533
  if (!el) return;
@@ -31426,7 +31546,7 @@ function usePanZoom(viewportRef, contentSize, minZoom, maxZoom) {
31426
31546
  },
31427
31547
  [viewportRef, minZoom, maxZoom, clampTransform]
31428
31548
  );
31429
- const fit = (0, import_react71.useCallback)(() => {
31549
+ const fit = (0, import_react70.useCallback)(() => {
31430
31550
  const el = viewportRef.current;
31431
31551
  if (!el || !contentSize.w) return;
31432
31552
  const rect = el.getBoundingClientRect();
@@ -31440,10 +31560,10 @@ function usePanZoom(viewportRef, contentSize, minZoom, maxZoom) {
31440
31560
  scale
31441
31561
  });
31442
31562
  }, [viewportRef, contentSize, minZoom, maxZoom]);
31443
- const reset = (0, import_react71.useCallback)(() => {
31563
+ const reset = (0, import_react70.useCallback)(() => {
31444
31564
  setTransform({ x: 40, y: 40, scale: 1 });
31445
31565
  }, []);
31446
- const panTo = (0, import_react71.useCallback)(
31566
+ const panTo = (0, import_react70.useCallback)(
31447
31567
  (contentX, contentY) => {
31448
31568
  const el = viewportRef.current;
31449
31569
  if (!el) return;
@@ -31516,15 +31636,15 @@ function BracketGenerator({
31516
31636
  ariaLabel = "Tournament bracket"
31517
31637
  }) {
31518
31638
  const dark = useDarkMode(darkMode);
31519
- const [bracketState, setBracketState] = (0, import_react71.useState)({
31639
+ const [bracketState, setBracketState] = (0, import_react70.useState)({
31520
31640
  matches: [],
31521
31641
  rounds: 0,
31522
31642
  champion: null
31523
31643
  });
31524
- const [simulating, setSimulating] = (0, import_react71.useState)(false);
31525
- const autoRef = (0, import_react71.useRef)(null);
31526
- const viewportRef = (0, import_react71.useRef)(null);
31527
- const effectiveParticipants = (0, import_react71.useMemo)(() => {
31644
+ const [simulating, setSimulating] = (0, import_react70.useState)(false);
31645
+ const autoRef = (0, import_react70.useRef)(null);
31646
+ const viewportRef = (0, import_react70.useRef)(null);
31647
+ const effectiveParticipants = (0, import_react70.useMemo)(() => {
31528
31648
  if (participantsProp?.length) {
31529
31649
  return padToSize(
31530
31650
  participantsProp.map((p, i) => ({
@@ -31543,7 +31663,7 @@ function BracketGenerator({
31543
31663
  }));
31544
31664
  }, [participantsProp, size]);
31545
31665
  const preset = typeof theme5 === "string" ? PRESETS2[theme5] || PRESETS2.blue : null;
31546
- const themeColors = (0, import_react71.useMemo)(() => {
31666
+ const themeColors = (0, import_react70.useMemo)(() => {
31547
31667
  if (typeof theme5 === "object") return theme5;
31548
31668
  const p = preset;
31549
31669
  return {
@@ -31552,9 +31672,9 @@ function BracketGenerator({
31552
31672
  color: p.color
31553
31673
  };
31554
31674
  }, [theme5, dark, preset]);
31555
- const colors = (0, import_react71.useMemo)(() => buildColorTokens(dark), [dark]);
31675
+ const colors = (0, import_react70.useMemo)(() => buildColorTokens(dark), [dark]);
31556
31676
  const roundLabels = ROUND_NAMES[effectiveParticipants.length] || [];
31557
- const layout = (0, import_react71.useMemo)(() => {
31677
+ const layout = (0, import_react70.useMemo)(() => {
31558
31678
  if (!bracketState.rounds)
31559
31679
  return {
31560
31680
  positions: {},
@@ -31563,7 +31683,7 @@ function BracketGenerator({
31563
31683
  };
31564
31684
  return computeLayout(bracketState.rounds, effectiveParticipants.length / 2);
31565
31685
  }, [bracketState.rounds, effectiveParticipants.length]);
31566
- const contentSize = (0, import_react71.useMemo)(
31686
+ const contentSize = (0, import_react70.useMemo)(
31567
31687
  () => ({
31568
31688
  w: layout.totalW + CANVAS_PAD * 2,
31569
31689
  h: layout.totalH + CANVAS_PAD * 2
@@ -31576,10 +31696,10 @@ function BracketGenerator({
31576
31696
  minZoom,
31577
31697
  maxZoom
31578
31698
  );
31579
- const [miniMapHidden, setMiniMapHidden] = (0, import_react71.useState)(false);
31580
- const miniMapDragRef = (0, import_react71.useRef)(false);
31581
- const resumeRef = (0, import_react71.useRef)(resumeMatches);
31582
- const hydrateFromResume = (0, import_react71.useCallback)(() => {
31699
+ const [miniMapHidden, setMiniMapHidden] = (0, import_react70.useState)(false);
31700
+ const miniMapDragRef = (0, import_react70.useRef)(false);
31701
+ const resumeRef = (0, import_react70.useRef)(resumeMatches);
31702
+ const hydrateFromResume = (0, import_react70.useCallback)(() => {
31583
31703
  const resume = resumeRef.current;
31584
31704
  if (!resume?.length) return null;
31585
31705
  const n = effectiveParticipants.length;
@@ -31619,7 +31739,7 @@ function BracketGenerator({
31619
31739
  const finalMatch = matches.find((m) => m.round === rounds - 1);
31620
31740
  return { matches, rounds, champion: finalMatch?.winner ?? null };
31621
31741
  }, [effectiveParticipants]);
31622
- const seedBracket = (0, import_react71.useCallback)(() => {
31742
+ const seedBracket = (0, import_react70.useCallback)(() => {
31623
31743
  const { matches, rounds } = buildBracket(
31624
31744
  effectiveParticipants,
31625
31745
  seedingStrategy
@@ -31642,7 +31762,7 @@ function BracketGenerator({
31642
31762
  };
31643
31763
  setBracketState(newState);
31644
31764
  }, [effectiveParticipants, seedingStrategy]);
31645
- const simulateNext = (0, import_react71.useCallback)(() => {
31765
+ const simulateNext = (0, import_react70.useCallback)(() => {
31646
31766
  setBracketState((prev) => {
31647
31767
  const next = prev.matches.find((m) => m.a && m.b && !m.winner);
31648
31768
  if (!next) return prev;
@@ -31657,7 +31777,7 @@ function BracketGenerator({
31657
31777
  return { ...prev, matches, champion };
31658
31778
  });
31659
31779
  }, []);
31660
- const autoPlay = (0, import_react71.useCallback)(() => {
31780
+ const autoPlay = (0, import_react70.useCallback)(() => {
31661
31781
  if (simulating) {
31662
31782
  if (autoRef.current) clearTimeout(autoRef.current);
31663
31783
  setSimulating(false);
@@ -31686,7 +31806,7 @@ function BracketGenerator({
31686
31806
  };
31687
31807
  tick();
31688
31808
  }, [simulating, simulateDelayMs]);
31689
- const advanceWinner = (0, import_react71.useCallback)(
31809
+ const advanceWinner = (0, import_react70.useCallback)(
31690
31810
  (matchId, winnerSlot) => {
31691
31811
  setBracketState((prev) => {
31692
31812
  const m = prev.matches.find((x) => x.id === matchId);
@@ -31709,8 +31829,8 @@ function BracketGenerator({
31709
31829
  },
31710
31830
  []
31711
31831
  );
31712
- const prevBracketStateRef = (0, import_react71.useRef)(bracketState);
31713
- (0, import_react71.useEffect)(() => {
31832
+ const prevBracketStateRef = (0, import_react70.useRef)(bracketState);
31833
+ (0, import_react70.useEffect)(() => {
31714
31834
  const prev = prevBracketStateRef.current;
31715
31835
  const next = bracketState;
31716
31836
  if (prev === next) return;
@@ -31729,12 +31849,12 @@ function BracketGenerator({
31729
31849
  onStateChange?.(next);
31730
31850
  prevBracketStateRef.current = next;
31731
31851
  }, [bracketState, onMatchComplete, onChampion, onStateChange]);
31732
- const structuralKey = (0, import_react71.useMemo)(
31852
+ const structuralKey = (0, import_react70.useMemo)(
31733
31853
  () => `${seedingStrategy}|${effectiveParticipants.map((p) => `${p.id}:${p.seed ?? ""}`).join(",")}`,
31734
31854
  [effectiveParticipants, seedingStrategy]
31735
31855
  );
31736
- const seededKeyRef = (0, import_react71.useRef)(null);
31737
- (0, import_react71.useEffect)(() => {
31856
+ const seededKeyRef = (0, import_react70.useRef)(null);
31857
+ (0, import_react70.useEffect)(() => {
31738
31858
  if (seededKeyRef.current === structuralKey) {
31739
31859
  setBracketState((prev) => {
31740
31860
  if (!prev.matches.length) return prev;
@@ -31765,24 +31885,24 @@ function BracketGenerator({
31765
31885
  seedBracket();
31766
31886
  }
31767
31887
  }, [structuralKey, effectiveParticipants, hydrateFromResume, seedBracket]);
31768
- (0, import_react71.useEffect)(() => {
31888
+ (0, import_react70.useEffect)(() => {
31769
31889
  return () => {
31770
31890
  if (autoRef.current) clearTimeout(autoRef.current);
31771
31891
  };
31772
31892
  }, []);
31773
- (0, import_react71.useLayoutEffect)(() => {
31893
+ (0, import_react70.useLayoutEffect)(() => {
31774
31894
  if (contentSize.w > 0) {
31775
31895
  const timer = setTimeout(fit, 50);
31776
31896
  return () => clearTimeout(timer);
31777
31897
  }
31778
31898
  }, [contentSize.w]);
31779
- (0, import_react71.useEffect)(() => {
31899
+ (0, import_react70.useEffect)(() => {
31780
31900
  if (autoSimulate && bracketState.matches.length && !bracketState.champion) {
31781
31901
  const timer = setTimeout(autoPlay, 500);
31782
31902
  return () => clearTimeout(timer);
31783
31903
  }
31784
31904
  }, [autoSimulate]);
31785
- const stats = (0, import_react71.useMemo)(() => {
31905
+ const stats = (0, import_react70.useMemo)(() => {
31786
31906
  const done = bracketState.matches.filter((m) => m.winner).length;
31787
31907
  const total = bracketState.matches.length;
31788
31908
  const remaining = bracketState.matches.filter(
@@ -31790,7 +31910,7 @@ function BracketGenerator({
31790
31910
  ).length;
31791
31911
  return { done, total, remaining };
31792
31912
  }, [bracketState]);
31793
- const connectorPaths = (0, import_react71.useMemo)(() => {
31913
+ const connectorPaths = (0, import_react70.useMemo)(() => {
31794
31914
  const paths = [];
31795
31915
  bracketState.matches.forEach((m) => {
31796
31916
  if (m.round >= bracketState.rounds - 1) return;
@@ -32495,7 +32615,7 @@ function BracketGenerator({
32495
32615
  }
32496
32616
 
32497
32617
  // src/components/ui/coin-flipper/index.tsx
32498
- var import_react72 = require("react");
32618
+ var import_react71 = require("react");
32499
32619
  var import_jsx_runtime115 = require("react/jsx-runtime");
32500
32620
  var PRESETS3 = {
32501
32621
  gold: {
@@ -32679,15 +32799,15 @@ function CoinFlipper({
32679
32799
  }) {
32680
32800
  const dark = useDarkMode(darkMode);
32681
32801
  const tc = typeof theme5 === "string" ? PRESETS3[theme5] || PRESETS3.gold : theme5;
32682
- const [result, setResult] = (0, import_react72.useState)(null);
32683
- const [flipping, setFlipping] = (0, import_react72.useState)(false);
32684
- const [history, setHistory] = (0, import_react72.useState)([]);
32685
- const [stats, setStats] = (0, import_react72.useState)({ heads: 0, tails: 0 });
32686
- const flipIdRef = (0, import_react72.useRef)(0);
32687
- const coinRef = (0, import_react72.useRef)(null);
32688
- const shadowRef = (0, import_react72.useRef)(null);
32689
- const timersRef = (0, import_react72.useRef)([]);
32690
- const colors = (0, import_react72.useMemo)(
32802
+ const [result, setResult] = (0, import_react71.useState)(null);
32803
+ const [flipping, setFlipping] = (0, import_react71.useState)(false);
32804
+ const [history, setHistory] = (0, import_react71.useState)([]);
32805
+ const [stats, setStats] = (0, import_react71.useState)({ heads: 0, tails: 0 });
32806
+ const flipIdRef = (0, import_react71.useRef)(0);
32807
+ const coinRef = (0, import_react71.useRef)(null);
32808
+ const shadowRef = (0, import_react71.useRef)(null);
32809
+ const timersRef = (0, import_react71.useRef)([]);
32810
+ const colors = (0, import_react71.useMemo)(
32691
32811
  () => ({
32692
32812
  bg: dark ? "#0f1117" : "#f8fafc",
32693
32813
  surface: dark ? "#1a1d27" : "#ffffff",
@@ -32698,7 +32818,7 @@ function CoinFlipper({
32698
32818
  }),
32699
32819
  [dark]
32700
32820
  );
32701
- const flip = (0, import_react72.useCallback)(() => {
32821
+ const flip = (0, import_react71.useCallback)(() => {
32702
32822
  if (flipping) return;
32703
32823
  setFlipping(true);
32704
32824
  const outcome = Math.random() < 0.5 ? "heads" : "tails";
@@ -32738,16 +32858,19 @@ function CoinFlipper({
32738
32858
  }, frames.length * stepMs);
32739
32859
  timersRef.current.push(settleTimer);
32740
32860
  }, [flipping, onFlip, historyMax]);
32741
- (0, import_react72.useEffect)(() => {
32861
+ (0, import_react71.useEffect)(() => {
32742
32862
  if (flipRef) flipRef.current = flip;
32863
+ return () => {
32864
+ if (flipRef && flipRef.current === flip) flipRef.current = null;
32865
+ };
32743
32866
  }, [flip, flipRef]);
32744
- (0, import_react72.useEffect)(() => {
32867
+ (0, import_react71.useEffect)(() => {
32745
32868
  if (autoFlip) {
32746
32869
  const timer = setTimeout(flip, 300);
32747
32870
  return () => clearTimeout(timer);
32748
32871
  }
32749
32872
  }, []);
32750
- (0, import_react72.useEffect)(() => {
32873
+ (0, import_react71.useEffect)(() => {
32751
32874
  return () => {
32752
32875
  timersRef.current.forEach(clearTimeout);
32753
32876
  };
@@ -33184,6 +33307,23 @@ function CoinFlipper({
33184
33307
 
33185
33308
  // src/components/ui/random-player-picker/index.tsx
33186
33309
  var import_react73 = require("react");
33310
+
33311
+ // src/hooks/web/use-prefers-reduced-motion.ts
33312
+ var import_react72 = require("react");
33313
+ function usePrefersReducedMotion() {
33314
+ const [reduced, setReduced] = (0, import_react72.useState)(false);
33315
+ (0, import_react72.useEffect)(() => {
33316
+ if (typeof window === "undefined" || !window.matchMedia) return;
33317
+ const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
33318
+ setReduced(mq.matches);
33319
+ const read = () => setReduced(mq.matches);
33320
+ mq.addEventListener("change", read);
33321
+ return () => mq.removeEventListener("change", read);
33322
+ }, []);
33323
+ return reduced;
33324
+ }
33325
+
33326
+ // src/components/ui/random-player-picker/index.tsx
33187
33327
  var import_jsx_runtime116 = require("react/jsx-runtime");
33188
33328
  var PRESETS4 = {
33189
33329
  blue: {
@@ -33631,6 +33771,8 @@ function RandomPlayerPicker({
33631
33771
  const particleTimer = (0, import_react73.useRef)(null);
33632
33772
  const popTimer = (0, import_react73.useRef)(null);
33633
33773
  const tickInterval = (0, import_react73.useRef)(null);
33774
+ const pendingPopIdxRef = (0, import_react73.useRef)(null);
33775
+ const removeWinnersRef = (0, import_react73.useRef)(removeWinners);
33634
33776
  const tickAudioRef = (0, import_react73.useRef)(null);
33635
33777
  const winAudioRef = (0, import_react73.useRef)(null);
33636
33778
  const containerRef = (0, import_react73.useRef)(null);
@@ -33669,10 +33811,27 @@ function RandomPlayerPicker({
33669
33811
  winAudioRef.current = null;
33670
33812
  };
33671
33813
  }, [winSoundUrl, soundVolume]);
33814
+ (0, import_react73.useEffect)(() => {
33815
+ removeWinnersRef.current = removeWinners;
33816
+ }, [removeWinners]);
33672
33817
  (0, import_react73.useEffect)(() => {
33673
33818
  const ids = participants.map((p) => p.id).join("|");
33674
33819
  const structuralChange = ids !== prevIdsRef.current;
33675
33820
  if (structuralChange) {
33821
+ if (winnerTimer.current) {
33822
+ clearTimeout(winnerTimer.current);
33823
+ winnerTimer.current = null;
33824
+ }
33825
+ if (tickInterval.current) {
33826
+ clearTimeout(tickInterval.current);
33827
+ tickInterval.current = null;
33828
+ }
33829
+ if (popTimer.current) {
33830
+ clearTimeout(popTimer.current);
33831
+ popTimer.current = null;
33832
+ }
33833
+ pendingPopIdxRef.current = null;
33834
+ setSpinning(false);
33676
33835
  setPool(participants);
33677
33836
  setHistory([]);
33678
33837
  setWinner(null);
@@ -33729,33 +33888,50 @@ function RandomPlayerPicker({
33729
33888
  [dark]
33730
33889
  );
33731
33890
  const spin = (0, import_react73.useCallback)(() => {
33732
- if (spinning || n < 2) return;
33891
+ if (spinning) return;
33892
+ let activePool = pool;
33893
+ if (popTimer.current) {
33894
+ clearTimeout(popTimer.current);
33895
+ popTimer.current = null;
33896
+ const pendingIdx = pendingPopIdxRef.current;
33897
+ pendingPopIdxRef.current = null;
33898
+ if (pendingIdx != null && pendingIdx >= 0 && pendingIdx < pool.length && removeWinnersRef.current) {
33899
+ activePool = pool.filter((_, i) => i !== pendingIdx);
33900
+ setPool(activePool);
33901
+ setWinner(null);
33902
+ }
33903
+ }
33904
+ const activeN = activePool.length;
33905
+ if (activeN < 2) return;
33906
+ const activeAnglePerWedge = 2 * Math.PI / activeN;
33907
+ const activeRotation = rotation;
33908
+ const activeReelY = reelY;
33733
33909
  onSpinStart?.();
33734
- const winnerIdx = weightedIndex(pool);
33910
+ const winnerIdx = weightedIndex(activePool);
33735
33911
  const minRev = minRevolutions + intensityCfg.revsBoost;
33736
33912
  const maxRev = maxRevolutions + intensityCfg.revsBoost;
33737
33913
  const minFullRevs = Math.ceil(minRev);
33738
33914
  const maxFullRevs = Math.max(minFullRevs, Math.floor(maxRev));
33739
33915
  const revolutions = minFullRevs + Math.floor(Math.random() * (maxFullRevs - minFullRevs + 1));
33740
- const wedgeMidRad = winnerIdx * anglePerWedge + anglePerWedge / 2;
33916
+ const wedgeMidRad = winnerIdx * activeAnglePerWedge + activeAnglePerWedge / 2;
33741
33917
  const wedgeMidDeg = wedgeMidRad * 180 / Math.PI;
33742
- const wheelWobble = (Math.random() - 0.5) * (anglePerWedge * 180 / Math.PI) * 0.6;
33743
- const currentMod = (rotation % 360 + 360) % 360;
33918
+ const wheelWobble = (Math.random() - 0.5) * (activeAnglePerWedge * 180 / Math.PI) * 0.6;
33919
+ const currentMod = (activeRotation % 360 + 360) % 360;
33744
33920
  const targetMod = ((-wedgeMidDeg + wheelWobble) % 360 + 360) % 360;
33745
33921
  let deltaMod = targetMod - currentMod;
33746
33922
  if (deltaMod < 0) deltaMod += 360;
33747
- const finalRotation = rotation + deltaMod + revolutions * 360;
33923
+ const finalRotation = activeRotation + deltaMod + revolutions * 360;
33748
33924
  const centerOffsetRows = (reelVisibleRows - 1) / 2;
33749
33925
  const centerOffsetPx = centerOffsetRows * reelRowHeight;
33750
33926
  const currentCenterRow = Math.round(
33751
- (reelY + centerOffsetPx) / reelRowHeight
33927
+ (activeReelY + centerOffsetPx) / reelRowHeight
33752
33928
  );
33753
- const currentCenterMod = mod(currentCenterRow, n);
33754
- const targetRowOffset = (winnerIdx - currentCenterMod + n) % n;
33755
- const reelRevRows = Math.round(revolutions * n);
33756
- const finalReelY = reelY + (reelRevRows + targetRowOffset) * reelRowHeight;
33757
- const settledWinnerIdx = mode === "reel" ? reelIndexAtCenter(finalReelY, centerOffsetPx, reelRowHeight, n) : wheelIndexAtPointer(finalRotation, n);
33758
- const finalWinnerIdx = settledWinnerIdx >= 0 && settledWinnerIdx < pool.length ? settledWinnerIdx : winnerIdx;
33929
+ const currentCenterMod = mod(currentCenterRow, activeN);
33930
+ const targetRowOffset = (winnerIdx - currentCenterMod + activeN) % activeN;
33931
+ const reelRevRows = Math.round(revolutions * activeN);
33932
+ const finalReelY = activeReelY + (reelRevRows + targetRowOffset) * reelRowHeight;
33933
+ const settledWinnerIdx = mode === "reel" ? reelIndexAtCenter(finalReelY, centerOffsetPx, reelRowHeight, activeN) : wheelIndexAtPointer(finalRotation, activeN);
33934
+ const finalWinnerIdx = settledWinnerIdx >= 0 && settledWinnerIdx < activePool.length ? settledWinnerIdx : winnerIdx;
33759
33935
  const effectiveDuration = reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult);
33760
33936
  setSpinning(true);
33761
33937
  setWinner(null);
@@ -33768,7 +33944,7 @@ function RandomPlayerPicker({
33768
33944
  if ((onTick || tickAudioRef.current) && !reducedMotion) {
33769
33945
  const wedges = mode === "reel" ? Math.max(1, reelRevRows + targetRowOffset) : (() => {
33770
33946
  const totalDelta = deltaMod + revolutions * 360;
33771
- const wedgeDeg = anglePerWedge * 180 / Math.PI;
33947
+ const wedgeDeg = activeAnglePerWedge * 180 / Math.PI;
33772
33948
  return Math.max(
33773
33949
  1,
33774
33950
  Math.round(totalDelta / Math.max(wedgeDeg, 1))
@@ -33800,7 +33976,7 @@ function RandomPlayerPicker({
33800
33976
  schedule();
33801
33977
  }
33802
33978
  winnerTimer.current = setTimeout(() => {
33803
- const picked = pool[finalWinnerIdx];
33979
+ const picked = activePool[finalWinnerIdx];
33804
33980
  if (!picked) {
33805
33981
  setSpinning(false);
33806
33982
  return;
@@ -33846,7 +34022,11 @@ function RandomPlayerPicker({
33846
34022
  particleTimer.current = setTimeout(() => setParticles([]), 2400);
33847
34023
  }
33848
34024
  if (removeWinners) {
34025
+ pendingPopIdxRef.current = finalWinnerIdx;
33849
34026
  popTimer.current = setTimeout(() => {
34027
+ popTimer.current = null;
34028
+ pendingPopIdxRef.current = null;
34029
+ if (!removeWinnersRef.current) return;
33850
34030
  setPool((p) => p.filter((_, i) => i !== finalWinnerIdx));
33851
34031
  setWinner(null);
33852
34032
  setRotation(0);
@@ -33857,8 +34037,6 @@ function RandomPlayerPicker({
33857
34037
  }, [
33858
34038
  pool,
33859
34039
  spinning,
33860
- n,
33861
- anglePerWedge,
33862
34040
  rotation,
33863
34041
  reelY,
33864
34042
  mode,
@@ -33894,6 +34072,7 @@ function RandomPlayerPicker({
33894
34072
  []
33895
34073
  );
33896
34074
  (0, import_react73.useEffect)(() => {
34075
+ if (trigger === "external") return;
33897
34076
  const el = containerRef.current;
33898
34077
  if (!el) return;
33899
34078
  const h = (e) => {
@@ -33904,8 +34083,22 @@ function RandomPlayerPicker({
33904
34083
  };
33905
34084
  el.addEventListener("keydown", h);
33906
34085
  return () => el.removeEventListener("keydown", h);
33907
- }, [spin]);
34086
+ }, [spin, trigger]);
33908
34087
  const reset = (0, import_react73.useCallback)(() => {
34088
+ if (winnerTimer.current) {
34089
+ clearTimeout(winnerTimer.current);
34090
+ winnerTimer.current = null;
34091
+ }
34092
+ if (tickInterval.current) {
34093
+ clearTimeout(tickInterval.current);
34094
+ tickInterval.current = null;
34095
+ }
34096
+ if (popTimer.current) {
34097
+ clearTimeout(popTimer.current);
34098
+ popTimer.current = null;
34099
+ }
34100
+ pendingPopIdxRef.current = null;
34101
+ setSpinning(false);
33909
34102
  setPool(participants);
33910
34103
  setHistory([]);
33911
34104
  setWinner(null);
@@ -38358,27 +38551,28 @@ function GameTimer({
38358
38551
  const now = performance.now();
38359
38552
  const delta = now - lastTickRef.current;
38360
38553
  lastTickRef.current = now;
38361
- setElapsedMs((prev) => {
38362
- const next = prev + delta;
38363
- if (mode === "countdown" && next >= durationMs) {
38364
- setRunning(false);
38365
- setCompleted(true);
38366
- onComplete?.();
38367
- return durationMs;
38554
+ const next = elapsedRef.current + delta;
38555
+ if (mode === "countdown" && next >= durationMs) {
38556
+ elapsedRef.current = durationMs;
38557
+ setElapsedMs(durationMs);
38558
+ setRunning(false);
38559
+ setCompleted(true);
38560
+ onComplete?.();
38561
+ return;
38562
+ }
38563
+ if (mode === "countdown") {
38564
+ const secLeft = Math.ceil((durationMs - next) / 1e3);
38565
+ if (secLeft <= warningAt && !warningFiredRef.current) {
38566
+ warningFiredRef.current = true;
38567
+ onWarning?.();
38368
38568
  }
38369
- if (mode === "countdown") {
38370
- const secLeft = Math.ceil((durationMs - next) / 1e3);
38371
- if (secLeft <= warningAt && !warningFiredRef.current) {
38372
- warningFiredRef.current = true;
38373
- onWarning?.();
38374
- }
38375
- if (secLeft <= dangerAt && !dangerFiredRef.current) {
38376
- dangerFiredRef.current = true;
38377
- onDanger?.();
38378
- }
38569
+ if (secLeft <= dangerAt && !dangerFiredRef.current) {
38570
+ dangerFiredRef.current = true;
38571
+ onDanger?.();
38379
38572
  }
38380
- return next;
38381
- });
38573
+ }
38574
+ elapsedRef.current = next;
38575
+ setElapsedMs(next);
38382
38576
  rafRef.current = requestAnimationFrame(tick);
38383
38577
  }, [mode, durationMs, warningAt, dangerAt, onComplete, onWarning, onDanger]);
38384
38578
  (0, import_react76.useEffect)(() => {
@@ -38399,6 +38593,7 @@ function GameTimer({
38399
38593
  }, []);
38400
38594
  const reset = (0, import_react76.useCallback)(() => {
38401
38595
  setRunning(false);
38596
+ elapsedRef.current = 0;
38402
38597
  setElapsedMs(0);
38403
38598
  setCompleted(false);
38404
38599
  setLaps([]);
@@ -40338,6 +40533,8 @@ function RoundRobinScheduler({
40338
40533
  theme: theme5 = "slate",
40339
40534
  darkMode,
40340
40535
  minTeams = 3,
40536
+ maxTeams,
40537
+ nameMaxLength,
40341
40538
  defaultTeams = [],
40342
40539
  resumeRounds,
40343
40540
  onTeamsChange,
@@ -40360,39 +40557,41 @@ function RoundRobinScheduler({
40360
40557
  (0, import_react79.useEffect)(() => {
40361
40558
  onTeamsChange?.(teams);
40362
40559
  }, [teams, onTeamsChange]);
40363
- const addTeams = (0, import_react79.useCallback)((raw) => {
40364
- const names = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
40365
- if (names.length === 0) return;
40366
- setTeams((prev) => {
40367
- const existing = new Set(prev.map((t) => t.toLowerCase()));
40368
- const next = [...prev];
40369
- for (const name of names) {
40370
- if (!existing.has(name.toLowerCase())) {
40371
- existing.add(name.toLowerCase());
40372
- next.push(name);
40373
- }
40374
- }
40375
- return next;
40376
- });
40377
- setInput("");
40378
- setRounds([]);
40379
- }, []);
40380
- const importTeams = (0, import_react79.useCallback)((incoming) => {
40381
- if (incoming.length === 0) return;
40382
- setTeams((prev) => {
40560
+ const mergeNames = (0, import_react79.useCallback)(
40561
+ (prev, incoming) => {
40383
40562
  const existing = new Set(prev.map((t) => t.toLowerCase()));
40384
40563
  const next = [...prev];
40385
- for (const name of incoming) {
40564
+ for (const rawName of incoming) {
40565
+ if (maxTeams != null && next.length >= maxTeams) break;
40566
+ const name = nameMaxLength != null ? rawName.slice(0, nameMaxLength) : rawName;
40386
40567
  const key = name.toLowerCase();
40387
- if (!existing.has(key)) {
40568
+ if (name.length > 0 && !existing.has(key)) {
40388
40569
  existing.add(key);
40389
40570
  next.push(name);
40390
40571
  }
40391
40572
  }
40392
40573
  return next;
40393
- });
40394
- setRounds([]);
40395
- }, []);
40574
+ },
40575
+ [maxTeams, nameMaxLength]
40576
+ );
40577
+ const addTeams = (0, import_react79.useCallback)(
40578
+ (raw) => {
40579
+ const names = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
40580
+ if (names.length === 0) return;
40581
+ setTeams((prev) => mergeNames(prev, names));
40582
+ setInput("");
40583
+ setRounds([]);
40584
+ },
40585
+ [mergeNames]
40586
+ );
40587
+ const importTeams = (0, import_react79.useCallback)(
40588
+ (incoming) => {
40589
+ if (incoming.length === 0) return;
40590
+ setTeams((prev) => mergeNames(prev, incoming));
40591
+ setRounds([]);
40592
+ },
40593
+ [mergeNames]
40594
+ );
40396
40595
  const handleKeyDown = (0, import_react79.useCallback)(
40397
40596
  (e) => {
40398
40597
  if (e.key === "Enter") {
@@ -40881,6 +41080,8 @@ function Scoreboard({
40881
41080
  onTeamsChange,
40882
41081
  onRoundAdvance,
40883
41082
  onNewGame,
41083
+ onReset,
41084
+ nameMaxLength,
40884
41085
  labels,
40885
41086
  ariaLabel = "Scoreboard",
40886
41087
  className
@@ -40955,7 +41156,8 @@ function Scoreboard({
40955
41156
  setRound(1);
40956
41157
  setHistory([]);
40957
41158
  setHistoryOpen(false);
40958
- }, []);
41159
+ onReset?.();
41160
+ }, [onReset]);
40959
41161
  const newGame = (0, import_react80.useCallback)(() => {
40960
41162
  setTeams(createDefaultTeams(seedCount, L.teamNamePrefix));
40961
41163
  setRound(1);
@@ -41177,6 +41379,7 @@ function Scoreboard({
41177
41379
  {
41178
41380
  type: "text",
41179
41381
  value: team.name,
41382
+ maxLength: nameMaxLength,
41180
41383
  onChange: (e) => updateName(index, e.target.value),
41181
41384
  style: nameInputStyle,
41182
41385
  "aria-label": `Team ${index + 1} name`