react-raffle-picker 0.1.0 → 0.2.1

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/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  <div align="center">
2
- <img src="https://i.ibb.co/k2MRQCqT/logo-nbg.png" alt="react-raffle-picker logo placeholder" width="160" />
2
+ <img src="https://i.ibb.co/yB50hJTv/logo-nbg.png" alt="react-raffle-picker logo placeholder" width="160" />
3
3
 
4
4
  <h1>react-raffle-picker</h1>
5
5
 
6
- <p><strong>Tiny raffle engine. Big winner energy.</strong></p>
6
+ <p><strong>React like you just won. The raffle picker your UI deserves.</strong></p>
7
7
 
8
8
  <p>
9
9
  <a href="https://react-raffle-one.vercel.app/">Live demo</a>
@@ -69,31 +69,48 @@ This means:
69
69
  ```tsx
70
70
  import { RafflePick } from 'react-raffle-picker'
71
71
 
72
- <RafflePick min={1} max={100} interval={100} inertia onSelect={(v) => console.log(v)}>
72
+ ;<RafflePick min={1} max={100} interval={100} inertia onSelect={(v) => console.log(v)}>
73
73
  <RafflePick.Value animation="roll" className="my-value" />
74
74
  <RafflePick.Button startLabel="Pick" stopLabel="Stop" />
75
75
  </RafflePick>
76
76
  ```
77
77
 
78
+ ### Styles
79
+
80
+ The library is headless — no global stylesheet is required for `Value`, `Button`, or `Countdown`. You bring your own CSS.
81
+
82
+ For the **slot reel** (`<RafflePick.Slots>`), a minimal stylesheet _is_ required to make the column animate. Two ways to load it:
83
+
84
+ 1. **Auto-injected at runtime** — `<RafflePick.Slots>` injects a `<style data-rrp-slot-base>` tag into `document.head` on first mount. No action needed in CSR apps.
85
+ 2. **Static import** (recommended for SSR / strict CSP / full control):
86
+
87
+ ```ts
88
+ import 'react-raffle-picker/styles.css'
89
+ ```
90
+
91
+ This file also includes opt-in keyframes for `<RafflePick.Value animation="roll|fade|blur|reel" />`. Override or replace any selector in your own CSS.
92
+
78
93
  ## Components
79
94
 
80
95
  ### `<RafflePick>` (root)
81
96
 
82
97
  Provides context. Renders an optional wrapper element (`as` prop, default `'div'`).
83
98
 
84
- | Prop | Type | Default | Notes |
85
- | ------------ | --------------------------- | ---------- | --------------------------------------------- |
86
- | `min`, `max` | `number` | `1`, `100` | Numeric range. Ignored if `items` provided. |
87
- | `items` | `string[]` | — | Switches to item mode (cycles through names). |
88
- | `interval` | `number` (ms, clamped ≥ 50) | `100` | Tick speed. |
89
- | `random` | `boolean` | `true` | Random pick vs sequential. |
90
- | `inertia` | `boolean` | `false` | Soft start / soft stop ramp. |
91
- | `autoStart` | `boolean` | `true` | Begin cycling on mount. |
92
- | `onSelect` | `(value) => void` | — | Fires once per round on freeze. |
93
- | `as` | `ElementType` | `'div'` | Wrapper tag. |
94
- | `className` | `string` | — | Wrapper class. |
95
- | `style` | `CSSProperties` | | Wrapper style. |
96
- | `children` | `ReactNode` | — | Sub-components. |
99
+ | Prop | Type | Default | Notes |
100
+ | -------------- | --------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
101
+ | `min`, `max` | `number` | `1`, `100` | Numeric range. Ignored if `items` provided. |
102
+ | `items` | `string[]` | — | Switches to item mode (cycles through names). |
103
+ | `interval` | `number` (ms, clamped ≥ 50) | `100` | Tick speed. |
104
+ | `random` | `boolean` | `true` | Random pick vs sequential. |
105
+ | `inertia` | `boolean` | `false` | Soft start / soft stop ramp. |
106
+ | `autoStart` | `boolean` | `true` | Begin cycling on mount. |
107
+ | `initialValue` | `number \| string` | — | Starting display before first run. Number for `min`/`max` mode, string for `items` mode. For `<Slots>`, each character seeds the corresponding reel. |
108
+ | `finalValue` | `number \| string` | | Forces settle to land on this value. Cycle still appears random; only final freeze is rigged. For `<Slots>`, each character is the final char of the corresponding reel. |
109
+ | `onSelect` | `(value) => void` | — | Fires once per round on freeze. |
110
+ | `as` | `ElementType` | `'div'` | Wrapper tag. |
111
+ | `className` | `string` | — | Wrapper class. |
112
+ | `style` | `CSSProperties` | — | Wrapper style. |
113
+ | `children` | `ReactNode` | — | Sub-components. |
97
114
 
98
115
  ### `<RafflePick.Value>`
99
116
 
@@ -136,6 +153,8 @@ Schedules auto-freeze after `seconds`. Renders an SVG ring + numeric label by de
136
153
 
137
154
  Independent multi-reel slot machine. Each reel ticks on its own and stops with a stagger.
138
155
 
156
+ `initialValue` (root prop) seeds reels char-by-char before the first run. `finalValue` rigs the freeze so each reel lands on the corresponding character — useful for predetermined winners or scripted demos.
157
+
139
158
  | Prop | Type | Default | Notes |
140
159
  | -------------------------------------------------- | -------------------------- | -------------- | -------------------------------------------- |
141
160
  | `length` | `number` | `3` | Number of reels. |
@@ -166,6 +185,23 @@ Independent multi-reel slot machine. Each reel ticks on its own and stops with a
166
185
  </RafflePick>
167
186
  ```
168
187
 
188
+ ### Rigged draw with predetermined winner
189
+
190
+ `finalValue` lands the freeze on a specific value while the cycle still looks random — useful for staged demos, scripted reveals, or showing a known winner.
191
+
192
+ ```tsx
193
+ <RafflePick
194
+ items={['Alice', 'Bob', 'Carol']}
195
+ initialValue="Alice"
196
+ finalValue="Bob"
197
+ autoStart={false}
198
+ onSelect={(winner) => console.log(winner)} // always 'Bob'
199
+ >
200
+ <RafflePick.Value animation="reel" />
201
+ <RafflePick.Button startLabel="Draw" stopLabel="Reveal" />
202
+ </RafflePick>
203
+ ```
204
+
169
205
  ### Slot machine with custom result handler
170
206
 
171
207
  ```tsx
package/dist/index.cjs CHANGED
@@ -172,7 +172,7 @@ const useRaffleContext = (componentName) => {
172
172
  };
173
173
  //#endregion
174
174
  //#region src/components/RafflePick/RafflePick.tsx
175
- function RafflePickRoot({ items, min = 1, max = 100, interval = 100, random = true, inertia = false, autoStart = true, onSelect, as = "div", className, style, children }) {
175
+ function RafflePickRoot({ items, min = 1, max = 100, interval = 100, random = true, inertia = false, autoStart = true, initialValue, finalValue, onSelect, as = "div", className, style, children }) {
176
176
  const itemCount = items?.length ?? 0;
177
177
  const hasItems = itemCount > 0;
178
178
  const cycleMin = hasItems ? 0 : min;
@@ -186,7 +186,33 @@ function RafflePickRoot({ items, min = 1, max = 100, interval = 100, random = tr
186
186
  const its = itemsRef.current;
187
187
  return its && its.length > 0 ? its[index] : index;
188
188
  }, []);
189
- const [displayed, setDisplayed] = (0, react.useState)(() => items && items.length > 0 ? items[cycleMin] : cycleMin);
189
+ const valueToIndex = (0, react.useCallback)((value) => {
190
+ if (value === void 0) return void 0;
191
+ const its = itemsRef.current;
192
+ if (its && its.length > 0) {
193
+ const i = its.indexOf(String(value));
194
+ return i >= 0 ? i : void 0;
195
+ }
196
+ return typeof value === "number" ? value : void 0;
197
+ }, []);
198
+ const initialIndex = (() => {
199
+ if (initialValue === void 0) return cycleMin;
200
+ if (items && items.length > 0) {
201
+ const i = items.indexOf(String(initialValue));
202
+ return i >= 0 ? i : cycleMin;
203
+ }
204
+ return typeof initialValue === "number" ? initialValue : cycleMin;
205
+ })();
206
+ const [displayed, setDisplayed] = (0, react.useState)(() => {
207
+ if (initialValue !== void 0) {
208
+ const its = items;
209
+ if (its && its.length > 0) {
210
+ const i = its.indexOf(String(initialValue));
211
+ if (i >= 0) return its[i];
212
+ } else if (typeof initialValue === "number") return initialValue;
213
+ }
214
+ return items && items.length > 0 ? items[cycleMin] : cycleMin;
215
+ });
190
216
  const subscribersRef = (0, react.useRef)(/* @__PURE__ */ new Set());
191
217
  const subscribe = (0, react.useCallback)((fn) => {
192
218
  subscribersRef.current.add(fn);
@@ -197,12 +223,22 @@ function RafflePickRoot({ items, min = 1, max = 100, interval = 100, random = tr
197
223
  const onTick = (0, react.useCallback)((value) => {
198
224
  subscribersRef.current.forEach((fn) => fn(value));
199
225
  }, []);
200
- const valueRef = (0, react.useRef)(cycleMin);
226
+ const valueRef = (0, react.useRef)(initialIndex);
227
+ const finalValueRef = (0, react.useRef)(finalValue);
228
+ (0, react.useEffect)(() => {
229
+ finalValueRef.current = finalValue;
230
+ }, [finalValue]);
201
231
  const { phase, step, start, freeze, reset } = useRafflePhase(inertia, initialPhase, (0, react.useCallback)(() => {
232
+ const forced = valueToIndex(finalValueRef.current);
233
+ if (forced !== void 0) valueRef.current = forced;
202
234
  const v = displayValue(valueRef.current);
203
235
  setDisplayed(v);
204
236
  onSelect?.(v);
205
- }, [displayValue, onSelect]));
237
+ }, [
238
+ displayValue,
239
+ onSelect,
240
+ valueToIndex
241
+ ]));
206
242
  const multiplier = getInertiaMultiplier(phase, step, inertia);
207
243
  const cycleInterval = Math.round(Math.max(50, interval) * multiplier);
208
244
  useNumberCycle({
@@ -221,7 +257,9 @@ function RafflePickRoot({ items, min = 1, max = 100, interval = 100, random = tr
221
257
  cycleInterval,
222
258
  inertia,
223
259
  hasItems,
224
- initialIndex: cycleMin,
260
+ initialIndex,
261
+ initialValue,
262
+ finalValue,
225
263
  valueRef,
226
264
  displayValue,
227
265
  subscribe,
@@ -235,7 +273,9 @@ function RafflePickRoot({ items, min = 1, max = 100, interval = 100, random = tr
235
273
  cycleInterval,
236
274
  inertia,
237
275
  hasItems,
238
- cycleMin,
276
+ initialIndex,
277
+ initialValue,
278
+ finalValue,
239
279
  displayValue,
240
280
  subscribe,
241
281
  start,
@@ -388,6 +428,27 @@ function CountdownRunning({ seconds, className, style, children }) {
388
428
  //#endregion
389
429
  //#region src/components/RafflePick/RafflePickSlots.tsx
390
430
  const pickRandom = (pool) => pool[Math.floor(Math.random() * pool.length)] ?? "";
431
+ const SLOT_BASE_CSS = `
432
+ .rrp-slot{display:inline-block;overflow:hidden;height:1em;line-height:1em;vertical-align:baseline}
433
+ .rrp-slot__col{display:flex;flex-direction:column;transform:translateY(-1em);animation:rrp-slot-reel var(--rrp-tick,80ms) linear infinite}
434
+ .rrp-slot__cell{display:block;height:1em;line-height:1em}
435
+ .rrp-slot[data-stopped] .rrp-slot__col{animation:none;transform:translateY(-1em)}
436
+ @keyframes rrp-slot-reel{from{transform:translateY(-1em)}to{transform:translateY(-2em)}}
437
+ `;
438
+ let slotStylesInjected = false;
439
+ const injectSlotStyles = () => {
440
+ if (slotStylesInjected) return;
441
+ if (typeof document === "undefined") return;
442
+ if (document.querySelector("style[data-rrp-slot-base]")) {
443
+ slotStylesInjected = true;
444
+ return;
445
+ }
446
+ const tag = document.createElement("style");
447
+ tag.setAttribute("data-rrp-slot-base", "");
448
+ tag.textContent = SLOT_BASE_CSS;
449
+ document.head.appendChild(tag);
450
+ slotStylesInjected = true;
451
+ };
391
452
  const makeRefs = () => ({
392
453
  root: null,
393
454
  col: null,
@@ -398,7 +459,20 @@ const makeRefs = () => ({
398
459
  stopped: false
399
460
  });
400
461
  function RafflePickSlots({ length = 3, chars = "0123456789", spinInterval = 80, staggerMs = 220, className, slotClassName, style, slotStyle, onResult }) {
401
- const { phase } = useRaffleContext("RafflePick.Slots");
462
+ injectSlotStyles();
463
+ const { phase, initialValue, finalValue } = useRaffleContext("RafflePick.Slots");
464
+ const initialChars = (0, react.useMemo)(() => {
465
+ const seed = typeof initialValue === "string" ? initialValue : "";
466
+ return Array.from({ length }, (_, i) => seed[i] ?? "");
467
+ }, [initialValue, length]);
468
+ const finalChars = (0, react.useMemo)(() => {
469
+ if (typeof finalValue !== "string") return null;
470
+ return Array.from({ length }, (_, i) => finalValue[i] ?? "");
471
+ }, [finalValue, length]);
472
+ const finalCharsRef = (0, react.useRef)(finalChars);
473
+ (0, react.useEffect)(() => {
474
+ finalCharsRef.current = finalChars;
475
+ }, [finalChars]);
402
476
  const slotsRef = (0, react.useRef)([]);
403
477
  const stopTimersRef = (0, react.useRef)([]);
404
478
  const finalRef = (0, react.useRef)([]);
@@ -456,6 +530,8 @@ function RafflePickSlots({ length = 3, chars = "0123456789", spinInterval = 80,
456
530
  for (let i = 0; i < length; i++) {
457
531
  const id = setTimeout(() => {
458
532
  const s = slotsRef.current[i];
533
+ const forced = finalCharsRef.current?.[i];
534
+ if (forced) writeSlot(s, s.currChar, forced, forced);
459
535
  s.stopped = true;
460
536
  if (s.root) s.root.setAttribute("data-stopped", "");
461
537
  finalRef.current[i] = s.currChar;
@@ -471,17 +547,19 @@ function RafflePickSlots({ length = 3, chars = "0123456789", spinInterval = 80,
471
547
  ]);
472
548
  (0, react.useEffect)(() => {
473
549
  if (phase !== "idle") return;
474
- const c = pool[0] ?? "";
550
+ const fallback = pool[0] ?? "";
475
551
  for (let i = 0; i < length; i++) {
476
552
  const s = slotsRef.current[i];
553
+ const c = initialChars[i] || fallback;
477
554
  s.stopped = true;
478
555
  writeSlot(s, c, c, c);
479
- if (s.root) s.root.removeAttribute("data-stopped");
556
+ if (s.root) s.root.setAttribute("data-stopped", "");
480
557
  }
481
558
  }, [
482
559
  phase,
483
560
  length,
484
- pool
561
+ pool,
562
+ initialChars
485
563
  ]);
486
564
  const cls = (0, react.useMemo)(() => joinClassNames("rrp-slots", className), [className]);
487
565
  const slotCls = (0, react.useMemo)(() => joinClassNames("rrp-slot", slotClassName), [slotClassName]);
package/dist/index.d.cts CHANGED
@@ -13,6 +13,10 @@ interface RafflePickRootProps {
13
13
  random?: boolean;
14
14
  inertia?: boolean;
15
15
  autoStart?: boolean;
16
+ /** Value shown before first run. Number in min/max mode, string in items mode. */
17
+ initialValue?: RafflePickValue$1;
18
+ /** When set, settle always lands on this value while cycle still appears random. */
19
+ finalValue?: RafflePickValue$1;
16
20
  onSelect?: (value: RafflePickValue$1) => void;
17
21
  as?: ElementType;
18
22
  className?: string;
@@ -71,6 +75,8 @@ declare function RafflePickRoot({
71
75
  random,
72
76
  inertia,
73
77
  autoStart,
78
+ initialValue,
79
+ finalValue,
74
80
  onSelect,
75
81
  as,
76
82
  className,
@@ -129,6 +135,8 @@ interface RaffleContextValue {
129
135
  inertia: boolean;
130
136
  hasItems: boolean;
131
137
  initialIndex: number;
138
+ initialValue?: RafflePickValue$1;
139
+ finalValue?: RafflePickValue$1;
132
140
  valueRef: RefObject<number>;
133
141
  displayValue: (index: number) => RafflePickValue$1;
134
142
  subscribe: (fn: (value: number) => void) => () => void;
package/dist/index.d.mts CHANGED
@@ -13,6 +13,10 @@ interface RafflePickRootProps {
13
13
  random?: boolean;
14
14
  inertia?: boolean;
15
15
  autoStart?: boolean;
16
+ /** Value shown before first run. Number in min/max mode, string in items mode. */
17
+ initialValue?: RafflePickValue$1;
18
+ /** When set, settle always lands on this value while cycle still appears random. */
19
+ finalValue?: RafflePickValue$1;
16
20
  onSelect?: (value: RafflePickValue$1) => void;
17
21
  as?: ElementType;
18
22
  className?: string;
@@ -71,6 +75,8 @@ declare function RafflePickRoot({
71
75
  random,
72
76
  inertia,
73
77
  autoStart,
78
+ initialValue,
79
+ finalValue,
74
80
  onSelect,
75
81
  as,
76
82
  className,
@@ -129,6 +135,8 @@ interface RaffleContextValue {
129
135
  inertia: boolean;
130
136
  hasItems: boolean;
131
137
  initialIndex: number;
138
+ initialValue?: RafflePickValue$1;
139
+ finalValue?: RafflePickValue$1;
132
140
  valueRef: RefObject<number>;
133
141
  displayValue: (index: number) => RafflePickValue$1;
134
142
  subscribe: (fn: (value: number) => void) => () => void;
package/dist/index.mjs CHANGED
@@ -171,7 +171,7 @@ const useRaffleContext = (componentName) => {
171
171
  };
172
172
  //#endregion
173
173
  //#region src/components/RafflePick/RafflePick.tsx
174
- function RafflePickRoot({ items, min = 1, max = 100, interval = 100, random = true, inertia = false, autoStart = true, onSelect, as = "div", className, style, children }) {
174
+ function RafflePickRoot({ items, min = 1, max = 100, interval = 100, random = true, inertia = false, autoStart = true, initialValue, finalValue, onSelect, as = "div", className, style, children }) {
175
175
  const itemCount = items?.length ?? 0;
176
176
  const hasItems = itemCount > 0;
177
177
  const cycleMin = hasItems ? 0 : min;
@@ -185,7 +185,33 @@ function RafflePickRoot({ items, min = 1, max = 100, interval = 100, random = tr
185
185
  const its = itemsRef.current;
186
186
  return its && its.length > 0 ? its[index] : index;
187
187
  }, []);
188
- const [displayed, setDisplayed] = useState(() => items && items.length > 0 ? items[cycleMin] : cycleMin);
188
+ const valueToIndex = useCallback((value) => {
189
+ if (value === void 0) return void 0;
190
+ const its = itemsRef.current;
191
+ if (its && its.length > 0) {
192
+ const i = its.indexOf(String(value));
193
+ return i >= 0 ? i : void 0;
194
+ }
195
+ return typeof value === "number" ? value : void 0;
196
+ }, []);
197
+ const initialIndex = (() => {
198
+ if (initialValue === void 0) return cycleMin;
199
+ if (items && items.length > 0) {
200
+ const i = items.indexOf(String(initialValue));
201
+ return i >= 0 ? i : cycleMin;
202
+ }
203
+ return typeof initialValue === "number" ? initialValue : cycleMin;
204
+ })();
205
+ const [displayed, setDisplayed] = useState(() => {
206
+ if (initialValue !== void 0) {
207
+ const its = items;
208
+ if (its && its.length > 0) {
209
+ const i = its.indexOf(String(initialValue));
210
+ if (i >= 0) return its[i];
211
+ } else if (typeof initialValue === "number") return initialValue;
212
+ }
213
+ return items && items.length > 0 ? items[cycleMin] : cycleMin;
214
+ });
189
215
  const subscribersRef = useRef(/* @__PURE__ */ new Set());
190
216
  const subscribe = useCallback((fn) => {
191
217
  subscribersRef.current.add(fn);
@@ -196,12 +222,22 @@ function RafflePickRoot({ items, min = 1, max = 100, interval = 100, random = tr
196
222
  const onTick = useCallback((value) => {
197
223
  subscribersRef.current.forEach((fn) => fn(value));
198
224
  }, []);
199
- const valueRef = useRef(cycleMin);
225
+ const valueRef = useRef(initialIndex);
226
+ const finalValueRef = useRef(finalValue);
227
+ useEffect(() => {
228
+ finalValueRef.current = finalValue;
229
+ }, [finalValue]);
200
230
  const { phase, step, start, freeze, reset } = useRafflePhase(inertia, initialPhase, useCallback(() => {
231
+ const forced = valueToIndex(finalValueRef.current);
232
+ if (forced !== void 0) valueRef.current = forced;
201
233
  const v = displayValue(valueRef.current);
202
234
  setDisplayed(v);
203
235
  onSelect?.(v);
204
- }, [displayValue, onSelect]));
236
+ }, [
237
+ displayValue,
238
+ onSelect,
239
+ valueToIndex
240
+ ]));
205
241
  const multiplier = getInertiaMultiplier(phase, step, inertia);
206
242
  const cycleInterval = Math.round(Math.max(50, interval) * multiplier);
207
243
  useNumberCycle({
@@ -220,7 +256,9 @@ function RafflePickRoot({ items, min = 1, max = 100, interval = 100, random = tr
220
256
  cycleInterval,
221
257
  inertia,
222
258
  hasItems,
223
- initialIndex: cycleMin,
259
+ initialIndex,
260
+ initialValue,
261
+ finalValue,
224
262
  valueRef,
225
263
  displayValue,
226
264
  subscribe,
@@ -234,7 +272,9 @@ function RafflePickRoot({ items, min = 1, max = 100, interval = 100, random = tr
234
272
  cycleInterval,
235
273
  inertia,
236
274
  hasItems,
237
- cycleMin,
275
+ initialIndex,
276
+ initialValue,
277
+ finalValue,
238
278
  displayValue,
239
279
  subscribe,
240
280
  start,
@@ -387,6 +427,27 @@ function CountdownRunning({ seconds, className, style, children }) {
387
427
  //#endregion
388
428
  //#region src/components/RafflePick/RafflePickSlots.tsx
389
429
  const pickRandom = (pool) => pool[Math.floor(Math.random() * pool.length)] ?? "";
430
+ const SLOT_BASE_CSS = `
431
+ .rrp-slot{display:inline-block;overflow:hidden;height:1em;line-height:1em;vertical-align:baseline}
432
+ .rrp-slot__col{display:flex;flex-direction:column;transform:translateY(-1em);animation:rrp-slot-reel var(--rrp-tick,80ms) linear infinite}
433
+ .rrp-slot__cell{display:block;height:1em;line-height:1em}
434
+ .rrp-slot[data-stopped] .rrp-slot__col{animation:none;transform:translateY(-1em)}
435
+ @keyframes rrp-slot-reel{from{transform:translateY(-1em)}to{transform:translateY(-2em)}}
436
+ `;
437
+ let slotStylesInjected = false;
438
+ const injectSlotStyles = () => {
439
+ if (slotStylesInjected) return;
440
+ if (typeof document === "undefined") return;
441
+ if (document.querySelector("style[data-rrp-slot-base]")) {
442
+ slotStylesInjected = true;
443
+ return;
444
+ }
445
+ const tag = document.createElement("style");
446
+ tag.setAttribute("data-rrp-slot-base", "");
447
+ tag.textContent = SLOT_BASE_CSS;
448
+ document.head.appendChild(tag);
449
+ slotStylesInjected = true;
450
+ };
390
451
  const makeRefs = () => ({
391
452
  root: null,
392
453
  col: null,
@@ -397,7 +458,20 @@ const makeRefs = () => ({
397
458
  stopped: false
398
459
  });
399
460
  function RafflePickSlots({ length = 3, chars = "0123456789", spinInterval = 80, staggerMs = 220, className, slotClassName, style, slotStyle, onResult }) {
400
- const { phase } = useRaffleContext("RafflePick.Slots");
461
+ injectSlotStyles();
462
+ const { phase, initialValue, finalValue } = useRaffleContext("RafflePick.Slots");
463
+ const initialChars = useMemo(() => {
464
+ const seed = typeof initialValue === "string" ? initialValue : "";
465
+ return Array.from({ length }, (_, i) => seed[i] ?? "");
466
+ }, [initialValue, length]);
467
+ const finalChars = useMemo(() => {
468
+ if (typeof finalValue !== "string") return null;
469
+ return Array.from({ length }, (_, i) => finalValue[i] ?? "");
470
+ }, [finalValue, length]);
471
+ const finalCharsRef = useRef(finalChars);
472
+ useEffect(() => {
473
+ finalCharsRef.current = finalChars;
474
+ }, [finalChars]);
401
475
  const slotsRef = useRef([]);
402
476
  const stopTimersRef = useRef([]);
403
477
  const finalRef = useRef([]);
@@ -455,6 +529,8 @@ function RafflePickSlots({ length = 3, chars = "0123456789", spinInterval = 80,
455
529
  for (let i = 0; i < length; i++) {
456
530
  const id = setTimeout(() => {
457
531
  const s = slotsRef.current[i];
532
+ const forced = finalCharsRef.current?.[i];
533
+ if (forced) writeSlot(s, s.currChar, forced, forced);
458
534
  s.stopped = true;
459
535
  if (s.root) s.root.setAttribute("data-stopped", "");
460
536
  finalRef.current[i] = s.currChar;
@@ -470,17 +546,19 @@ function RafflePickSlots({ length = 3, chars = "0123456789", spinInterval = 80,
470
546
  ]);
471
547
  useEffect(() => {
472
548
  if (phase !== "idle") return;
473
- const c = pool[0] ?? "";
549
+ const fallback = pool[0] ?? "";
474
550
  for (let i = 0; i < length; i++) {
475
551
  const s = slotsRef.current[i];
552
+ const c = initialChars[i] || fallback;
476
553
  s.stopped = true;
477
554
  writeSlot(s, c, c, c);
478
- if (s.root) s.root.removeAttribute("data-stopped");
555
+ if (s.root) s.root.setAttribute("data-stopped", "");
479
556
  }
480
557
  }, [
481
558
  phase,
482
559
  length,
483
- pool
560
+ pool,
561
+ initialChars
484
562
  ]);
485
563
  const cls = useMemo(() => joinClassNames("rrp-slots", className), [className]);
486
564
  const slotCls = useMemo(() => joinClassNames("rrp-slot", slotClassName), [slotClassName]);
@@ -0,0 +1,63 @@
1
+ /* react-raffle-picker — base styles */
2
+ /* Optional. Slot reel animation is also auto-injected at runtime. */
3
+ /* Import via: import 'react-raffle-picker/styles.css' */
4
+
5
+ .rrp-slot {
6
+ display: inline-block;
7
+ overflow: hidden;
8
+ height: 1em;
9
+ line-height: 1em;
10
+ vertical-align: baseline;
11
+ }
12
+ .rrp-slot__col {
13
+ display: flex;
14
+ flex-direction: column;
15
+ transform: translateY(-1em);
16
+ animation: rrp-slot-reel var(--rrp-tick, 80ms) linear infinite;
17
+ }
18
+ .rrp-slot__cell {
19
+ display: block;
20
+ height: 1em;
21
+ line-height: 1em;
22
+ }
23
+ .rrp-slot[data-stopped] .rrp-slot__col {
24
+ animation: none;
25
+ transform: translateY(-1em);
26
+ }
27
+ @keyframes rrp-slot-reel {
28
+ from { transform: translateY(-1em); }
29
+ to { transform: translateY(-2em); }
30
+ }
31
+
32
+ /* Value animations: opt-in via <RafflePick.Value animation="..." /> */
33
+ .rrp-value {
34
+ display: inline-block;
35
+ }
36
+ .rrp-value[data-animation='roll'][data-phase='running'] {
37
+ animation: rrp-value-roll var(--rrp-tick, 100ms) linear;
38
+ }
39
+ .rrp-value[data-animation='fade'][data-phase='running'] {
40
+ animation: rrp-value-fade var(--rrp-tick, 100ms) linear;
41
+ }
42
+ .rrp-value[data-animation='blur'][data-phase='running'] {
43
+ animation: rrp-value-blur var(--rrp-tick, 100ms) linear;
44
+ }
45
+ .rrp-value[data-animation='reel'][data-phase='running'] {
46
+ animation: rrp-value-reel var(--rrp-tick, 100ms) linear;
47
+ }
48
+ @keyframes rrp-value-roll {
49
+ from { transform: translateY(-0.4em); opacity: 0.4; }
50
+ to { transform: translateY(0); opacity: 1; }
51
+ }
52
+ @keyframes rrp-value-fade {
53
+ from { opacity: 0.2; }
54
+ to { opacity: 1; }
55
+ }
56
+ @keyframes rrp-value-blur {
57
+ from { filter: blur(4px); }
58
+ to { filter: blur(0); }
59
+ }
60
+ @keyframes rrp-value-reel {
61
+ from { transform: translateY(-1em); opacity: 0; }
62
+ to { transform: translateY(0); opacity: 1; }
63
+ }
package/package.json CHANGED
@@ -1,8 +1,13 @@
1
1
  {
2
2
  "name": "react-raffle-picker",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Headless React raffle picker for giveaways, winner draws, countdowns, and slot-machine UIs.",
5
5
  "type": "module",
6
+ "publishConfig": {
7
+ "access": "public",
8
+ "registry": "https://registry.npmjs.org/",
9
+ "provenance": true
10
+ },
6
11
  "main": "./dist/index.cjs",
7
12
  "module": "./dist/index.mjs",
8
13
  "types": "./dist/index.d.mts",
@@ -16,9 +21,12 @@
16
21
  "require": "./dist/index.cjs",
17
22
  "default": "./dist/index.mjs"
18
23
  },
24
+ "./styles.css": "./dist/styles.css",
19
25
  "./package.json": "./package.json"
20
26
  },
21
- "sideEffects": false,
27
+ "sideEffects": [
28
+ "**/*.css"
29
+ ],
22
30
  "files": [
23
31
  "dist"
24
32
  ],
@@ -31,7 +39,7 @@
31
39
  },
32
40
  "homepage": "https://react-raffle-one.vercel.app/",
33
41
  "scripts": {
34
- "build": "tsdown",
42
+ "build": "tsdown && cp src/styles.css dist/styles.css",
35
43
  "dev": "tsdown --watch",
36
44
  "typecheck": "tsc --noEmit",
37
45
  "test": "vitest",
@@ -45,7 +53,7 @@
45
53
  "build-storybook": "storybook build",
46
54
  "changeset": "changeset",
47
55
  "version": "changeset version",
48
- "release": "changeset publish --provenance"
56
+ "release": "changeset publish"
49
57
  },
50
58
  "peerDependencies": {
51
59
  "react": ">=17",
@@ -68,9 +76,6 @@
68
76
  ],
69
77
  "author": "Kirilinsky (https://github.com/kirilinsky)",
70
78
  "license": "MIT",
71
- "publishConfig": {
72
- "access": "public"
73
- },
74
79
  "engines": {
75
80
  "node": ">=18"
76
81
  },
@@ -107,4 +112,4 @@
107
112
  "vite": "^8.0.8",
108
113
  "vitest": "^4.1.4"
109
114
  }
110
- }
115
+ }