@fmarlats/react-like-button 1.1.4

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.cjs ADDED
@@ -0,0 +1,926 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ DefaultHeartIcon: () => DefaultHeartIcon,
24
+ LIKE_BUTTON_DEFAULTS: () => LIKE_BUTTON_DEFAULTS,
25
+ LikeButton: () => LikeButton,
26
+ LikeButtonVanilla: () => LikeButtonVanilla,
27
+ PARTICLE_PRESETS: () => PARTICLE_PRESETS,
28
+ default: () => LikeButton,
29
+ useLikeButton: () => useLikeButton
30
+ });
31
+ module.exports = __toCommonJS(src_exports);
32
+
33
+ // src/LikeButton/DefaultHeartIcon.tsx
34
+ var import_jsx_runtime = require("react/jsx-runtime");
35
+ function DefaultHeartIcon({ size, className }) {
36
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
37
+ "svg",
38
+ {
39
+ className,
40
+ style: {
41
+ width: size,
42
+ height: size,
43
+ stroke: "#111827",
44
+ strokeWidth: 2,
45
+ fill: "transparent"
46
+ },
47
+ viewBox: "0 0 24 24",
48
+ "aria-hidden": "true",
49
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
50
+ "path",
51
+ {
52
+ strokeLinecap: "round",
53
+ strokeLinejoin: "round",
54
+ d: "M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
55
+ }
56
+ )
57
+ }
58
+ );
59
+ }
60
+
61
+ // src/LikeButton/LikeButton.tsx
62
+ var import_react3 = require("react");
63
+
64
+ // src/Particle/shapes/CircleShape.tsx
65
+ var import_jsx_runtime2 = require("react/jsx-runtime");
66
+ function CircleShape({ size, color, className = "" }) {
67
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
68
+ "svg",
69
+ {
70
+ width: size,
71
+ height: size,
72
+ viewBox: "0 0 24 24",
73
+ className: `fill-current ${className}`,
74
+ style: { color },
75
+ "aria-hidden": "true",
76
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "12", cy: "12", r: "10" })
77
+ }
78
+ );
79
+ }
80
+
81
+ // src/Particle/shapes/HeartShape.tsx
82
+ var import_jsx_runtime3 = require("react/jsx-runtime");
83
+ function HeartShape({ size, color, className = "" }) {
84
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
85
+ "svg",
86
+ {
87
+ width: size,
88
+ height: size,
89
+ viewBox: "0 0 24 24",
90
+ className: `fill-current ${className}`,
91
+ style: { color },
92
+ "aria-hidden": "true",
93
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" })
94
+ }
95
+ );
96
+ }
97
+
98
+ // src/Particle/shapes/SparkleShape.tsx
99
+ var import_jsx_runtime4 = require("react/jsx-runtime");
100
+ function SparkleShape({ size, color, className = "" }) {
101
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
102
+ "svg",
103
+ {
104
+ width: size,
105
+ height: size,
106
+ viewBox: "0 0 24 24",
107
+ className: `fill-current ${className}`,
108
+ style: { color },
109
+ "aria-hidden": "true",
110
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M12 2l2.5 7.5L22 12l-7.5 2.5L12 22l-2.5-7.5L2 12l7.5-2.5L12 2z" })
111
+ }
112
+ );
113
+ }
114
+
115
+ // src/Particle/shapes/SquareShape.tsx
116
+ var import_jsx_runtime5 = require("react/jsx-runtime");
117
+ function SquareShape({ size, color, className = "" }) {
118
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
119
+ "svg",
120
+ {
121
+ width: size,
122
+ height: size,
123
+ viewBox: "0 0 24 24",
124
+ className: `fill-current ${className}`,
125
+ style: { color },
126
+ "aria-hidden": "true",
127
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("rect", { x: "3", y: "3", width: "18", height: "18", rx: "3" })
128
+ }
129
+ );
130
+ }
131
+
132
+ // src/Particle/shapes/StarShape.tsx
133
+ var import_jsx_runtime6 = require("react/jsx-runtime");
134
+ function StarShape({ size, color, className = "" }) {
135
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
136
+ "svg",
137
+ {
138
+ width: size,
139
+ height: size,
140
+ viewBox: "0 0 24 24",
141
+ className: `fill-current ${className}`,
142
+ style: { color },
143
+ "aria-hidden": "true",
144
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" })
145
+ }
146
+ );
147
+ }
148
+
149
+ // src/Particle/shapes/utils.ts
150
+ function getParticleShape(shape) {
151
+ if (typeof shape === "object" && "render" in shape) {
152
+ return ({ size, color, className }) => shape.render({ size, color, className });
153
+ }
154
+ switch (shape) {
155
+ case "heart":
156
+ return HeartShape;
157
+ case "star":
158
+ return StarShape;
159
+ case "circle":
160
+ return CircleShape;
161
+ case "square":
162
+ return SquareShape;
163
+ case "sparkle":
164
+ return SparkleShape;
165
+ default:
166
+ return HeartShape;
167
+ }
168
+ }
169
+
170
+ // src/Particle/useParticle.ts
171
+ var import_react = require("react");
172
+ function useParticle({
173
+ angle,
174
+ distance,
175
+ scale,
176
+ speed,
177
+ easing,
178
+ fadeOut
179
+ }) {
180
+ const [isAnimating, setIsAnimating] = (0, import_react.useState)(false);
181
+ (0, import_react.useEffect)(() => {
182
+ let cancelled = false;
183
+ let raf2;
184
+ const raf1 = requestAnimationFrame(() => {
185
+ raf2 = requestAnimationFrame(() => {
186
+ if (!cancelled) {
187
+ setIsAnimating(true);
188
+ }
189
+ });
190
+ });
191
+ return () => {
192
+ cancelled = true;
193
+ cancelAnimationFrame(raf1);
194
+ if (raf2 !== void 0) cancelAnimationFrame(raf2);
195
+ };
196
+ }, []);
197
+ const x = Math.cos(angle * Math.PI / 180) * distance;
198
+ const y = Math.sin(angle * Math.PI / 180) * distance;
199
+ return {
200
+ isAnimating,
201
+ x,
202
+ y,
203
+ transform: isAnimating ? `translate(${x}px, ${y}px) scale(${scale})` : "translate(0, 0) scale(1)",
204
+ opacity: isAnimating ? 0 : 1,
205
+ speed,
206
+ easing,
207
+ fadeOut
208
+ };
209
+ }
210
+
211
+ // src/Particle/Particle.tsx
212
+ var import_jsx_runtime7 = require("react/jsx-runtime");
213
+ function Particle({
214
+ angle,
215
+ distance,
216
+ scale,
217
+ color,
218
+ shape,
219
+ speed,
220
+ easing,
221
+ fadeOut
222
+ }) {
223
+ const { transform, opacity } = useParticle({
224
+ angle,
225
+ distance,
226
+ scale,
227
+ speed,
228
+ easing,
229
+ fadeOut
230
+ });
231
+ const ShapeComponent = getParticleShape(shape);
232
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
233
+ "div",
234
+ {
235
+ className: "absolute w-10 h-10 transition-all",
236
+ style: {
237
+ color,
238
+ transform,
239
+ opacity: fadeOut ? opacity : 1,
240
+ transitionDuration: `${speed}ms`,
241
+ transitionTimingFunction: easing
242
+ },
243
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ShapeComponent, { size: 40, color, className: "w-full h-full" })
244
+ }
245
+ );
246
+ }
247
+
248
+ // src/LikeButton/useLikeButton.ts
249
+ var import_react2 = require("react");
250
+
251
+ // src/Particle/presets.ts
252
+ var DEFAULT_PARTICLE_CONFIG = {
253
+ shape: "heart",
254
+ speed: 500,
255
+ distance: { min: 60, max: 100 },
256
+ spread: 360,
257
+ spreadOffset: 0,
258
+ size: { min: 1, max: 1.5 },
259
+ colors: ["#EF4444", "#B9FF14", "#3B82F6"],
260
+ count: 8,
261
+ easing: "cubic-bezier(0.22, 1, 0.36, 1)",
262
+ fadeOut: true
263
+ };
264
+ var PARTICLE_PRESETS = {
265
+ /**
266
+ * Burst - Fast, enthusiastic explosion effect
267
+ * - Wide 360° spread
268
+ * - Fast animation (400ms)
269
+ * - Multiple colors
270
+ * - 12 particles
271
+ * - Perfect for: Likes, favorites, celebrations
272
+ */
273
+ burst: {
274
+ shape: "heart",
275
+ speed: 400,
276
+ distance: { min: 80, max: 120 },
277
+ spread: 360,
278
+ spreadOffset: 0,
279
+ size: { min: 1, max: 1.5 },
280
+ colors: ["#EF4444", "#F59E0B", "#3B82F6"],
281
+ count: 12,
282
+ easing: "cubic-bezier(0.22, 1, 0.36, 1)",
283
+ fadeOut: true
284
+ },
285
+ /**
286
+ * Fountain - Upward spray effect
287
+ * - 120° upward spread
288
+ * - Medium animation (600ms)
289
+ * - Cool colors (blue, purple, pink)
290
+ * - 10 particles
291
+ * - Perfect for: Achievements, upgrades, success
292
+ */
293
+ fountain: {
294
+ shape: "circle",
295
+ speed: 600,
296
+ distance: { min: 60, max: 100 },
297
+ spread: 120,
298
+ spreadOffset: -90,
299
+ // Upward
300
+ size: { min: 0.8, max: 1.2 },
301
+ colors: ["#3B82F6", "#8B5CF6", "#EC4899"],
302
+ count: 10,
303
+ easing: "cubic-bezier(0.22, 1, 0.36, 1)",
304
+ fadeOut: true
305
+ },
306
+ /**
307
+ * Confetti - Colorful celebration effect
308
+ * - Full 360° spread
309
+ * - Slow animation (800ms)
310
+ * - Rainbow colors
311
+ * - 15 particles
312
+ * - Perfect for: Milestones, victories, special events
313
+ */
314
+ confetti: {
315
+ shape: "square",
316
+ speed: 800,
317
+ distance: { min: 70, max: 110 },
318
+ spread: 360,
319
+ spreadOffset: 0,
320
+ size: { min: 0.6, max: 1.4 },
321
+ colors: ["#EF4444", "#F59E0B", "#10B981", "#3B82F6", "#8B5CF6", "#EC4899"],
322
+ count: 15,
323
+ easing: "ease-out",
324
+ fadeOut: true
325
+ },
326
+ /**
327
+ * Gentle - Subtle, calm effect
328
+ * - 180° upward spread
329
+ * - Slow animation (700ms)
330
+ * - Soft red tones
331
+ * - 6 particles
332
+ * - Perfect for: Subtle interactions, quiet appreciation
333
+ */
334
+ gentle: {
335
+ shape: "heart",
336
+ speed: 700,
337
+ distance: { min: 40, max: 60 },
338
+ spread: 180,
339
+ spreadOffset: -90,
340
+ size: { min: 0.8, max: 1 },
341
+ colors: ["#EF4444", "#F87171"],
342
+ count: 6,
343
+ easing: "ease-in-out",
344
+ fadeOut: true
345
+ },
346
+ /**
347
+ * Fireworks - Explosive sparkle effect
348
+ * - Full 360° spread
349
+ * - Medium-fast animation (500ms)
350
+ * - Warm sparkle colors
351
+ * - 16 particles
352
+ * - Large size range
353
+ * - Perfect for: Big celebrations, major achievements
354
+ */
355
+ fireworks: {
356
+ shape: "sparkle",
357
+ speed: 500,
358
+ distance: { min: 100, max: 150 },
359
+ spread: 360,
360
+ spreadOffset: 0,
361
+ size: { min: 1.2, max: 2 },
362
+ colors: ["#FBBF24", "#F59E0B", "#EF4444", "#EC4899"],
363
+ count: 16,
364
+ easing: "cubic-bezier(0.22, 1, 0.36, 1)",
365
+ fadeOut: true
366
+ }
367
+ };
368
+ function getParticlePreset(preset) {
369
+ return PARTICLE_PRESETS[preset];
370
+ }
371
+
372
+ // src/Particle/utils.ts
373
+ function normalizeRange(value) {
374
+ return typeof value === "number" ? { min: value, max: value } : value;
375
+ }
376
+ function randomInRange(range) {
377
+ return range.min + Math.random() * (range.max - range.min);
378
+ }
379
+ function normalizeAngle(angle) {
380
+ const normalized = angle % 360;
381
+ const result = normalized < 0 ? normalized + 360 : normalized;
382
+ return result === 0 ? 0 : result;
383
+ }
384
+ function randomAngle(spread, offset) {
385
+ const angle = offset + Math.random() * spread;
386
+ return normalizeAngle(angle);
387
+ }
388
+ function mergeParticleConfig(base, override) {
389
+ if (!override) {
390
+ return base;
391
+ }
392
+ return {
393
+ ...base,
394
+ ...override
395
+ };
396
+ }
397
+ function resolveParticleConfig(preset, config) {
398
+ let resolved = { ...DEFAULT_PARTICLE_CONFIG };
399
+ if (preset) {
400
+ resolved = mergeParticleConfig(resolved, getParticlePreset(preset));
401
+ }
402
+ if (config) {
403
+ resolved = mergeParticleConfig(resolved, config);
404
+ }
405
+ return resolved;
406
+ }
407
+
408
+ // src/LikeButton/utils.ts
409
+ var MAX_FILL_HEIGHT = 85;
410
+ var ICON_SIZE_RATIO = 0.5;
411
+ var MIN_HOVER_OFFSET = 2;
412
+ var PARTICLE_CLEANUP_BUFFER_MS = 100;
413
+ var WAVE_BACK_DURATION_S = 3;
414
+ var WAVE_FRONT_DURATION_S = 1.5;
415
+ var DEFAULT_STYLES = {
416
+ borderWidth: 4,
417
+ borderColor: "#111827",
418
+ shadowOffset: 8,
419
+ shadowColor: "#111827",
420
+ backgroundColor: "white"
421
+ };
422
+ function getShapeStyles(shape = "circle") {
423
+ if (typeof shape === "string") {
424
+ switch (shape) {
425
+ case "circle":
426
+ return { borderRadius: "9999px" };
427
+ case "rounded":
428
+ return { borderRadius: "1rem" };
429
+ case "square":
430
+ return { borderRadius: "0" };
431
+ default:
432
+ return { borderRadius: "9999px" };
433
+ }
434
+ }
435
+ const styles = {};
436
+ if (shape.clipPath) {
437
+ styles.clipPath = shape.clipPath;
438
+ styles.borderRadius = shape.borderRadius ?? "0";
439
+ } else if (shape.borderRadius) {
440
+ styles.borderRadius = shape.borderRadius;
441
+ }
442
+ return styles;
443
+ }
444
+ function computeHoverOffset(shadowOffset) {
445
+ return Math.max(shadowOffset / 2, MIN_HOVER_OFFSET);
446
+ }
447
+ function computeButtonStyles(size, mergedStyles, shapeStyles) {
448
+ return {
449
+ width: size,
450
+ height: size,
451
+ borderWidth: mergedStyles.borderWidth,
452
+ borderColor: mergedStyles.borderColor,
453
+ backgroundColor: mergedStyles.backgroundColor,
454
+ boxShadow: `${mergedStyles.shadowOffset}px ${mergedStyles.shadowOffset}px 0px ${mergedStyles.shadowColor}`,
455
+ ...shapeStyles
456
+ };
457
+ }
458
+ function computeHoverActiveVars(hoverShadowOffset, mergedStyles) {
459
+ const translateOnHover = mergedStyles.shadowOffset - hoverShadowOffset;
460
+ return {
461
+ "--shadow-offset": `${mergedStyles.shadowOffset}px`,
462
+ "--shadow-color": mergedStyles.shadowColor,
463
+ "--hover-shadow-offset": `${hoverShadowOffset}px`,
464
+ "--translate-hover": `${translateOnHover}px`
465
+ };
466
+ }
467
+ var CURSOR_SVGS = {
468
+ heart: `data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 24 24' fill='%23EF4444' stroke='%23111827' stroke-width='1.5'><path d='M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z'/></svg>`,
469
+ star: `data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 24 24' fill='%23FBBF24' stroke='%23111827' stroke-width='1.5'><path d='M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z'/></svg>`,
470
+ "thumbs-up": `data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 24 24' fill='%233B82F6' stroke='%23111827' stroke-width='1.5'><path d='M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3'/></svg>`
471
+ };
472
+ var DEFAULT_HOTSPOT = 16;
473
+ function getCursorStyle(cursor = "heart") {
474
+ if (typeof cursor === "string") {
475
+ switch (cursor) {
476
+ case "pointer":
477
+ return "pointer";
478
+ case "none":
479
+ return "none";
480
+ case "heart":
481
+ case "star":
482
+ case "thumbs-up":
483
+ return `url("${CURSOR_SVGS[cursor]}") ${DEFAULT_HOTSPOT} ${DEFAULT_HOTSPOT}, pointer`;
484
+ default:
485
+ return "pointer";
486
+ }
487
+ }
488
+ const {
489
+ url,
490
+ hotspotX = DEFAULT_HOTSPOT,
491
+ hotspotY = DEFAULT_HOTSPOT,
492
+ fallback = "pointer"
493
+ } = cursor;
494
+ return `url("${url}") ${hotspotX} ${hotspotY}, ${fallback}`;
495
+ }
496
+
497
+ // src/LikeButton/useLikeButton.ts
498
+ var LIKE_BUTTON_DEFAULTS = {
499
+ maxClicks: 1,
500
+ size: 96,
501
+ fillColor: "#EF4444",
502
+ waveColor: "#B91C1C"
503
+ };
504
+ function useLikeButton(options = {}) {
505
+ const {
506
+ clicks: externalClicks,
507
+ defaultClicks = 0,
508
+ maxClicks = LIKE_BUTTON_DEFAULTS.maxClicks,
509
+ onClick,
510
+ onChange,
511
+ onRightClick,
512
+ disabled: externalDisabled,
513
+ showParticles = true,
514
+ particlePreset,
515
+ particleConfig,
516
+ ariaLabel: customAriaLabel
517
+ } = options;
518
+ if (externalClicks !== void 0 && options.defaultClicks !== void 0) {
519
+ console.warn(
520
+ "LikeButton: `defaultClicks` is ignored when `clicks` is provided (controlled mode)."
521
+ );
522
+ }
523
+ const [internalClicks, setInternalClicks] = (0, import_react2.useState)(defaultClicks);
524
+ const [particles, setParticles] = (0, import_react2.useState)([]);
525
+ const timeoutRefs = (0, import_react2.useRef)(/* @__PURE__ */ new Set());
526
+ (0, import_react2.useEffect)(() => {
527
+ return () => {
528
+ for (const timeoutId of timeoutRefs.current) {
529
+ clearTimeout(timeoutId);
530
+ }
531
+ };
532
+ }, []);
533
+ const clicks = externalClicks ?? internalClicks;
534
+ const isMaxed = clicks >= maxClicks;
535
+ const disabled = externalDisabled ?? isMaxed;
536
+ const spawnParticles = (0, import_react2.useCallback)(() => {
537
+ if (!showParticles) return;
538
+ const config = resolveParticleConfig(particlePreset, particleConfig);
539
+ const distanceRange = normalizeRange(config.distance);
540
+ const sizeRange = normalizeRange(config.size);
541
+ const id = Date.now();
542
+ const newParticles = Array.from({ length: config.count }).map((_, i) => ({
543
+ id: `${id}-${i}`,
544
+ angle: randomAngle(config.spread, config.spreadOffset),
545
+ distance: randomInRange(distanceRange),
546
+ scale: randomInRange(sizeRange),
547
+ color: config.colors[Math.floor(Math.random() * config.colors.length)],
548
+ shape: config.shape,
549
+ speed: config.speed,
550
+ easing: config.easing,
551
+ fadeOut: config.fadeOut
552
+ }));
553
+ setParticles((prev) => [...prev, ...newParticles]);
554
+ const cleanupDelay = config.speed + PARTICLE_CLEANUP_BUFFER_MS;
555
+ const idsToRemove = new Set(newParticles.map((p) => p.id));
556
+ const timeoutId = setTimeout(() => {
557
+ setParticles((prev) => prev.filter((p) => !idsToRemove.has(p.id)));
558
+ timeoutRefs.current.delete(timeoutId);
559
+ }, cleanupDelay);
560
+ timeoutRefs.current.add(timeoutId);
561
+ }, [showParticles, particlePreset, particleConfig]);
562
+ const handleClick = (0, import_react2.useCallback)(
563
+ (e) => {
564
+ if (disabled) return;
565
+ const newClicks = clicks + 1;
566
+ if (externalClicks === void 0) {
567
+ setInternalClicks(newClicks);
568
+ }
569
+ spawnParticles();
570
+ onChange?.(newClicks);
571
+ onClick?.(newClicks, e);
572
+ },
573
+ [disabled, clicks, externalClicks, spawnParticles, onChange, onClick]
574
+ );
575
+ const handleRightClick = (0, import_react2.useCallback)(
576
+ (e) => {
577
+ e.preventDefault();
578
+ if (disabled) return;
579
+ onRightClick?.(clicks, e);
580
+ },
581
+ [disabled, clicks, onRightClick]
582
+ );
583
+ const handleKeyDown = (0, import_react2.useCallback)(
584
+ (e) => {
585
+ if (e.shiftKey && e.key === "Enter") {
586
+ e.preventDefault();
587
+ if (disabled) return;
588
+ onRightClick?.(clicks, e);
589
+ }
590
+ },
591
+ [disabled, clicks, onRightClick]
592
+ );
593
+ const fillPercentage = clicks / maxClicks * 100;
594
+ const defaultAriaLabel = isMaxed ? "Thank you for your likes!" : `Like this content. ${maxClicks - clicks} clicks remaining`;
595
+ const ariaLabelState = {
596
+ isMaxed,
597
+ remaining: maxClicks - clicks,
598
+ clicks,
599
+ maxClicks
600
+ };
601
+ const computedAriaLabel = customAriaLabel === void 0 ? defaultAriaLabel : typeof customAriaLabel === "function" ? customAriaLabel(ariaLabelState) : customAriaLabel;
602
+ return {
603
+ clicks,
604
+ isMaxed,
605
+ disabled,
606
+ fillPercentage,
607
+ particles,
608
+ handleClick,
609
+ handleRightClick,
610
+ handleKeyDown,
611
+ ariaLabel: computedAriaLabel,
612
+ isPressed: clicks > 0,
613
+ hasRightClickAction: onRightClick !== void 0
614
+ };
615
+ }
616
+
617
+ // src/LikeButton/LikeButton.tsx
618
+ var import_jsx_runtime8 = require("react/jsx-runtime");
619
+ var BUTTON_BASE_CLASSES = "relative overflow-hidden z-10 border-solid flex items-center justify-center group transition-all focus:outline-none focus-visible:ring-4 focus-visible:ring-primary-dark focus-visible:ring-offset-2";
620
+ var WRAPPER_CLASSES = "relative inline-block";
621
+ var FILL_CONTAINER_CLASSES = "absolute bottom-0 left-0 right-0 z-0 transition-[height] duration-500 ease-out";
622
+ var WAVE_CONTAINER_CLASSES = "absolute bottom-full left-0 w-[200%] h-4 flex";
623
+ var WAVE_SVG_CLASSES = "w-1/2 h-full fill-current";
624
+ var PARTICLE_CONTAINER_CLASSES = "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 pointer-events-none";
625
+ var LikeButton = (0, import_react3.forwardRef)(function LikeButton2({
626
+ size = LIKE_BUTTON_DEFAULTS.size,
627
+ fillColor = LIKE_BUTTON_DEFAULTS.fillColor,
628
+ waveColor = LIKE_BUTTON_DEFAULTS.waveColor,
629
+ className = "",
630
+ showParticles = true,
631
+ showWaves = true,
632
+ renderIcon,
633
+ shape = "circle",
634
+ styles = {},
635
+ cursor = "heart",
636
+ minFillPercent = 0,
637
+ ...hookOptions
638
+ }, ref) {
639
+ const clampedMinFill = Math.max(0, Math.min(MAX_FILL_HEIGHT, minFillPercent));
640
+ const reactId = (0, import_react3.useId)();
641
+ const buttonId = `like-button${reactId.replace(/:/g, "-")}`;
642
+ const {
643
+ handleClick,
644
+ handleRightClick,
645
+ handleKeyDown,
646
+ disabled,
647
+ ariaLabel,
648
+ isPressed,
649
+ isMaxed,
650
+ fillPercentage,
651
+ particles,
652
+ hasRightClickAction
653
+ } = useLikeButton({ showParticles, ...hookOptions });
654
+ const mergedStyles = (0, import_react3.useMemo)(() => ({ ...DEFAULT_STYLES, ...styles }), [styles]);
655
+ const shapeStyles = (0, import_react3.useMemo)(() => getShapeStyles(shape), [shape]);
656
+ const buttonStyle = (0, import_react3.useMemo)(
657
+ () => computeButtonStyles(size, mergedStyles, shapeStyles),
658
+ [size, mergedStyles, shapeStyles]
659
+ );
660
+ const hoverShadowOffset = (0, import_react3.useMemo)(
661
+ () => computeHoverOffset(mergedStyles.shadowOffset),
662
+ [mergedStyles.shadowOffset]
663
+ );
664
+ const hoverActiveVars = (0, import_react3.useMemo)(
665
+ () => computeHoverActiveVars(hoverShadowOffset, mergedStyles),
666
+ [hoverShadowOffset, mergedStyles]
667
+ );
668
+ const cursorStyle = (0, import_react3.useMemo)(
669
+ () => disabled ? "not-allowed" : getCursorStyle(cursor),
670
+ [cursor, disabled]
671
+ );
672
+ const iconSize = size * ICON_SIZE_RATIO;
673
+ const iconRenderProps = {
674
+ size: iconSize,
675
+ className: "relative z-20 transition-colors duration-300 pointer-events-none",
676
+ isMaxed,
677
+ fillPercentage
678
+ };
679
+ const renderedIcon = renderIcon === null ? null : renderIcon === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(DefaultHeartIcon, { ...iconRenderProps }) : renderIcon(iconRenderProps);
680
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: WRAPPER_CLASSES, children: [
681
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
682
+ "button",
683
+ {
684
+ ref,
685
+ id: buttonId,
686
+ "data-like-button": true,
687
+ type: "button",
688
+ onClick: handleClick,
689
+ onContextMenu: handleRightClick,
690
+ onKeyDown: handleKeyDown,
691
+ disabled,
692
+ "aria-label": ariaLabel,
693
+ "aria-pressed": isPressed,
694
+ "aria-disabled": disabled,
695
+ "aria-keyshortcuts": hasRightClickAction ? "Shift+Enter" : void 0,
696
+ style: { ...buttonStyle, ...hoverActiveVars, cursor: cursorStyle },
697
+ className: `${BUTTON_BASE_CLASSES} ${className}`,
698
+ children: [
699
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
700
+ "div",
701
+ {
702
+ className: FILL_CONTAINER_CLASSES,
703
+ style: {
704
+ backgroundColor: fillColor,
705
+ height: isMaxed ? "100%" : `${clampedMinFill + fillPercentage / 100 * (MAX_FILL_HEIGHT - clampedMinFill)}%`
706
+ },
707
+ children: showWaves && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
708
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
709
+ "div",
710
+ {
711
+ className: WAVE_CONTAINER_CLASSES,
712
+ style: { animation: `wave-scroll-left ${WAVE_BACK_DURATION_S}s linear infinite` },
713
+ children: [0, 1].map((i) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
714
+ "svg",
715
+ {
716
+ className: WAVE_SVG_CLASSES,
717
+ style: { color: waveColor },
718
+ viewBox: "0 0 100 20",
719
+ preserveAspectRatio: "none",
720
+ "aria-hidden": "true",
721
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M0,10 Q25,0 50,10 T100,10 V20 H0 Z" })
722
+ },
723
+ i
724
+ ))
725
+ }
726
+ ),
727
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
728
+ "div",
729
+ {
730
+ className: WAVE_CONTAINER_CLASSES,
731
+ style: { animation: `wave-scroll-right ${WAVE_FRONT_DURATION_S}s linear infinite` },
732
+ children: [0, 1].map((i) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
733
+ "svg",
734
+ {
735
+ className: WAVE_SVG_CLASSES,
736
+ style: { color: fillColor },
737
+ viewBox: "0 0 100 20",
738
+ preserveAspectRatio: "none",
739
+ "aria-hidden": "true",
740
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M0,10 Q25,5 50,10 T100,10 V20 H0 Z" })
741
+ },
742
+ i
743
+ ))
744
+ }
745
+ )
746
+ ] })
747
+ }
748
+ ),
749
+ renderedIcon
750
+ ]
751
+ }
752
+ ),
753
+ showParticles && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: PARTICLE_CONTAINER_CLASSES, "aria-hidden": "true", children: particles.map((p) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Particle, { ...p }, p.id)) })
754
+ ] });
755
+ });
756
+
757
+ // src/LikeButton/LikeButton.vanilla.tsx
758
+ var import_react4 = require("react");
759
+
760
+ // src/Particle/Particle.vanilla.tsx
761
+ var import_jsx_runtime9 = require("react/jsx-runtime");
762
+ function ParticleVanilla({
763
+ angle,
764
+ distance,
765
+ scale,
766
+ color,
767
+ shape,
768
+ speed,
769
+ easing,
770
+ fadeOut
771
+ }) {
772
+ const { transform, opacity } = useParticle({
773
+ angle,
774
+ distance,
775
+ scale,
776
+ speed,
777
+ easing,
778
+ fadeOut
779
+ });
780
+ const ShapeComponent = getParticleShape(shape);
781
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
782
+ "div",
783
+ {
784
+ className: "particle",
785
+ style: {
786
+ color,
787
+ transform,
788
+ opacity: fadeOut ? opacity : 1,
789
+ transitionDuration: `${speed}ms`,
790
+ transitionTimingFunction: easing
791
+ },
792
+ children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ShapeComponent, { size: 40, color, className: "particle__icon" })
793
+ }
794
+ );
795
+ }
796
+
797
+ // src/LikeButton/LikeButton.vanilla.tsx
798
+ var import_jsx_runtime10 = require("react/jsx-runtime");
799
+ var LikeButtonVanilla = (0, import_react4.forwardRef)(
800
+ function LikeButtonVanilla2({
801
+ size = LIKE_BUTTON_DEFAULTS.size,
802
+ fillColor = LIKE_BUTTON_DEFAULTS.fillColor,
803
+ waveColor = LIKE_BUTTON_DEFAULTS.waveColor,
804
+ className = "",
805
+ showParticles = true,
806
+ showWaves = true,
807
+ renderIcon,
808
+ shape = "circle",
809
+ styles = {},
810
+ cursor = "heart",
811
+ minFillPercent = 0,
812
+ ...hookOptions
813
+ }, ref) {
814
+ const clampedMinFill = Math.max(0, Math.min(MAX_FILL_HEIGHT, minFillPercent));
815
+ const reactId = (0, import_react4.useId)();
816
+ const buttonId = `like-button${reactId.replace(/:/g, "-")}`;
817
+ const {
818
+ handleClick,
819
+ handleRightClick,
820
+ handleKeyDown,
821
+ disabled,
822
+ ariaLabel,
823
+ isPressed,
824
+ isMaxed,
825
+ fillPercentage,
826
+ particles,
827
+ hasRightClickAction
828
+ } = useLikeButton({ showParticles, ...hookOptions });
829
+ const mergedStyles = (0, import_react4.useMemo)(() => ({ ...DEFAULT_STYLES, ...styles }), [styles]);
830
+ const shapeStyles = (0, import_react4.useMemo)(() => getShapeStyles(shape), [shape]);
831
+ const buttonStyle = (0, import_react4.useMemo)(
832
+ () => computeButtonStyles(size, mergedStyles, shapeStyles),
833
+ [size, mergedStyles, shapeStyles]
834
+ );
835
+ const hoverShadowOffset = (0, import_react4.useMemo)(
836
+ () => computeHoverOffset(mergedStyles.shadowOffset),
837
+ [mergedStyles.shadowOffset]
838
+ );
839
+ const hoverActiveVars = (0, import_react4.useMemo)(
840
+ () => computeHoverActiveVars(hoverShadowOffset, mergedStyles),
841
+ [hoverShadowOffset, mergedStyles]
842
+ );
843
+ const cursorStyle = (0, import_react4.useMemo)(
844
+ () => disabled ? "not-allowed" : getCursorStyle(cursor),
845
+ [cursor, disabled]
846
+ );
847
+ const iconSize = size * ICON_SIZE_RATIO;
848
+ const iconRenderProps = {
849
+ size: iconSize,
850
+ className: "like-button__icon",
851
+ isMaxed,
852
+ fillPercentage
853
+ };
854
+ const renderedIcon = renderIcon === null ? null : renderIcon === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(DefaultHeartIcon, { ...iconRenderProps }) : renderIcon(iconRenderProps);
855
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "like-button-container", children: [
856
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
857
+ "button",
858
+ {
859
+ ref,
860
+ id: buttonId,
861
+ type: "button",
862
+ onClick: handleClick,
863
+ onContextMenu: handleRightClick,
864
+ onKeyDown: handleKeyDown,
865
+ disabled,
866
+ "aria-label": ariaLabel,
867
+ "aria-pressed": isPressed,
868
+ "aria-disabled": disabled,
869
+ "aria-keyshortcuts": hasRightClickAction ? "Shift+Enter" : void 0,
870
+ style: { ...buttonStyle, ...hoverActiveVars, cursor: cursorStyle },
871
+ className: `like-button ${className}`.trim(),
872
+ children: [
873
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
874
+ "div",
875
+ {
876
+ className: "like-button__fill",
877
+ style: {
878
+ backgroundColor: fillColor,
879
+ height: isMaxed ? "100%" : `${clampedMinFill + fillPercentage / 100 * (MAX_FILL_HEIGHT - clampedMinFill)}%`
880
+ },
881
+ children: showWaves && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
882
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "like-button__wave like-button__wave--back", children: [0, 1].map((i) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
883
+ "svg",
884
+ {
885
+ className: "like-button__wave-svg",
886
+ style: { color: waveColor },
887
+ viewBox: "0 0 100 20",
888
+ preserveAspectRatio: "none",
889
+ "aria-hidden": "true",
890
+ children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("path", { d: "M0,10 Q25,0 50,10 T100,10 V20 H0 Z" })
891
+ },
892
+ i
893
+ )) }),
894
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "like-button__wave like-button__wave--front", children: [0, 1].map((i) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
895
+ "svg",
896
+ {
897
+ className: "like-button__wave-svg",
898
+ style: { color: fillColor },
899
+ viewBox: "0 0 100 20",
900
+ preserveAspectRatio: "none",
901
+ "aria-hidden": "true",
902
+ children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("path", { d: "M0,10 Q25,5 50,10 T100,10 V20 H0 Z" })
903
+ },
904
+ i
905
+ )) })
906
+ ] })
907
+ }
908
+ ),
909
+ renderedIcon
910
+ ]
911
+ }
912
+ ),
913
+ showParticles && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "like-button__particles", "aria-hidden": "true", children: particles.map((p) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ParticleVanilla, { ...p }, p.id)) })
914
+ ] });
915
+ }
916
+ );
917
+ // Annotate the CommonJS export names for ESM import in node:
918
+ 0 && (module.exports = {
919
+ DefaultHeartIcon,
920
+ LIKE_BUTTON_DEFAULTS,
921
+ LikeButton,
922
+ LikeButtonVanilla,
923
+ PARTICLE_PRESETS,
924
+ useLikeButton
925
+ });
926
+ //# sourceMappingURL=index.cjs.map