@hua-labs/motion-core 2.2.2 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js DELETED
@@ -1,4248 +0,0 @@
1
- 'use strict';
2
-
3
- var react = require('react');
4
-
5
- // src/core/MotionEngine.ts
6
- var MotionEngine = class {
7
- constructor() {
8
- this.motions = /* @__PURE__ */ new Map();
9
- this.isRunning = false;
10
- this.animationFrameId = null;
11
- }
12
- /**
13
- * 모션 시작
14
- */
15
- motion(element, motionFrames, options) {
16
- return new Promise((resolve) => {
17
- const motionId = this.generateMotionId();
18
- this.enableGPUAcceleration(element);
19
- this.createLayer(element);
20
- const motion = {
21
- id: motionId,
22
- element,
23
- isRunning: true,
24
- isPaused: false,
25
- currentProgress: 0,
26
- startTime: Date.now() + (options.delay || 0),
27
- pauseTime: 0,
28
- options: {
29
- ...options,
30
- onComplete: () => {
31
- options.onComplete?.();
32
- this.motions.delete(motionId);
33
- }
34
- }
35
- };
36
- this.motions.set(motionId, motion);
37
- if (!this.isRunning) {
38
- this.startAnimationLoop();
39
- }
40
- options.onStart?.();
41
- resolve(motionId);
42
- });
43
- }
44
- /**
45
- * 모션 중지
46
- */
47
- stop(motionId) {
48
- const motion = this.motions.get(motionId);
49
- if (motion) {
50
- motion.isRunning = false;
51
- motion.options.onCancel?.();
52
- this.motions.delete(motionId);
53
- }
54
- }
55
- /**
56
- * 모션 일시정지
57
- */
58
- pause(motionId) {
59
- const motion = this.motions.get(motionId);
60
- if (motion && motion.isRunning && !motion.isPaused) {
61
- motion.isPaused = true;
62
- motion.pauseTime = Date.now();
63
- }
64
- }
65
- /**
66
- * 모션 재개
67
- */
68
- resume(motionId) {
69
- const motion = this.motions.get(motionId);
70
- if (motion && motion.isPaused) {
71
- motion.isPaused = false;
72
- if (motion.pauseTime > 0) {
73
- motion.startTime += Date.now() - motion.pauseTime;
74
- }
75
- }
76
- }
77
- /**
78
- * 모든 모션 중지
79
- */
80
- stopAll() {
81
- this.motions.forEach((motion) => {
82
- motion.isRunning = false;
83
- motion.options.onCancel?.();
84
- });
85
- this.motions.clear();
86
- this.stopAnimationLoop();
87
- }
88
- /**
89
- * 모션 상태 확인
90
- */
91
- getMotion(motionId) {
92
- return this.motions.get(motionId);
93
- }
94
- /**
95
- * 실행 중인 모션 수
96
- */
97
- getActiveMotionCount() {
98
- return this.motions.size;
99
- }
100
- /**
101
- * 애니메이션 루프 시작
102
- */
103
- startAnimationLoop() {
104
- if (this.isRunning) return;
105
- this.isRunning = true;
106
- this.animate();
107
- }
108
- /**
109
- * 애니메이션 루프 중지
110
- */
111
- stopAnimationLoop() {
112
- if (this.animationFrameId) {
113
- cancelAnimationFrame(this.animationFrameId);
114
- this.animationFrameId = null;
115
- }
116
- this.isRunning = false;
117
- }
118
- /**
119
- * 메인 애니메이션 루프
120
- */
121
- animate() {
122
- if (!this.isRunning || this.motions.size === 0) {
123
- this.stopAnimationLoop();
124
- return;
125
- }
126
- const currentTime = Date.now();
127
- const completedMotions = [];
128
- this.motions.forEach((motion) => {
129
- if (!motion.isRunning || motion.isPaused) return;
130
- const elapsed = currentTime - motion.startTime;
131
- if (elapsed < 0) return;
132
- const progress = Math.min(elapsed / motion.options.duration, 1);
133
- const easedProgress = motion.options.easing(progress);
134
- motion.currentProgress = easedProgress;
135
- this.applyMotionFrame(motion.element, easedProgress);
136
- motion.options.onUpdate?.(easedProgress);
137
- if (progress >= 1) {
138
- completedMotions.push(motion.id);
139
- motion.isRunning = false;
140
- motion.options.onComplete?.();
141
- }
142
- });
143
- completedMotions.forEach((id) => this.motions.delete(id));
144
- this.animationFrameId = requestAnimationFrame(() => this.animate());
145
- }
146
- /**
147
- * 모션 프레임 적용
148
- */
149
- applyMotionFrame(element, progress) {
150
- const transforms = [];
151
- if (element.style.transform) {
152
- transforms.push(element.style.transform);
153
- }
154
- element.style.transform = transforms.join(" ");
155
- }
156
- /**
157
- * GPU 가속 활성화
158
- */
159
- enableGPUAcceleration(element) {
160
- element.style.willChange = "transform, opacity";
161
- element.style.transform = "translateZ(0)";
162
- }
163
- /**
164
- * 레이어 분리
165
- */
166
- createLayer(element) {
167
- element.style.transform = "translateZ(0)";
168
- element.style.backfaceVisibility = "hidden";
169
- }
170
- /**
171
- * 고유 모션 ID 생성
172
- */
173
- generateMotionId() {
174
- return `motion_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
175
- }
176
- /**
177
- * 정리
178
- */
179
- destroy() {
180
- this.stopAll();
181
- this.stopAnimationLoop();
182
- }
183
- };
184
- var motionEngine = new MotionEngine();
185
-
186
- // src/core/TransitionEffects.ts
187
- var TransitionEffects = class _TransitionEffects {
188
- constructor() {
189
- this.activeTransitions = /* @__PURE__ */ new Map();
190
- }
191
- static getInstance() {
192
- if (!_TransitionEffects.instance) {
193
- _TransitionEffects.instance = new _TransitionEffects();
194
- }
195
- return _TransitionEffects.instance;
196
- }
197
- /**
198
- * 페이드 인/아웃 전환
199
- */
200
- async fade(element, options) {
201
- const transitionId = this.generateTransitionId();
202
- return new Promise(async (resolve) => {
203
- const initialOpacity = parseFloat(getComputedStyle(element).opacity) || 1;
204
- const targetOpacity = options.direction === "reverse" ? 0 : 1;
205
- if (options.direction === "reverse") {
206
- element.style.opacity = initialOpacity.toString();
207
- } else {
208
- element.style.opacity = "0";
209
- }
210
- this.enableGPUAcceleration(element);
211
- const motionId = await motionEngine.motion(
212
- element,
213
- [
214
- { progress: 0, properties: { opacity: options.direction === "reverse" ? initialOpacity : 0 } },
215
- { progress: 1, properties: { opacity: targetOpacity } }
216
- ],
217
- {
218
- duration: options.duration,
219
- easing: options.easing || this.getDefaultEasing(),
220
- delay: options.delay,
221
- onStart: options.onTransitionStart,
222
- onUpdate: (progress) => {
223
- const currentOpacity = options.direction === "reverse" ? initialOpacity * (1 - progress) : targetOpacity * progress;
224
- element.style.opacity = currentOpacity.toString();
225
- },
226
- onComplete: () => {
227
- options.onTransitionComplete?.();
228
- this.activeTransitions.delete(transitionId);
229
- resolve();
230
- }
231
- }
232
- );
233
- this.activeTransitions.set(transitionId, motionId);
234
- });
235
- }
236
- /**
237
- * 슬라이드 전환
238
- */
239
- async slide(element, options) {
240
- const transitionId = this.generateTransitionId();
241
- const distance = options.distance || 100;
242
- return new Promise(async (resolve) => {
243
- const initialTransform = getComputedStyle(element).transform;
244
- const isReverse = options.direction === "reverse";
245
- if (!isReverse) {
246
- element.style.transform = `translateX(${distance}px)`;
247
- }
248
- this.enableGPUAcceleration(element);
249
- const motionId = await motionEngine.motion(
250
- element,
251
- [
252
- { progress: 0, properties: { translateX: isReverse ? 0 : distance } },
253
- { progress: 1, properties: { translateX: isReverse ? distance : 0 } }
254
- ],
255
- {
256
- duration: options.duration,
257
- easing: options.easing || this.getDefaultEasing(),
258
- delay: options.delay,
259
- onStart: options.onTransitionStart,
260
- onUpdate: (progress) => {
261
- const currentTranslateX = isReverse ? distance * progress : distance * (1 - progress);
262
- element.style.transform = `translateX(${currentTranslateX}px)`;
263
- },
264
- onComplete: () => {
265
- element.style.transform = initialTransform;
266
- options.onTransitionComplete?.();
267
- this.activeTransitions.delete(transitionId);
268
- resolve();
269
- }
270
- }
271
- );
272
- this.activeTransitions.set(transitionId, motionId);
273
- });
274
- }
275
- /**
276
- * 스케일 전환
277
- */
278
- async scale(element, options) {
279
- const transitionId = this.generateTransitionId();
280
- const scaleValue = options.scale || 0.8;
281
- return new Promise(async (resolve) => {
282
- const initialTransform = getComputedStyle(element).transform;
283
- const isReverse = options.direction === "reverse";
284
- if (!isReverse) {
285
- element.style.transform = `scale(${scaleValue})`;
286
- }
287
- this.enableGPUAcceleration(element);
288
- const motionId = await motionEngine.motion(
289
- element,
290
- [
291
- { progress: 0, properties: { scale: isReverse ? 1 : scaleValue } },
292
- { progress: 1, properties: { scale: isReverse ? scaleValue : 1 } }
293
- ],
294
- {
295
- duration: options.duration,
296
- easing: options.easing || this.getDefaultEasing(),
297
- delay: options.delay,
298
- onStart: options.onTransitionStart,
299
- onUpdate: (progress) => {
300
- const currentScale = isReverse ? 1 - (1 - scaleValue) * progress : scaleValue + (1 - scaleValue) * progress;
301
- element.style.transform = `scale(${currentScale})`;
302
- },
303
- onComplete: () => {
304
- element.style.transform = initialTransform;
305
- options.onTransitionComplete?.();
306
- this.activeTransitions.delete(transitionId);
307
- resolve();
308
- }
309
- }
310
- );
311
- this.activeTransitions.set(transitionId, motionId);
312
- });
313
- }
314
- /**
315
- * 플립 전환 (3D 회전)
316
- */
317
- async flip(element, options) {
318
- const transitionId = this.generateTransitionId();
319
- const perspective = options.perspective || 1e3;
320
- return new Promise(async (resolve) => {
321
- const initialTransform = getComputedStyle(element).transform;
322
- const isReverse = options.direction === "reverse";
323
- element.style.perspective = `${perspective}px`;
324
- element.style.transformStyle = "preserve-3d";
325
- if (!isReverse) {
326
- element.style.transform = `rotateY(90deg)`;
327
- }
328
- this.enableGPUAcceleration(element);
329
- const motionId = await motionEngine.motion(
330
- element,
331
- [
332
- { progress: 0, properties: { rotateY: isReverse ? 0 : 90 } },
333
- { progress: 1, properties: { rotateY: isReverse ? 90 : 0 } }
334
- ],
335
- {
336
- duration: options.duration,
337
- easing: options.easing || this.getDefaultEasing(),
338
- delay: options.delay,
339
- onStart: options.onTransitionStart,
340
- onUpdate: (progress) => {
341
- const currentRotateY = isReverse ? 90 * progress : 90 * (1 - progress);
342
- element.style.transform = `rotateY(${currentRotateY}deg)`;
343
- },
344
- onComplete: () => {
345
- element.style.transform = initialTransform;
346
- element.style.perspective = "";
347
- element.style.transformStyle = "";
348
- options.onTransitionComplete?.();
349
- this.activeTransitions.delete(transitionId);
350
- resolve();
351
- }
352
- }
353
- );
354
- this.activeTransitions.set(transitionId, motionId);
355
- });
356
- }
357
- /**
358
- * 큐브 전환 (3D 큐브 회전)
359
- */
360
- async cube(element, options) {
361
- const transitionId = this.generateTransitionId();
362
- const perspective = options.perspective || 1200;
363
- return new Promise(async (resolve) => {
364
- const initialTransform = getComputedStyle(element).transform;
365
- const isReverse = options.direction === "reverse";
366
- element.style.perspective = `${perspective}px`;
367
- element.style.transformStyle = "preserve-3d";
368
- if (!isReverse) {
369
- element.style.transform = `rotateX(90deg) rotateY(45deg)`;
370
- }
371
- this.enableGPUAcceleration(element);
372
- const motionId = await motionEngine.motion(
373
- element,
374
- [
375
- { progress: 0, properties: { rotateX: isReverse ? 0 : 90, rotateY: isReverse ? 0 : 45 } },
376
- { progress: 1, properties: { rotateX: isReverse ? 90 : 0, rotateY: isReverse ? 45 : 0 } }
377
- ],
378
- {
379
- duration: options.duration,
380
- easing: options.easing || this.getDefaultEasing(),
381
- delay: options.delay,
382
- onStart: options.onTransitionStart,
383
- onUpdate: (progress) => {
384
- const currentRotateX = isReverse ? 90 * progress : 90 * (1 - progress);
385
- const currentRotateY = isReverse ? 45 * progress : 45 * (1 - progress);
386
- element.style.transform = `rotateX(${currentRotateX}deg) rotateY(${currentRotateY}deg)`;
387
- },
388
- onComplete: () => {
389
- element.style.transform = initialTransform;
390
- element.style.perspective = "";
391
- element.style.transformStyle = "";
392
- options.onTransitionComplete?.();
393
- this.activeTransitions.delete(transitionId);
394
- resolve();
395
- }
396
- }
397
- );
398
- this.activeTransitions.set(transitionId, motionId);
399
- });
400
- }
401
- /**
402
- * 모프 전환 (복합 변형)
403
- */
404
- async morph(element, options) {
405
- const transitionId = this.generateTransitionId();
406
- return new Promise(async (resolve) => {
407
- const initialTransform = getComputedStyle(element).transform;
408
- const isReverse = options.direction === "reverse";
409
- if (!isReverse) {
410
- element.style.transform = `scale(0.9) rotate(5deg)`;
411
- }
412
- this.enableGPUAcceleration(element);
413
- const motionId = await motionEngine.motion(
414
- element,
415
- [
416
- { progress: 0, properties: { scale: isReverse ? 1 : 0.9, rotate: isReverse ? 0 : 5 } },
417
- { progress: 1, properties: { scale: isReverse ? 0.9 : 1, rotate: isReverse ? 5 : 0 } }
418
- ],
419
- {
420
- duration: options.duration,
421
- easing: options.easing || this.getDefaultEasing(),
422
- delay: options.delay,
423
- onStart: options.onTransitionStart,
424
- onUpdate: (progress) => {
425
- const currentScale = isReverse ? 1 - 0.1 * progress : 0.9 + 0.1 * progress;
426
- const currentRotate = isReverse ? 5 * progress : 5 * (1 - progress);
427
- element.style.transform = `scale(${currentScale}) rotate(${currentRotate}deg)`;
428
- },
429
- onComplete: () => {
430
- element.style.transform = initialTransform;
431
- options.onTransitionComplete?.();
432
- this.activeTransitions.delete(transitionId);
433
- resolve();
434
- }
435
- }
436
- );
437
- this.activeTransitions.set(transitionId, motionId);
438
- });
439
- }
440
- /**
441
- * 전환 중지
442
- */
443
- stopTransition(transitionId) {
444
- const motionId = this.activeTransitions.get(transitionId);
445
- if (motionId) {
446
- motionEngine.stop(motionId);
447
- this.activeTransitions.delete(transitionId);
448
- }
449
- }
450
- /**
451
- * 모든 전환 중지
452
- */
453
- stopAllTransitions() {
454
- this.activeTransitions.forEach((motionId) => {
455
- motionEngine.stop(motionId);
456
- });
457
- this.activeTransitions.clear();
458
- }
459
- /**
460
- * 활성 전환 수 확인
461
- */
462
- getActiveTransitionCount() {
463
- return this.activeTransitions.size;
464
- }
465
- /**
466
- * GPU 가속 활성화
467
- */
468
- enableGPUAcceleration(element) {
469
- element.style.willChange = "transform, opacity";
470
- const currentTransform = element.style.transform;
471
- if (currentTransform && currentTransform !== "none" && currentTransform !== "") {
472
- if (!currentTransform.includes("translateZ")) {
473
- element.style.transform = `${currentTransform} translateZ(0)`;
474
- }
475
- } else {
476
- element.style.transform = "translateZ(0)";
477
- }
478
- }
479
- /**
480
- * 기본 이징 함수
481
- */
482
- getDefaultEasing() {
483
- return (t) => t * t * (3 - 2 * t);
484
- }
485
- /**
486
- * 고유 전환 ID 생성
487
- */
488
- generateTransitionId() {
489
- return `transition_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
490
- }
491
- /**
492
- * 정리
493
- */
494
- destroy() {
495
- this.stopAllTransitions();
496
- }
497
- };
498
- var transitionEffects = TransitionEffects.getInstance();
499
-
500
- // src/core/PerformanceOptimizer.ts
501
- var PerformanceOptimizer = class _PerformanceOptimizer {
502
- constructor() {
503
- this.performanceObserver = null;
504
- this.layerRegistry = /* @__PURE__ */ new Set();
505
- this.isMonitoring = false;
506
- this.config = {
507
- enableGPUAcceleration: true,
508
- enableLayerSeparation: true,
509
- enableMemoryOptimization: true,
510
- targetFPS: 60,
511
- maxLayerCount: 100,
512
- memoryThreshold: 50 * 1024 * 1024
513
- // 50MB
514
- };
515
- this.metrics = {
516
- fps: 0,
517
- layerCount: 0,
518
- activeMotions: 0
519
- };
520
- this.initializePerformanceMonitoring();
521
- }
522
- static getInstance() {
523
- if (!_PerformanceOptimizer.instance) {
524
- _PerformanceOptimizer.instance = new _PerformanceOptimizer();
525
- }
526
- return _PerformanceOptimizer.instance;
527
- }
528
- /**
529
- * 성능 모니터링 초기화
530
- */
531
- initializePerformanceMonitoring() {
532
- if (typeof PerformanceObserver !== "undefined") {
533
- try {
534
- this.performanceObserver = new PerformanceObserver((list) => {
535
- const entries = list.getEntries();
536
- this.updatePerformanceMetrics(entries);
537
- });
538
- this.performanceObserver.observe({ entryTypes: ["measure", "navigation"] });
539
- } catch (error) {
540
- if (process.env.NODE_ENV === "development") {
541
- console.warn("Performance monitoring not supported:", error);
542
- }
543
- }
544
- }
545
- }
546
- /**
547
- * 성능 메트릭 업데이트
548
- */
549
- updatePerformanceMetrics(entries) {
550
- entries.forEach((entry) => {
551
- if (entry.entryType === "measure") {
552
- this.calculateFPS();
553
- }
554
- });
555
- }
556
- /**
557
- * FPS 계산
558
- */
559
- calculateFPS() {
560
- const now = performance.now();
561
- const deltaTime = now - (this.lastFrameTime || now);
562
- this.lastFrameTime = now;
563
- if (deltaTime > 0) {
564
- this.metrics.fps = Math.round(1e3 / deltaTime);
565
- }
566
- }
567
- /**
568
- * GPU 가속 활성화
569
- */
570
- enableGPUAcceleration(element) {
571
- if (!this.config.enableGPUAcceleration) return;
572
- try {
573
- element.style.willChange = "transform, opacity";
574
- element.style.transform = "translateZ(0)";
575
- element.style.backfaceVisibility = "hidden";
576
- element.style.transformStyle = "preserve-3d";
577
- this.registerLayer(element);
578
- } catch (error) {
579
- if (process.env.NODE_ENV === "development") {
580
- console.warn("GPU acceleration failed:", error);
581
- }
582
- }
583
- }
584
- /**
585
- * 레이어 분리 및 최적화
586
- */
587
- createOptimizedLayer(element) {
588
- if (!this.config.enableLayerSeparation) return;
589
- try {
590
- element.style.transform = "translateZ(0)";
591
- element.style.backfaceVisibility = "hidden";
592
- element.style.perspective = "1000px";
593
- this.registerLayer(element);
594
- this.checkLayerLimit();
595
- } catch (error) {
596
- if (process.env.NODE_ENV === "development") {
597
- console.warn("Layer optimization failed:", error);
598
- }
599
- }
600
- }
601
- /**
602
- * 레이어 등록
603
- */
604
- registerLayer(element) {
605
- if (this.layerRegistry.has(element)) return;
606
- this.layerRegistry.add(element);
607
- this.metrics.layerCount = this.layerRegistry.size;
608
- this.checkMemoryUsage();
609
- }
610
- /**
611
- * 레이어 제거
612
- */
613
- removeLayer(element) {
614
- if (this.layerRegistry.has(element)) {
615
- this.layerRegistry.delete(element);
616
- this.metrics.layerCount = this.layerRegistry.size;
617
- element.style.willChange = "auto";
618
- element.style.transform = "";
619
- element.style.backfaceVisibility = "";
620
- element.style.transformStyle = "";
621
- element.style.perspective = "";
622
- }
623
- }
624
- /**
625
- * 레이어 수 제한 체크
626
- */
627
- checkLayerLimit() {
628
- if (this.metrics.layerCount > this.config.maxLayerCount) {
629
- if (process.env.NODE_ENV === "development") {
630
- console.warn(`Layer count (${this.metrics.layerCount}) exceeds limit (${this.config.maxLayerCount})`);
631
- }
632
- this.cleanupOldLayers();
633
- }
634
- }
635
- /**
636
- * 오래된 레이어 정리
637
- */
638
- cleanupOldLayers() {
639
- const layersToRemove = Array.from(this.layerRegistry).slice(0, 10);
640
- layersToRemove.forEach((layer) => {
641
- this.removeLayer(layer);
642
- });
643
- }
644
- /**
645
- * 메모리 사용량 체크
646
- */
647
- checkMemoryUsage() {
648
- if (!this.config.enableMemoryOptimization) return;
649
- if ("memory" in performance) {
650
- const memory = performance.memory;
651
- this.metrics.memoryUsage = memory.usedJSHeapSize;
652
- if (memory.usedJSHeapSize > this.config.memoryThreshold) {
653
- if (process.env.NODE_ENV === "development") {
654
- console.warn("Memory usage high, cleaning up...");
655
- }
656
- this.cleanupMemory();
657
- }
658
- }
659
- }
660
- /**
661
- * 메모리 정리
662
- */
663
- cleanupMemory() {
664
- if ("gc" in window) {
665
- try {
666
- window.gc();
667
- } catch (error) {
668
- }
669
- }
670
- this.cleanupOldLayers();
671
- }
672
- /**
673
- * 성능 최적화 설정 업데이트
674
- */
675
- updateConfig(newConfig) {
676
- this.config = { ...this.config, ...newConfig };
677
- if (!this.config.enableGPUAcceleration) {
678
- this.disableAllGPUAcceleration();
679
- }
680
- if (!this.config.enableLayerSeparation) {
681
- this.disableAllLayers();
682
- }
683
- }
684
- /**
685
- * 모든 GPU 가속 비활성화
686
- */
687
- disableAllGPUAcceleration() {
688
- this.layerRegistry.forEach((element) => {
689
- element.style.willChange = "auto";
690
- element.style.transform = "";
691
- });
692
- }
693
- /**
694
- * 모든 레이어 비활성화
695
- */
696
- disableAllLayers() {
697
- this.layerRegistry.forEach((element) => {
698
- this.removeLayer(element);
699
- });
700
- }
701
- /**
702
- * 성능 메트릭 가져오기
703
- */
704
- getMetrics() {
705
- return { ...this.metrics };
706
- }
707
- /**
708
- * 성능 모니터링 시작
709
- */
710
- startMonitoring() {
711
- if (this.isMonitoring) return;
712
- this.isMonitoring = true;
713
- this.monitoringInterval = setInterval(() => {
714
- this.updateMetrics();
715
- }, 1e3);
716
- }
717
- /**
718
- * 성능 모니터링 중지
719
- */
720
- stopMonitoring() {
721
- if (!this.isMonitoring) return;
722
- this.isMonitoring = false;
723
- if (this.monitoringInterval) {
724
- clearInterval(this.monitoringInterval);
725
- this.monitoringInterval = void 0;
726
- }
727
- }
728
- /**
729
- * 메트릭 업데이트
730
- */
731
- updateMetrics() {
732
- if ("memory" in performance) {
733
- const memory = performance.memory;
734
- this.metrics.memoryUsage = memory.usedJSHeapSize;
735
- }
736
- this.metrics.layerCount = this.layerRegistry.size;
737
- }
738
- /**
739
- * 성능 리포트 생성
740
- */
741
- generateReport() {
742
- const metrics = this.getMetrics();
743
- return `
744
- === HUA Motion Performance Report ===
745
- FPS: ${metrics.fps}
746
- Active Layers: ${metrics.layerCount}
747
- Memory Usage: ${this.formatBytes(metrics.memoryUsage || 0)}
748
- Active Motions: ${metrics.activeMotions}
749
- GPU Acceleration: ${this.config.enableGPUAcceleration ? "Enabled" : "Disabled"}
750
- Layer Separation: ${this.config.enableLayerSeparation ? "Enabled" : "Disabled"}
751
- Memory Optimization: ${this.config.enableMemoryOptimization ? "Enabled" : "Disabled"}
752
- =====================================
753
- `.trim();
754
- }
755
- /**
756
- * 바이트 단위 포맷팅
757
- */
758
- formatBytes(bytes) {
759
- if (bytes === 0) return "0 B";
760
- const k = 1024;
761
- const sizes = ["B", "KB", "MB", "GB"];
762
- const i = Math.floor(Math.log(bytes) / Math.log(k));
763
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
764
- }
765
- /**
766
- * 성능 최적화 권장사항
767
- */
768
- getOptimizationRecommendations() {
769
- const recommendations = [];
770
- const metrics = this.getMetrics();
771
- if (metrics.fps < this.config.targetFPS) {
772
- recommendations.push("FPS\uAC00 \uB0AE\uC2B5\uB2C8\uB2E4. GPU \uAC00\uC18D\uC744 \uD65C\uC131\uD654\uD558\uAC70\uB098 \uB808\uC774\uC5B4 \uC218\uB97C \uC904\uC774\uC138\uC694.");
773
- }
774
- if (metrics.layerCount > this.config.maxLayerCount * 0.8) {
775
- recommendations.push("\uB808\uC774\uC5B4 \uC218\uAC00 \uB9CE\uC2B5\uB2C8\uB2E4. \uBD88\uD544\uC694\uD55C \uB808\uC774\uC5B4\uB97C \uC815\uB9AC\uD558\uC138\uC694.");
776
- }
777
- if (metrics.memoryUsage && metrics.memoryUsage > this.config.memoryThreshold * 0.8) {
778
- recommendations.push("\uBA54\uBAA8\uB9AC \uC0AC\uC6A9\uB7C9\uC774 \uB192\uC2B5\uB2C8\uB2E4. \uBA54\uBAA8\uB9AC \uC815\uB9AC\uB97C \uACE0\uB824\uD558\uC138\uC694.");
779
- }
780
- return recommendations;
781
- }
782
- /**
783
- * 정리
784
- */
785
- destroy() {
786
- this.stopMonitoring();
787
- if (this.performanceObserver) {
788
- this.performanceObserver.disconnect();
789
- this.performanceObserver = null;
790
- }
791
- this.layerRegistry.forEach((element) => {
792
- this.removeLayer(element);
793
- });
794
- this.layerRegistry.clear();
795
- }
796
- };
797
- var performanceOptimizer = PerformanceOptimizer.getInstance();
798
-
799
- // src/presets/index.ts
800
- var MOTION_PRESETS = {
801
- hero: {
802
- entrance: "fadeIn",
803
- delay: 200,
804
- duration: 800,
805
- hover: false,
806
- click: false
807
- },
808
- title: {
809
- entrance: "slideUp",
810
- delay: 400,
811
- duration: 700,
812
- hover: false,
813
- click: false
814
- },
815
- button: {
816
- entrance: "scaleIn",
817
- delay: 600,
818
- duration: 300,
819
- hover: true,
820
- click: true
821
- },
822
- card: {
823
- entrance: "slideUp",
824
- delay: 800,
825
- duration: 500,
826
- hover: true,
827
- click: false
828
- },
829
- text: {
830
- entrance: "fadeIn",
831
- delay: 200,
832
- duration: 600,
833
- hover: false,
834
- click: false
835
- },
836
- image: {
837
- entrance: "scaleIn",
838
- delay: 400,
839
- duration: 600,
840
- hover: true,
841
- click: false
842
- }
843
- };
844
- var PAGE_MOTIONS = {
845
- // 홈페이지
846
- home: {
847
- hero: { type: "hero" },
848
- title: { type: "title" },
849
- description: { type: "text" },
850
- cta: { type: "button" },
851
- feature1: { type: "card" },
852
- feature2: { type: "card" },
853
- feature3: { type: "card" }
854
- },
855
- // 대시보드
856
- dashboard: {
857
- header: { type: "hero" },
858
- sidebar: { type: "card", entrance: "slideLeft" },
859
- main: { type: "text", entrance: "fadeIn" },
860
- card1: { type: "card" },
861
- card2: { type: "card" },
862
- card3: { type: "card" },
863
- chart: { type: "image" }
864
- },
865
- // 제품 페이지
866
- product: {
867
- hero: { type: "hero" },
868
- title: { type: "title" },
869
- image: { type: "image" },
870
- description: { type: "text" },
871
- price: { type: "text" },
872
- buyButton: { type: "button" },
873
- features: { type: "card" }
874
- },
875
- // 블로그
876
- blog: {
877
- header: { type: "hero" },
878
- title: { type: "title" },
879
- content: { type: "text" },
880
- sidebar: { type: "card", entrance: "slideRight" },
881
- related1: { type: "card" },
882
- related2: { type: "card" },
883
- related3: { type: "card" }
884
- }
885
- };
886
- function mergeWithPreset(preset, custom = {}) {
887
- return {
888
- ...preset,
889
- ...custom
890
- };
891
- }
892
- function getPagePreset(pageType) {
893
- return PAGE_MOTIONS[pageType];
894
- }
895
- function getMotionPreset(type) {
896
- return MOTION_PRESETS[type] || MOTION_PRESETS.text;
897
- }
898
-
899
- // src/hooks/useSimplePageMotion.ts
900
- function useSimplePageMotion(pageType) {
901
- const config = getPagePreset(pageType);
902
- return useSimplePageMotions(config);
903
- }
904
- function useSimplePageMotions(config) {
905
- const [motions, setMotions] = react.useState(/* @__PURE__ */ new Map());
906
- const observersRef = react.useRef(/* @__PURE__ */ new Map());
907
- const calculateMotionValues = react.useCallback((isVisible, elementConfig) => {
908
- const preset = getMotionPreset(elementConfig.type);
909
- mergeWithPreset(preset, elementConfig);
910
- let opacity = isVisible ? 1 : 0;
911
- let translateY = isVisible ? 0 : 20;
912
- let translateX = 0;
913
- let scale = isVisible ? 1 : 0.95;
914
- return { opacity, translateY, translateX, scale };
915
- }, []);
916
- react.useEffect(() => {
917
- const newMotions = /* @__PURE__ */ new Map();
918
- Object.entries(config).forEach(([elementId, elementConfig]) => {
919
- const ref = { current: null };
920
- const { opacity, translateY, translateX, scale } = calculateMotionValues(false, elementConfig);
921
- newMotions.set(elementId, {
922
- ref,
923
- style: {
924
- opacity,
925
- transform: `translate(${translateX}px, ${translateY}px) scale(${scale})`,
926
- transition: `all ${elementConfig.duration || 700}ms ease-out`,
927
- pointerEvents: "auto",
928
- willChange: "transform, opacity"
929
- },
930
- isVisible: false,
931
- isHovered: false,
932
- isClicked: false
933
- });
934
- });
935
- setMotions(newMotions);
936
- }, [config, calculateMotionValues]);
937
- react.useEffect(() => {
938
- const visibleElements = /* @__PURE__ */ new Set();
939
- Object.entries(config).forEach(([elementId, elementConfig]) => {
940
- const observer = new IntersectionObserver(
941
- (entries) => {
942
- entries.forEach((entry) => {
943
- if (entry.isIntersecting && !visibleElements.has(elementId)) {
944
- visibleElements.add(elementId);
945
- const preset = getMotionPreset(elementConfig.type);
946
- const mergedConfig = mergeWithPreset(preset, elementConfig);
947
- const delay = mergedConfig.delay || 0;
948
- setTimeout(() => {
949
- const { opacity, translateY, translateX, scale } = calculateMotionValues(true, elementConfig);
950
- setMotions((prev) => {
951
- const current = prev.get(elementId);
952
- if (!current) return prev;
953
- const newMotion = {
954
- ...current,
955
- style: {
956
- ...current.style,
957
- opacity,
958
- transform: `translate(${translateX}px, ${translateY}px) scale(${scale})`
959
- },
960
- isVisible: true
961
- };
962
- const newMap = new Map(prev);
963
- newMap.set(elementId, newMotion);
964
- return newMap;
965
- });
966
- if (process.env.NODE_ENV === "development") {
967
- console.log("\uBAA8\uC158 \uC2E4\uD589:", elementId, "delay:", delay);
968
- }
969
- }, delay);
970
- observer.unobserve(entry.target);
971
- }
972
- });
973
- },
974
- { threshold: elementConfig.threshold || 0.1 }
975
- );
976
- observersRef.current.set(elementId, observer);
977
- });
978
- const timer = setTimeout(() => {
979
- Object.entries(config).forEach(([elementId]) => {
980
- const observer = observersRef.current.get(elementId);
981
- if (observer) {
982
- const element = document.querySelector(`[data-motion-id="${elementId}"]`);
983
- if (element) {
984
- observer.observe(element);
985
- }
986
- }
987
- });
988
- }, 100);
989
- return () => {
990
- clearTimeout(timer);
991
- observersRef.current.forEach((observer) => observer.disconnect());
992
- observersRef.current.clear();
993
- };
994
- }, [config, calculateMotionValues]);
995
- const getPageMotionRefs = react.useCallback(() => {
996
- const result = {};
997
- motions.forEach((motion, elementId) => {
998
- result[elementId] = motion;
999
- });
1000
- return result;
1001
- }, [motions]);
1002
- return getPageMotionRefs();
1003
- }
1004
-
1005
- // src/managers/MotionStateManager.ts
1006
- var MotionStateManager = class {
1007
- constructor() {
1008
- this.states = /* @__PURE__ */ new Map();
1009
- this.listeners = /* @__PURE__ */ new Map();
1010
- }
1011
- /**
1012
- * 요소의 상태를 초기화
1013
- */
1014
- initializeElement(elementId, config) {
1015
- const initialState = {
1016
- internalVisibility: false,
1017
- // 초기에 숨김 상태로 시작 (스크롤 리빌용)
1018
- triggeredVisibility: false,
1019
- // Intersection Observer가 아직 트리거되지 않음
1020
- finalVisibility: false,
1021
- // 초기에 숨김 상태로 시작
1022
- opacity: 0,
1023
- // 초기에 투명 상태로 시작
1024
- translateY: 20,
1025
- // 초기에 아래로 이동된 상태로 시작
1026
- translateX: 0,
1027
- scale: 0.95,
1028
- // 초기에 약간 축소된 상태로 시작
1029
- rotation: 0,
1030
- isHovered: false,
1031
- isClicked: false,
1032
- isAnimating: false
1033
- };
1034
- this.states.set(elementId, initialState);
1035
- this.computeFinalState(elementId);
1036
- }
1037
- /**
1038
- * 내부 가시성 상태 업데이트 (초기화, 리셋 등)
1039
- */
1040
- setInternalVisibility(elementId, visible) {
1041
- const state = this.states.get(elementId);
1042
- if (!state) return;
1043
- state.internalVisibility = visible;
1044
- this.computeFinalState(elementId);
1045
- this.notifyListeners(elementId, state);
1046
- }
1047
- /**
1048
- * 외부 트리거 가시성 상태 업데이트 (Intersection Observer)
1049
- */
1050
- setTriggeredVisibility(elementId, visible) {
1051
- const state = this.states.get(elementId);
1052
- if (!state) return;
1053
- state.triggeredVisibility = visible;
1054
- this.computeFinalState(elementId);
1055
- this.notifyListeners(elementId, state);
1056
- }
1057
- /**
1058
- * 모션 값 업데이트
1059
- */
1060
- updateMotionValues(elementId, values) {
1061
- const state = this.states.get(elementId);
1062
- if (!state) return;
1063
- Object.assign(state, values);
1064
- this.notifyListeners(elementId, state);
1065
- }
1066
- /**
1067
- * 최종 상태 계산
1068
- */
1069
- computeFinalState(elementId) {
1070
- const state = this.states.get(elementId);
1071
- if (!state) return;
1072
- state.finalVisibility = state.internalVisibility || state.triggeredVisibility;
1073
- state.isAnimating = state.finalVisibility && (state.opacity < 1 || state.translateY > 0);
1074
- }
1075
- /**
1076
- * 현재 상태 조회
1077
- */
1078
- getState(elementId) {
1079
- return this.states.get(elementId);
1080
- }
1081
- /**
1082
- * 모든 상태 조회
1083
- */
1084
- getAllStates() {
1085
- return new Map(this.states);
1086
- }
1087
- /**
1088
- * 상태 변경 리스너 등록
1089
- */
1090
- subscribe(elementId, listener) {
1091
- if (!this.listeners.has(elementId)) {
1092
- this.listeners.set(elementId, /* @__PURE__ */ new Set());
1093
- }
1094
- this.listeners.get(elementId).add(listener);
1095
- return () => {
1096
- const listeners = this.listeners.get(elementId);
1097
- if (listeners) {
1098
- listeners.delete(listener);
1099
- }
1100
- };
1101
- }
1102
- /**
1103
- * 리스너들에게 상태 변경 알림
1104
- */
1105
- notifyListeners(elementId, state) {
1106
- const listeners = this.listeners.get(elementId);
1107
- if (!listeners) return;
1108
- listeners.forEach((listener) => {
1109
- try {
1110
- listener(state);
1111
- } catch (error) {
1112
- if (process.env.NODE_ENV === "development") {
1113
- console.error(`MotionStateManager listener error for ${elementId}:`, error);
1114
- }
1115
- }
1116
- });
1117
- }
1118
- /**
1119
- * 모든 상태 초기화
1120
- */
1121
- reset() {
1122
- this.states.clear();
1123
- this.listeners.clear();
1124
- }
1125
- /**
1126
- * 특정 요소 상태 초기화
1127
- */
1128
- resetElement(elementId) {
1129
- this.states.delete(elementId);
1130
- this.listeners.delete(elementId);
1131
- }
1132
- /**
1133
- * 디버그용 상태 출력
1134
- */
1135
- debug() {
1136
- if (process.env.NODE_ENV === "development") {
1137
- console.log("MotionStateManager Debug:");
1138
- this.states.forEach((state, elementId) => {
1139
- console.log(` ${elementId}:`, {
1140
- internalVisibility: state.internalVisibility,
1141
- triggeredVisibility: state.triggeredVisibility,
1142
- finalVisibility: state.finalVisibility,
1143
- opacity: state.opacity,
1144
- translateY: state.translateY,
1145
- isAnimating: state.isAnimating
1146
- });
1147
- });
1148
- }
1149
- }
1150
- };
1151
- var motionStateManager = new MotionStateManager();
1152
-
1153
- // src/hooks/usePageMotions.ts
1154
- function usePageMotions(config) {
1155
- const [motions, setMotions] = react.useState(/* @__PURE__ */ new Map());
1156
- const observersRef = react.useRef(/* @__PURE__ */ new Map());
1157
- const unsubscribeRef = react.useRef(/* @__PURE__ */ new Map());
1158
- const [resetKey, setResetKey] = react.useState(0);
1159
- const reset = react.useCallback(() => {
1160
- observersRef.current.forEach((observer) => observer.disconnect());
1161
- observersRef.current.clear();
1162
- unsubscribeRef.current.forEach((unsubscribe) => unsubscribe());
1163
- unsubscribeRef.current.clear();
1164
- motionStateManager.reset();
1165
- setMotions(/* @__PURE__ */ new Map());
1166
- setResetKey((prev) => prev + 1);
1167
- }, []);
1168
- const calculateMotionValues = react.useCallback((state, elementConfig) => {
1169
- const preset = getMotionPreset(elementConfig.type);
1170
- const mergedConfig = mergeWithPreset(preset, elementConfig);
1171
- let opacity = state.finalVisibility ? 1 : 0;
1172
- let translateY = state.finalVisibility ? 0 : 20;
1173
- let translateX = 0;
1174
- let scale = state.finalVisibility ? 1 : 0.95;
1175
- if (mergedConfig.hover && state.isHovered) {
1176
- scale *= 1.1;
1177
- translateY -= 5;
1178
- opacity = 0.9;
1179
- }
1180
- if (mergedConfig.click && state.isClicked) {
1181
- scale *= 0.9;
1182
- translateY += 3;
1183
- opacity = 0.8;
1184
- }
1185
- return { opacity, translateY, translateX, scale };
1186
- }, []);
1187
- const updateMotionState = react.useCallback((elementId, updates) => {
1188
- const currentState = motionStateManager.getState(elementId);
1189
- if (!currentState) return;
1190
- if (updates.opacity !== void 0 || updates.translateY !== void 0 || updates.translateX !== void 0 || updates.scale !== void 0) {
1191
- motionStateManager.updateMotionValues(elementId, updates);
1192
- }
1193
- if (updates.isHovered !== void 0) {
1194
- currentState.isHovered = updates.isHovered;
1195
- motionStateManager.notifyListeners(elementId, currentState);
1196
- }
1197
- if (updates.isClicked !== void 0) {
1198
- currentState.isClicked = updates.isClicked;
1199
- motionStateManager.notifyListeners(elementId, currentState);
1200
- }
1201
- }, []);
1202
- react.useEffect(() => {
1203
- const newMotions = /* @__PURE__ */ new Map();
1204
- if (!config || typeof config !== "object") {
1205
- if (process.env.NODE_ENV === "development") {
1206
- console.warn("usePageMotions: config\uAC00 \uC720\uD6A8\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4:", config);
1207
- }
1208
- return;
1209
- }
1210
- motionStateManager.reset();
1211
- Object.entries(config).forEach(([elementId, elementConfig]) => {
1212
- const ref = { current: null };
1213
- motionStateManager.initializeElement(elementId, elementConfig);
1214
- const initialState = motionStateManager.getState(elementId);
1215
- if (process.env.NODE_ENV === "development") {
1216
- console.log(`\uCD08\uAE30 \uC0C1\uD0DC [${elementId}]:`, initialState);
1217
- }
1218
- const { opacity, translateY, translateX, scale } = calculateMotionValues(initialState, elementConfig);
1219
- newMotions.set(elementId, {
1220
- ref,
1221
- style: {
1222
- opacity,
1223
- transform: `translate(${translateX}px, ${translateY}px) scale(${scale})`,
1224
- transition: `all ${elementConfig.duration || 700}ms ease-out`,
1225
- pointerEvents: "auto",
1226
- willChange: "transform, opacity"
1227
- },
1228
- isVisible: initialState.finalVisibility,
1229
- isHovered: initialState.isHovered,
1230
- isClicked: initialState.isClicked
1231
- });
1232
- const unsubscribe = motionStateManager.subscribe(elementId, (newState) => {
1233
- const { opacity: opacity2, translateY: translateY2, translateX: translateX2, scale: scale2 } = calculateMotionValues(newState, elementConfig);
1234
- setMotions((prev) => {
1235
- const current = prev.get(elementId);
1236
- if (!current) return prev;
1237
- const transform = `translate(${translateX2}px, ${translateY2}px) scale(${scale2})`;
1238
- const hasChanged = current.style.opacity !== opacity2 || current.style.transform !== transform || current.isVisible !== newState.finalVisibility || current.isHovered !== newState.isHovered || current.isClicked !== newState.isClicked;
1239
- if (!hasChanged) return prev;
1240
- const newMotion = {
1241
- ...current,
1242
- style: {
1243
- ...current.style,
1244
- opacity: opacity2,
1245
- transform
1246
- },
1247
- isVisible: newState.finalVisibility,
1248
- isHovered: newState.isHovered,
1249
- isClicked: newState.isClicked
1250
- };
1251
- const newMap = new Map(prev);
1252
- newMap.set(elementId, newMotion);
1253
- return newMap;
1254
- });
1255
- });
1256
- unsubscribeRef.current.set(elementId, unsubscribe);
1257
- });
1258
- setMotions(newMotions);
1259
- return () => {
1260
- unsubscribeRef.current.forEach((unsubscribe) => unsubscribe());
1261
- unsubscribeRef.current.clear();
1262
- motionStateManager.reset();
1263
- };
1264
- }, [config, resetKey]);
1265
- react.useEffect(() => {
1266
- const visibleElements = /* @__PURE__ */ new Set();
1267
- if (!config || typeof config !== "object") {
1268
- return;
1269
- }
1270
- Object.entries(config).forEach(([elementId, elementConfig]) => {
1271
- const observer = new IntersectionObserver(
1272
- (entries) => {
1273
- entries.forEach((entry) => {
1274
- if (entry.isIntersecting && !visibleElements.has(elementId)) {
1275
- visibleElements.add(elementId);
1276
- const preset = getMotionPreset(elementConfig.type);
1277
- const mergedConfig = mergeWithPreset(preset, elementConfig);
1278
- const delay = mergedConfig.delay || 0;
1279
- setTimeout(() => {
1280
- motionStateManager.setTriggeredVisibility(elementId, true);
1281
- if (process.env.NODE_ENV === "development") {
1282
- console.log("\uBAA8\uC158 \uC2E4\uD589:", elementId, "delay:", delay);
1283
- }
1284
- }, delay);
1285
- observer.unobserve(entry.target);
1286
- }
1287
- });
1288
- },
1289
- {
1290
- threshold: elementConfig.threshold || 0.3,
1291
- // 30% 보여야 실행
1292
- rootMargin: "0px 0px -50px 0px"
1293
- // 하단에서 50px 전에 실행
1294
- }
1295
- );
1296
- observersRef.current.set(elementId, observer);
1297
- });
1298
- const timer = setTimeout(() => {
1299
- if (!config || typeof config !== "object") {
1300
- return;
1301
- }
1302
- Object.entries(config).forEach(([elementId]) => {
1303
- const observer = observersRef.current.get(elementId);
1304
- if (observer) {
1305
- const element = document.querySelector(`[data-motion-id="${elementId}"]`);
1306
- if (element) {
1307
- observer.observe(element);
1308
- }
1309
- }
1310
- });
1311
- }, 100);
1312
- return () => {
1313
- clearTimeout(timer);
1314
- observersRef.current.forEach((observer) => observer.disconnect());
1315
- observersRef.current.clear();
1316
- };
1317
- }, [config, resetKey]);
1318
- react.useEffect(() => {
1319
- if (!config || typeof config !== "object") {
1320
- return;
1321
- }
1322
- const handleMouseEnter = (event) => {
1323
- const target = event.target;
1324
- if (!target) return;
1325
- let element = target;
1326
- let elementId = null;
1327
- while (element && element !== document.body) {
1328
- if (element.getAttribute && typeof element.getAttribute === "function") {
1329
- elementId = element.getAttribute("data-motion-id");
1330
- if (elementId) break;
1331
- }
1332
- element = element.parentElement;
1333
- }
1334
- if (elementId && config[elementId]?.hover) {
1335
- if (process.env.NODE_ENV === "development") {
1336
- console.log("\uD638\uBC84 \uC2DC\uC791:", elementId);
1337
- }
1338
- updateMotionState(elementId, { isHovered: true });
1339
- }
1340
- };
1341
- const handleMouseLeave = (event) => {
1342
- const target = event.target;
1343
- if (!target) return;
1344
- let element = target;
1345
- let elementId = null;
1346
- while (element && element !== document.body) {
1347
- if (element.getAttribute && typeof element.getAttribute === "function") {
1348
- elementId = element.getAttribute("data-motion-id");
1349
- if (elementId) break;
1350
- }
1351
- element = element.parentElement;
1352
- }
1353
- if (elementId && config[elementId]?.hover) {
1354
- if (process.env.NODE_ENV === "development") {
1355
- console.log("\uD638\uBC84 \uC885\uB8CC:", elementId);
1356
- }
1357
- updateMotionState(elementId, { isHovered: false });
1358
- }
1359
- };
1360
- const handleMouseDown = (event) => {
1361
- const target = event.target;
1362
- if (!target) return;
1363
- let element = target;
1364
- let elementId = null;
1365
- while (element && element !== document.body) {
1366
- if (element.getAttribute && typeof element.getAttribute === "function") {
1367
- elementId = element.getAttribute("data-motion-id");
1368
- if (elementId) break;
1369
- }
1370
- element = element.parentElement;
1371
- }
1372
- if (elementId && config[elementId]?.click) {
1373
- if (process.env.NODE_ENV === "development") {
1374
- console.log("\uD074\uB9AD \uC2DC\uC791:", elementId);
1375
- }
1376
- updateMotionState(elementId, { isClicked: true });
1377
- }
1378
- };
1379
- const handleMouseUp = (event) => {
1380
- const target = event.target;
1381
- if (!target) return;
1382
- let element = target;
1383
- let elementId = null;
1384
- while (element && element !== document.body) {
1385
- if (element.getAttribute && typeof element.getAttribute === "function") {
1386
- elementId = element.getAttribute("data-motion-id");
1387
- if (elementId) break;
1388
- }
1389
- element = element.parentElement;
1390
- }
1391
- if (elementId && config[elementId]?.click) {
1392
- if (process.env.NODE_ENV === "development") {
1393
- console.log("\uD074\uB9AD \uC885\uB8CC:", elementId);
1394
- }
1395
- updateMotionState(elementId, { isClicked: false });
1396
- }
1397
- };
1398
- const timer = setTimeout(() => {
1399
- document.addEventListener("mouseenter", handleMouseEnter, false);
1400
- document.addEventListener("mouseleave", handleMouseLeave, false);
1401
- document.addEventListener("mousedown", handleMouseDown, false);
1402
- document.addEventListener("mouseup", handleMouseUp, false);
1403
- }, 200);
1404
- return () => {
1405
- clearTimeout(timer);
1406
- document.removeEventListener("mouseenter", handleMouseEnter, false);
1407
- document.removeEventListener("mouseleave", handleMouseLeave, false);
1408
- document.removeEventListener("mousedown", handleMouseDown, false);
1409
- document.removeEventListener("mouseup", handleMouseUp, false);
1410
- };
1411
- }, [config]);
1412
- const getPageMotionRefs = react.useCallback(() => {
1413
- const result = {};
1414
- motions.forEach((motion, elementId) => {
1415
- result[elementId] = motion;
1416
- });
1417
- return result;
1418
- }, [motions]);
1419
- return {
1420
- ...getPageMotionRefs(),
1421
- reset
1422
- };
1423
- }
1424
- function useSmartMotion(options = {}) {
1425
- const {
1426
- type = "text",
1427
- entrance: customEntrance,
1428
- hover: customHover,
1429
- click: customClick,
1430
- delay: customDelay,
1431
- duration: customDuration,
1432
- threshold = 0.1,
1433
- autoLanguageSync = false
1434
- } = options;
1435
- const getPresetConfig = react.useCallback(() => {
1436
- return MOTION_PRESETS[type] || MOTION_PRESETS.text;
1437
- }, [type]);
1438
- const preset = getPresetConfig();
1439
- const entrance = customEntrance || preset.entrance;
1440
- const hover = customHover !== void 0 ? customHover : preset.hover;
1441
- const click = customClick !== void 0 ? customClick : preset.click;
1442
- const delay = customDelay !== void 0 ? customDelay : preset.delay;
1443
- const duration = customDuration !== void 0 ? customDuration : preset.duration;
1444
- const elementRef = react.useRef(null);
1445
- const getInitialMotionValues = () => {
1446
- const initialState = {
1447
- isVisible: false,
1448
- isHovered: false,
1449
- isClicked: false,
1450
- opacity: 0,
1451
- translateY: 0,
1452
- translateX: 0,
1453
- scale: 1
1454
- };
1455
- switch (entrance) {
1456
- case "fadeIn":
1457
- initialState.opacity = 0;
1458
- break;
1459
- case "slideUp":
1460
- initialState.opacity = 0;
1461
- initialState.translateY = 20;
1462
- break;
1463
- case "slideLeft":
1464
- initialState.opacity = 0;
1465
- initialState.translateX = -20;
1466
- break;
1467
- case "slideRight":
1468
- initialState.opacity = 0;
1469
- initialState.translateX = 20;
1470
- break;
1471
- case "scaleIn":
1472
- initialState.opacity = 0;
1473
- initialState.scale = 0.8;
1474
- break;
1475
- case "bounceIn":
1476
- initialState.opacity = 0;
1477
- initialState.scale = 0.5;
1478
- break;
1479
- }
1480
- return initialState;
1481
- };
1482
- const [state, setState] = react.useState(() => {
1483
- const initialValues = getInitialMotionValues();
1484
- if (threshold === 0) {
1485
- initialValues.isVisible = true;
1486
- initialValues.opacity = 1;
1487
- initialValues.translateY = 0;
1488
- initialValues.translateX = 0;
1489
- initialValues.scale = 1;
1490
- }
1491
- return initialValues;
1492
- });
1493
- const calculateMotionValues = react.useCallback((currentState) => {
1494
- const { isVisible, isHovered, isClicked } = currentState;
1495
- let opacity = 0;
1496
- let translateY = 0;
1497
- let translateX = 0;
1498
- let scale = 1;
1499
- if (isVisible) {
1500
- opacity = 1;
1501
- switch (entrance) {
1502
- case "fadeIn":
1503
- break;
1504
- case "slideUp":
1505
- translateY = 0;
1506
- break;
1507
- case "slideLeft":
1508
- translateX = 0;
1509
- break;
1510
- case "slideRight":
1511
- translateX = 0;
1512
- break;
1513
- case "scaleIn":
1514
- scale = 1;
1515
- break;
1516
- case "bounceIn":
1517
- scale = 1;
1518
- break;
1519
- }
1520
- } else {
1521
- switch (entrance) {
1522
- case "fadeIn":
1523
- opacity = 0;
1524
- break;
1525
- case "slideUp":
1526
- opacity = 0;
1527
- translateY = 20;
1528
- break;
1529
- case "slideLeft":
1530
- opacity = 0;
1531
- translateX = -20;
1532
- break;
1533
- case "slideRight":
1534
- opacity = 0;
1535
- translateX = 20;
1536
- break;
1537
- case "scaleIn":
1538
- opacity = 0;
1539
- scale = 0.8;
1540
- break;
1541
- case "bounceIn":
1542
- opacity = 0;
1543
- scale = 0.5;
1544
- break;
1545
- }
1546
- }
1547
- if (hover && isHovered) {
1548
- scale *= 1.1;
1549
- translateY -= 5;
1550
- }
1551
- if (click && isClicked) {
1552
- scale *= 0.85;
1553
- translateY += 3;
1554
- }
1555
- return { opacity, translateY, translateX, scale };
1556
- }, [entrance, hover, click]);
1557
- react.useEffect(() => {
1558
- if (!elementRef.current) return;
1559
- const observer = new IntersectionObserver(
1560
- (entries) => {
1561
- entries.forEach((entry) => {
1562
- if (entry.isIntersecting) {
1563
- setTimeout(() => {
1564
- setState((prev) => ({ ...prev, isVisible: true }));
1565
- }, delay);
1566
- }
1567
- });
1568
- },
1569
- { threshold }
1570
- );
1571
- observer.observe(elementRef.current);
1572
- return () => {
1573
- observer.disconnect();
1574
- };
1575
- }, [delay, threshold]);
1576
- react.useEffect(() => {
1577
- if (!hover || !elementRef.current) return;
1578
- const element = elementRef.current;
1579
- const handleMouseEnter = () => {
1580
- setState((prev) => ({ ...prev, isHovered: true }));
1581
- };
1582
- const handleMouseLeave = () => {
1583
- setState((prev) => ({ ...prev, isHovered: false }));
1584
- };
1585
- element.addEventListener("mouseenter", handleMouseEnter);
1586
- element.addEventListener("mouseleave", handleMouseLeave);
1587
- return () => {
1588
- element.removeEventListener("mouseenter", handleMouseEnter);
1589
- element.removeEventListener("mouseleave", handleMouseLeave);
1590
- };
1591
- }, [hover]);
1592
- react.useEffect(() => {
1593
- if (!click || !elementRef.current) return;
1594
- const element = elementRef.current;
1595
- const handleClick = () => {
1596
- setState((prev) => ({ ...prev, isClicked: true }));
1597
- setTimeout(() => {
1598
- setState((prev) => ({ ...prev, isClicked: false }));
1599
- }, 300);
1600
- };
1601
- element.addEventListener("click", handleClick);
1602
- return () => {
1603
- element.removeEventListener("click", handleClick);
1604
- };
1605
- }, [click]);
1606
- react.useEffect(() => {
1607
- setState((prev) => {
1608
- const { opacity, translateY, translateX, scale } = calculateMotionValues(prev);
1609
- if (prev.opacity === opacity && prev.translateY === translateY && prev.translateX === translateX && prev.scale === scale) {
1610
- return prev;
1611
- }
1612
- return { ...prev, opacity, translateY, translateX, scale };
1613
- });
1614
- }, [state.isVisible, state.isHovered, state.isClicked, calculateMotionValues]);
1615
- react.useEffect(() => {
1616
- if (!autoLanguageSync) return;
1617
- const handleLanguageChange = () => {
1618
- setState((prev) => ({ ...prev, isVisible: false }));
1619
- setTimeout(() => {
1620
- setState((prev) => ({ ...prev, isVisible: true }));
1621
- }, 100);
1622
- };
1623
- window.addEventListener("storage", handleLanguageChange);
1624
- return () => {
1625
- window.removeEventListener("storage", handleLanguageChange);
1626
- };
1627
- }, [autoLanguageSync]);
1628
- const motionStyle = react.useMemo(() => ({
1629
- opacity: state.opacity,
1630
- transform: `translate(${state.translateX}px, ${state.translateY}px) scale(${state.scale})`,
1631
- transition: `all ${duration}ms ease-out`,
1632
- // CSS transition과 충돌 방지
1633
- pointerEvents: "auto",
1634
- // 강제로 스타일 적용
1635
- willChange: "transform, opacity"
1636
- }), [state.opacity, state.translateX, state.translateY, state.scale, duration]);
1637
- return {
1638
- ref: elementRef,
1639
- style: motionStyle,
1640
- isVisible: state.isVisible,
1641
- isHovered: state.isHovered,
1642
- isClicked: state.isClicked
1643
- };
1644
- }
1645
- function getInitialStyle(type, distance) {
1646
- switch (type) {
1647
- case "slideUp":
1648
- return { opacity: 0, transform: `translateY(${distance}px)` };
1649
- case "slideLeft":
1650
- return { opacity: 0, transform: `translateX(${distance}px)` };
1651
- case "slideRight":
1652
- return { opacity: 0, transform: `translateX(-${distance}px)` };
1653
- case "scaleIn":
1654
- return { opacity: 0, transform: "scale(0)" };
1655
- case "bounceIn":
1656
- return { opacity: 0, transform: "scale(0)" };
1657
- case "fadeIn":
1658
- default:
1659
- return { opacity: 0, transform: "none" };
1660
- }
1661
- }
1662
- function getVisibleStyle() {
1663
- return { opacity: 1, transform: "none" };
1664
- }
1665
- function getEasingForType(type, easing2) {
1666
- if (easing2) return easing2;
1667
- if (type === "bounceIn") return "cubic-bezier(0.34, 1.56, 0.64, 1)";
1668
- return "ease-out";
1669
- }
1670
- function useUnifiedMotion(options) {
1671
- const {
1672
- type,
1673
- duration = 600,
1674
- autoStart = true,
1675
- delay = 0,
1676
- easing: easing2,
1677
- threshold = 0.1,
1678
- triggerOnce = true,
1679
- distance = 50,
1680
- onComplete,
1681
- onStart,
1682
- onStop,
1683
- onReset
1684
- } = options;
1685
- const resolvedEasing = getEasingForType(type, easing2);
1686
- const ref = react.useRef(null);
1687
- const [isVisible, setIsVisible] = react.useState(false);
1688
- const [isAnimating, setIsAnimating] = react.useState(false);
1689
- const [progress, setProgress] = react.useState(0);
1690
- const observerRef = react.useRef(null);
1691
- const timeoutRef = react.useRef(null);
1692
- const startRef = react.useRef(() => {
1693
- });
1694
- const start = react.useCallback(() => {
1695
- if (isAnimating) return;
1696
- setIsAnimating(true);
1697
- setProgress(0);
1698
- onStart?.();
1699
- timeoutRef.current = window.setTimeout(() => {
1700
- setIsVisible(true);
1701
- setProgress(1);
1702
- setIsAnimating(false);
1703
- onComplete?.();
1704
- }, delay);
1705
- }, [isAnimating, delay, onStart, onComplete]);
1706
- startRef.current = start;
1707
- const stop = react.useCallback(() => {
1708
- if (timeoutRef.current) {
1709
- clearTimeout(timeoutRef.current);
1710
- timeoutRef.current = null;
1711
- }
1712
- setIsAnimating(false);
1713
- onStop?.();
1714
- }, [onStop]);
1715
- const reset = react.useCallback(() => {
1716
- stop();
1717
- setIsVisible(false);
1718
- setProgress(0);
1719
- onReset?.();
1720
- }, [stop, onReset]);
1721
- react.useEffect(() => {
1722
- if (!ref.current || !autoStart) return;
1723
- observerRef.current = new IntersectionObserver(
1724
- (entries) => {
1725
- entries.forEach((entry) => {
1726
- if (entry.isIntersecting) {
1727
- startRef.current();
1728
- if (triggerOnce) {
1729
- observerRef.current?.disconnect();
1730
- }
1731
- }
1732
- });
1733
- },
1734
- { threshold }
1735
- );
1736
- observerRef.current.observe(ref.current);
1737
- return () => {
1738
- observerRef.current?.disconnect();
1739
- };
1740
- }, [autoStart, threshold, triggerOnce]);
1741
- react.useEffect(() => {
1742
- return () => stop();
1743
- }, [stop]);
1744
- const style = react.useMemo(() => {
1745
- const base = isVisible ? getVisibleStyle() : getInitialStyle(type, distance);
1746
- return {
1747
- ...base,
1748
- transition: `all ${duration}ms ${resolvedEasing}`,
1749
- "--motion-delay": `${delay}ms`,
1750
- "--motion-duration": `${duration}ms`,
1751
- "--motion-easing": resolvedEasing,
1752
- "--motion-progress": `${progress}`
1753
- };
1754
- }, [isVisible, type, distance, duration, resolvedEasing, delay, progress]);
1755
- return {
1756
- ref,
1757
- isVisible,
1758
- isAnimating,
1759
- style,
1760
- progress,
1761
- start,
1762
- stop,
1763
- reset
1764
- };
1765
- }
1766
- function useFadeIn(options = {}) {
1767
- const {
1768
- delay = 0,
1769
- duration = 700,
1770
- threshold = 0.1,
1771
- triggerOnce = true,
1772
- easing: easing2 = "ease-out",
1773
- autoStart = true,
1774
- initialOpacity = 0,
1775
- targetOpacity = 1,
1776
- onComplete,
1777
- onStart,
1778
- onStop,
1779
- onReset
1780
- } = options;
1781
- const ref = react.useRef(null);
1782
- const [isVisible, setIsVisible] = react.useState(false);
1783
- const [isAnimating, setIsAnimating] = react.useState(false);
1784
- const [progress, setProgress] = react.useState(0);
1785
- const [nodeReady, setNodeReady] = react.useState(false);
1786
- const observerRef = react.useRef(null);
1787
- const motionRef = react.useRef(null);
1788
- const timeoutRef = react.useRef(null);
1789
- const startRef = react.useRef(() => {
1790
- });
1791
- react.useEffect(() => {
1792
- if (nodeReady) return;
1793
- if (ref.current) {
1794
- setNodeReady(true);
1795
- return;
1796
- }
1797
- const id = setInterval(() => {
1798
- if (ref.current) {
1799
- setNodeReady(true);
1800
- clearInterval(id);
1801
- }
1802
- }, 50);
1803
- return () => clearInterval(id);
1804
- }, [nodeReady]);
1805
- const start = react.useCallback(() => {
1806
- if (isAnimating) return;
1807
- setIsAnimating(true);
1808
- setProgress(0);
1809
- onStart?.();
1810
- if (delay > 0) {
1811
- timeoutRef.current = window.setTimeout(() => {
1812
- setIsVisible(true);
1813
- setProgress(1);
1814
- setIsAnimating(false);
1815
- onComplete?.();
1816
- }, delay);
1817
- } else {
1818
- setIsVisible(true);
1819
- setProgress(1);
1820
- setIsAnimating(false);
1821
- onComplete?.();
1822
- }
1823
- }, [delay, isAnimating, onStart, onComplete]);
1824
- startRef.current = start;
1825
- const stop = react.useCallback(() => {
1826
- if (timeoutRef.current) {
1827
- clearTimeout(timeoutRef.current);
1828
- timeoutRef.current = null;
1829
- }
1830
- if (motionRef.current) {
1831
- cancelAnimationFrame(motionRef.current);
1832
- motionRef.current = null;
1833
- }
1834
- setIsAnimating(false);
1835
- onStop?.();
1836
- }, [onStop]);
1837
- const reset = react.useCallback(() => {
1838
- stop();
1839
- setIsVisible(false);
1840
- setProgress(0);
1841
- onReset?.();
1842
- }, [stop, onReset]);
1843
- react.useEffect(() => {
1844
- if (!ref.current || !autoStart) return;
1845
- observerRef.current = new IntersectionObserver(
1846
- (entries) => {
1847
- entries.forEach((entry) => {
1848
- if (entry.isIntersecting) {
1849
- startRef.current();
1850
- if (triggerOnce) {
1851
- observerRef.current?.disconnect();
1852
- }
1853
- }
1854
- });
1855
- },
1856
- { threshold }
1857
- );
1858
- observerRef.current.observe(ref.current);
1859
- return () => {
1860
- if (observerRef.current) {
1861
- observerRef.current.disconnect();
1862
- }
1863
- };
1864
- }, [autoStart, threshold, triggerOnce, nodeReady]);
1865
- react.useEffect(() => {
1866
- if (!autoStart) {
1867
- start();
1868
- }
1869
- }, [autoStart, start]);
1870
- react.useEffect(() => {
1871
- return () => {
1872
- stop();
1873
- };
1874
- }, [stop]);
1875
- const style = react.useMemo(() => ({
1876
- opacity: isVisible ? targetOpacity : initialOpacity,
1877
- transition: `opacity ${duration}ms ${easing2}`,
1878
- "--motion-delay": `${delay}ms`,
1879
- "--motion-duration": `${duration}ms`,
1880
- "--motion-easing": easing2,
1881
- "--motion-progress": `${progress}`
1882
- }), [isVisible, targetOpacity, initialOpacity, duration, easing2, delay, progress]);
1883
- return {
1884
- ref,
1885
- isVisible,
1886
- isAnimating,
1887
- style,
1888
- progress,
1889
- start,
1890
- stop,
1891
- reset
1892
- };
1893
- }
1894
- function useSlideUp(options = {}) {
1895
- const {
1896
- delay = 0,
1897
- duration = 700,
1898
- threshold = 0.1,
1899
- triggerOnce = true,
1900
- easing: easing2 = "ease-out",
1901
- autoStart = true,
1902
- direction = "up",
1903
- distance = 50,
1904
- onComplete,
1905
- onStart,
1906
- onStop,
1907
- onReset
1908
- } = options;
1909
- const ref = react.useRef(null);
1910
- const [isVisible, setIsVisible] = react.useState(false);
1911
- const [isAnimating, setIsAnimating] = react.useState(false);
1912
- const [progress, setProgress] = react.useState(0);
1913
- const [nodeReady, setNodeReady] = react.useState(false);
1914
- const observerRef = react.useRef(null);
1915
- const timeoutRef = react.useRef(null);
1916
- const startRef = react.useRef(() => {
1917
- });
1918
- react.useEffect(() => {
1919
- if (nodeReady) return;
1920
- if (ref.current) {
1921
- setNodeReady(true);
1922
- return;
1923
- }
1924
- const id = setInterval(() => {
1925
- if (ref.current) {
1926
- setNodeReady(true);
1927
- clearInterval(id);
1928
- }
1929
- }, 50);
1930
- return () => clearInterval(id);
1931
- }, [nodeReady]);
1932
- const getInitialTransform = react.useCallback(() => {
1933
- switch (direction) {
1934
- case "up":
1935
- return `translateY(${distance}px)`;
1936
- case "down":
1937
- return `translateY(-${distance}px)`;
1938
- case "left":
1939
- return `translateX(${distance}px)`;
1940
- case "right":
1941
- return `translateX(-${distance}px)`;
1942
- default:
1943
- return `translateY(${distance}px)`;
1944
- }
1945
- }, [direction, distance]);
1946
- const start = react.useCallback(() => {
1947
- if (isAnimating) return;
1948
- setIsAnimating(true);
1949
- setProgress(0);
1950
- onStart?.();
1951
- if (delay > 0) {
1952
- timeoutRef.current = window.setTimeout(() => {
1953
- setIsVisible(true);
1954
- setProgress(1);
1955
- setIsAnimating(false);
1956
- onComplete?.();
1957
- }, delay);
1958
- } else {
1959
- setIsVisible(true);
1960
- setProgress(1);
1961
- setIsAnimating(false);
1962
- onComplete?.();
1963
- }
1964
- }, [delay, isAnimating, onStart, onComplete]);
1965
- startRef.current = start;
1966
- const stop = react.useCallback(() => {
1967
- if (timeoutRef.current) {
1968
- clearTimeout(timeoutRef.current);
1969
- timeoutRef.current = null;
1970
- }
1971
- setIsAnimating(false);
1972
- onStop?.();
1973
- }, [onStop]);
1974
- const reset = react.useCallback(() => {
1975
- stop();
1976
- setIsVisible(false);
1977
- setProgress(0);
1978
- onReset?.();
1979
- }, [stop, onReset]);
1980
- react.useEffect(() => {
1981
- if (!ref.current || !autoStart) return;
1982
- observerRef.current = new IntersectionObserver(
1983
- (entries) => {
1984
- entries.forEach((entry) => {
1985
- if (entry.isIntersecting) {
1986
- startRef.current();
1987
- if (triggerOnce) {
1988
- observerRef.current?.disconnect();
1989
- }
1990
- }
1991
- });
1992
- },
1993
- { threshold }
1994
- );
1995
- observerRef.current.observe(ref.current);
1996
- return () => {
1997
- if (observerRef.current) {
1998
- observerRef.current.disconnect();
1999
- }
2000
- };
2001
- }, [autoStart, threshold, triggerOnce, nodeReady]);
2002
- react.useEffect(() => {
2003
- if (!autoStart) {
2004
- start();
2005
- }
2006
- }, [autoStart, start]);
2007
- react.useEffect(() => {
2008
- return () => {
2009
- stop();
2010
- };
2011
- }, [stop]);
2012
- const initialTransform = react.useMemo(() => getInitialTransform(), [getInitialTransform]);
2013
- const finalTransform = react.useMemo(() => {
2014
- return direction === "left" || direction === "right" ? "translateX(0)" : "translateY(0)";
2015
- }, [direction]);
2016
- const style = react.useMemo(() => ({
2017
- opacity: isVisible ? 1 : 0,
2018
- transform: isVisible ? finalTransform : initialTransform,
2019
- transition: `opacity ${duration}ms ${easing2}, transform ${duration}ms ${easing2}`,
2020
- "--motion-delay": `${delay}ms`,
2021
- "--motion-duration": `${duration}ms`,
2022
- "--motion-easing": easing2,
2023
- "--motion-progress": `${progress}`,
2024
- "--motion-direction": direction,
2025
- "--motion-distance": `${distance}px`
2026
- }), [isVisible, initialTransform, finalTransform, duration, easing2, delay, progress, direction, distance]);
2027
- return {
2028
- ref,
2029
- isVisible,
2030
- isAnimating,
2031
- style,
2032
- progress,
2033
- start,
2034
- stop,
2035
- reset
2036
- };
2037
- }
2038
-
2039
- // src/hooks/useSlideLeft.ts
2040
- function useSlideLeft(options = {}) {
2041
- return useSlideUp({ ...options, direction: "left" });
2042
- }
2043
-
2044
- // src/hooks/useSlideRight.ts
2045
- function useSlideRight(options = {}) {
2046
- return useSlideUp({ ...options, direction: "right" });
2047
- }
2048
- function useScaleIn(options = {}) {
2049
- const {
2050
- initialScale = 0,
2051
- targetScale = 1,
2052
- duration = 700,
2053
- delay = 0,
2054
- autoStart = true,
2055
- easing: easing2 = "ease-out",
2056
- threshold = 0.1,
2057
- triggerOnce = true,
2058
- onComplete,
2059
- onStart,
2060
- onStop,
2061
- onReset
2062
- } = options;
2063
- const ref = react.useRef(null);
2064
- const [scale, setScale] = react.useState(autoStart ? initialScale : targetScale);
2065
- const [opacity, setOpacity] = react.useState(autoStart ? 0 : 1);
2066
- const [isAnimating, setIsAnimating] = react.useState(false);
2067
- const [isVisible, setIsVisible] = react.useState(autoStart ? false : true);
2068
- const [progress, setProgress] = react.useState(autoStart ? 0 : 1);
2069
- const observerRef = react.useRef(null);
2070
- const timeoutRef = react.useRef(null);
2071
- const startRef = react.useRef(() => {
2072
- });
2073
- const start = react.useCallback(() => {
2074
- if (isAnimating) return;
2075
- setIsAnimating(true);
2076
- setScale(initialScale);
2077
- setOpacity(0);
2078
- setProgress(0);
2079
- onStart?.();
2080
- timeoutRef.current = window.setTimeout(() => {
2081
- setProgress(1);
2082
- setScale(targetScale);
2083
- setOpacity(1);
2084
- setIsVisible(true);
2085
- setIsAnimating(false);
2086
- onComplete?.();
2087
- }, delay);
2088
- }, [delay, initialScale, targetScale, isAnimating, onStart, onComplete]);
2089
- startRef.current = start;
2090
- const stop = react.useCallback(() => {
2091
- if (timeoutRef.current) {
2092
- clearTimeout(timeoutRef.current);
2093
- timeoutRef.current = null;
2094
- }
2095
- setIsAnimating(false);
2096
- onStop?.();
2097
- }, [onStop]);
2098
- const reset = react.useCallback(() => {
2099
- stop();
2100
- setScale(initialScale);
2101
- setOpacity(0);
2102
- setProgress(0);
2103
- setIsVisible(false);
2104
- if (ref.current) {
2105
- const element = ref.current;
2106
- element.style.transition = "none";
2107
- element.style.opacity = "0";
2108
- element.style.transform = `scale(${initialScale})`;
2109
- requestAnimationFrame(() => {
2110
- element.style.transition = "";
2111
- });
2112
- }
2113
- onReset?.();
2114
- }, [stop, initialScale, onReset]);
2115
- react.useEffect(() => {
2116
- if (!ref.current || !autoStart) return;
2117
- observerRef.current = new IntersectionObserver(
2118
- (entries) => {
2119
- entries.forEach((entry) => {
2120
- if (entry.isIntersecting) {
2121
- startRef.current();
2122
- if (triggerOnce) {
2123
- observerRef.current?.disconnect();
2124
- }
2125
- }
2126
- });
2127
- },
2128
- { threshold }
2129
- );
2130
- observerRef.current.observe(ref.current);
2131
- return () => {
2132
- if (observerRef.current) {
2133
- observerRef.current.disconnect();
2134
- }
2135
- };
2136
- }, [autoStart, threshold, triggerOnce]);
2137
- react.useEffect(() => {
2138
- return () => {
2139
- stop();
2140
- };
2141
- }, [stop]);
2142
- const style = react.useMemo(() => ({
2143
- transform: `scale(${scale})`,
2144
- opacity,
2145
- transition: `all ${duration}ms ${easing2}`,
2146
- "--motion-delay": `${delay}ms`,
2147
- "--motion-duration": `${duration}ms`,
2148
- "--motion-easing": easing2,
2149
- "--motion-progress": `${progress}`
2150
- }), [scale, opacity, duration, easing2, delay, progress]);
2151
- return {
2152
- ref,
2153
- isVisible,
2154
- isAnimating,
2155
- style,
2156
- progress,
2157
- start,
2158
- reset,
2159
- stop
2160
- };
2161
- }
2162
- function useBounceIn(options = {}) {
2163
- const {
2164
- duration = 600,
2165
- delay = 0,
2166
- autoStart = true,
2167
- intensity = 0.3,
2168
- threshold = 0.1,
2169
- triggerOnce = true,
2170
- easing: easing2 = "cubic-bezier(0.34, 1.56, 0.64, 1)",
2171
- // 바운스 이징
2172
- onComplete,
2173
- onStart,
2174
- onStop,
2175
- onReset
2176
- } = options;
2177
- const ref = react.useRef(null);
2178
- const [scale, setScale] = react.useState(autoStart ? 0 : 1);
2179
- const [opacity, setOpacity] = react.useState(autoStart ? 0 : 1);
2180
- const [isAnimating, setIsAnimating] = react.useState(false);
2181
- const [isVisible, setIsVisible] = react.useState(autoStart ? false : true);
2182
- const [progress, setProgress] = react.useState(autoStart ? 0 : 1);
2183
- const observerRef = react.useRef(null);
2184
- const timeoutRef = react.useRef(null);
2185
- const bounceTimeoutRef = react.useRef(null);
2186
- const startRef = react.useRef(() => {
2187
- });
2188
- const start = react.useCallback(() => {
2189
- if (isAnimating) return;
2190
- setIsAnimating(true);
2191
- setScale(0);
2192
- setOpacity(0);
2193
- setProgress(0);
2194
- onStart?.();
2195
- timeoutRef.current = window.setTimeout(() => {
2196
- setProgress(0.5);
2197
- setScale(1 + intensity);
2198
- setOpacity(1);
2199
- bounceTimeoutRef.current = window.setTimeout(() => {
2200
- setProgress(1);
2201
- setScale(1);
2202
- setIsVisible(true);
2203
- setIsAnimating(false);
2204
- onComplete?.();
2205
- }, duration * 0.3);
2206
- }, delay);
2207
- }, [delay, intensity, duration, isAnimating, onStart, onComplete]);
2208
- startRef.current = start;
2209
- const stop = react.useCallback(() => {
2210
- if (timeoutRef.current) {
2211
- clearTimeout(timeoutRef.current);
2212
- timeoutRef.current = null;
2213
- }
2214
- if (bounceTimeoutRef.current) {
2215
- clearTimeout(bounceTimeoutRef.current);
2216
- bounceTimeoutRef.current = null;
2217
- }
2218
- setIsAnimating(false);
2219
- onStop?.();
2220
- }, [onStop]);
2221
- const reset = react.useCallback(() => {
2222
- stop();
2223
- setScale(0);
2224
- setOpacity(0);
2225
- setProgress(0);
2226
- setIsVisible(false);
2227
- if (ref.current) {
2228
- const element = ref.current;
2229
- element.style.transition = "none";
2230
- element.style.opacity = "0";
2231
- element.style.transform = "scale(0)";
2232
- requestAnimationFrame(() => {
2233
- element.style.transition = "";
2234
- });
2235
- }
2236
- onReset?.();
2237
- }, [stop, onReset]);
2238
- react.useEffect(() => {
2239
- if (!ref.current || !autoStart) return;
2240
- observerRef.current = new IntersectionObserver(
2241
- (entries) => {
2242
- entries.forEach((entry) => {
2243
- if (entry.isIntersecting) {
2244
- startRef.current();
2245
- if (triggerOnce) {
2246
- observerRef.current?.disconnect();
2247
- }
2248
- }
2249
- });
2250
- },
2251
- { threshold }
2252
- );
2253
- observerRef.current.observe(ref.current);
2254
- return () => {
2255
- if (observerRef.current) {
2256
- observerRef.current.disconnect();
2257
- }
2258
- };
2259
- }, [autoStart, threshold, triggerOnce]);
2260
- react.useEffect(() => {
2261
- return () => {
2262
- stop();
2263
- };
2264
- }, [stop]);
2265
- const style = react.useMemo(() => ({
2266
- transform: `scale(${scale})`,
2267
- opacity,
2268
- transition: `all ${duration}ms ${easing2}`,
2269
- "--motion-delay": `${delay}ms`,
2270
- "--motion-duration": `${duration}ms`,
2271
- "--motion-easing": easing2,
2272
- "--motion-progress": `${progress}`
2273
- }), [scale, opacity, duration, easing2, delay, progress]);
2274
- return {
2275
- ref,
2276
- isVisible,
2277
- isAnimating,
2278
- style,
2279
- progress,
2280
- start,
2281
- reset,
2282
- stop
2283
- };
2284
- }
2285
-
2286
- // src/utils/easing.ts
2287
- var linear = (t) => t;
2288
- var easeIn = (t) => t * t;
2289
- var easeOut = (t) => 1 - (1 - t) * (1 - t);
2290
- var easeInOut = (t) => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
2291
- var easeInQuad = (t) => t * t;
2292
- var easeOutQuad = (t) => 1 - (1 - t) * (1 - t);
2293
- var easeInOutQuad = (t) => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
2294
- var easeInCubic = (t) => t * t * t;
2295
- var easeOutCubic = (t) => 1 - Math.pow(1 - t, 3);
2296
- var easeInOutCubic = (t) => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
2297
- var easeInQuart = (t) => t * t * t * t;
2298
- var easeOutQuart = (t) => 1 - Math.pow(1 - t, 4);
2299
- var easeInOutQuart = (t) => t < 0.5 ? 8 * t * t * t * t : 1 - Math.pow(-2 * t + 2, 4) / 2;
2300
- var easeInQuint = (t) => t * t * t * t * t;
2301
- var easeOutQuint = (t) => 1 - Math.pow(1 - t, 5);
2302
- var easeInOutQuint = (t) => t < 0.5 ? 16 * t * t * t * t * t : 1 - Math.pow(-2 * t + 2, 5) / 2;
2303
- var easeInSine = (t) => 1 - Math.cos(t * Math.PI / 2);
2304
- var easeOutSine = (t) => Math.sin(t * Math.PI / 2);
2305
- var easeInOutSine = (t) => -(Math.cos(Math.PI * t) - 1) / 2;
2306
- var easeInExpo = (t) => t === 0 ? 0 : Math.pow(2, 10 * t - 10);
2307
- var easeOutExpo = (t) => t === 1 ? 1 : 1 - Math.pow(2, -10 * t);
2308
- var easeInOutExpo = (t) => {
2309
- if (t === 0) return 0;
2310
- if (t === 1) return 1;
2311
- if (t < 0.5) return Math.pow(2, 20 * t - 10) / 2;
2312
- return (2 - Math.pow(2, -20 * t + 10)) / 2;
2313
- };
2314
- var easeInCirc = (t) => 1 - Math.sqrt(1 - Math.pow(t, 2));
2315
- var easeOutCirc = (t) => Math.sqrt(1 - Math.pow(t - 1, 2));
2316
- var easeInOutCirc = (t) => {
2317
- if (t < 0.5) return (1 - Math.sqrt(1 - Math.pow(2 * t, 2))) / 2;
2318
- return (Math.sqrt(1 - Math.pow(-2 * t + 2, 2)) + 1) / 2;
2319
- };
2320
- var easeInBounce = (t) => 1 - easeOutBounce(1 - t);
2321
- var easeOutBounce = (t) => {
2322
- const n1 = 7.5625;
2323
- const d1 = 2.75;
2324
- if (t < 1 / d1) {
2325
- return n1 * t * t;
2326
- } else if (t < 2 / d1) {
2327
- return n1 * (t -= 1.5 / d1) * t + 0.75;
2328
- } else if (t < 2.5 / d1) {
2329
- return n1 * (t -= 2.25 / d1) * t + 0.9375;
2330
- } else {
2331
- return n1 * (t -= 2.625 / d1) * t + 0.984375;
2332
- }
2333
- };
2334
- var easeInOutBounce = (t) => {
2335
- if (t < 0.5) return (1 - easeOutBounce(1 - 2 * t)) / 2;
2336
- return (1 + easeOutBounce(2 * t - 1)) / 2;
2337
- };
2338
- var easeInBack = (t) => {
2339
- const c1 = 1.70158;
2340
- const c3 = c1 + 1;
2341
- return c3 * t * t * t - c1 * t * t;
2342
- };
2343
- var easeOutBack = (t) => {
2344
- const c1 = 1.70158;
2345
- const c3 = c1 + 1;
2346
- return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
2347
- };
2348
- var easeInOutBack = (t) => {
2349
- const c1 = 1.70158;
2350
- const c2 = c1 * 1.525;
2351
- if (t < 0.5) {
2352
- return Math.pow(2 * t, 2) * ((c2 + 1) * 2 * t - c2) / 2;
2353
- } else {
2354
- return (Math.pow(2 * t - 2, 2) * ((c2 + 1) * (t * 2 - 2) + c2) + 2) / 2;
2355
- }
2356
- };
2357
- var easeInElastic = (t) => {
2358
- const c4 = 2 * Math.PI / 3;
2359
- if (t === 0) return 0;
2360
- if (t === 1) return 1;
2361
- return -Math.pow(2, 10 * t - 10) * Math.sin((t * 10 - 0.75) * c4);
2362
- };
2363
- var easeOutElastic = (t) => {
2364
- const c4 = 2 * Math.PI / 3;
2365
- if (t === 0) return 0;
2366
- if (t === 1) return 1;
2367
- return Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1;
2368
- };
2369
- var easeInOutElastic = (t) => {
2370
- const c5 = 2 * Math.PI / 4.5;
2371
- if (t === 0) return 0;
2372
- if (t === 1) return 1;
2373
- if (t < 0.5) {
2374
- return -(Math.pow(2, 20 * t - 10) * Math.sin((20 * t - 11.125) * c5)) / 2;
2375
- } else {
2376
- return Math.pow(2, -20 * t + 10) * Math.sin((20 * t - 11.125) * c5) / 2 + 1;
2377
- }
2378
- };
2379
- var pulse = (t) => Math.sin(t * Math.PI) * 0.5 + 0.5;
2380
- var pulseSmooth = (t) => Math.sin(t * Math.PI * 2) * 0.3 + 0.7;
2381
- var skeletonWave = (t) => Math.sin((t - 0.5) * Math.PI * 2) * 0.5 + 0.5;
2382
- var blink = (t) => t < 0.5 ? 1 : 0;
2383
- var easing = {
2384
- linear,
2385
- easeIn,
2386
- easeOut,
2387
- easeInOut,
2388
- easeInQuad,
2389
- easeOutQuad,
2390
- easeInOutQuad,
2391
- easeInCubic,
2392
- easeOutCubic,
2393
- easeInOutCubic,
2394
- easeInQuart,
2395
- easeOutQuart,
2396
- easeInOutQuart,
2397
- easeInQuint,
2398
- easeOutQuint,
2399
- easeInOutQuint,
2400
- easeInSine,
2401
- easeOutSine,
2402
- easeInOutSine,
2403
- easeInExpo,
2404
- easeOutExpo,
2405
- easeInOutExpo,
2406
- easeInCirc,
2407
- easeOutCirc,
2408
- easeInOutCirc,
2409
- easeInBounce,
2410
- easeOutBounce,
2411
- easeInOutBounce,
2412
- easeInBack,
2413
- easeOutBack,
2414
- easeInOutBack,
2415
- easeInElastic,
2416
- easeOutElastic,
2417
- easeInOutElastic,
2418
- pulse,
2419
- pulseSmooth,
2420
- skeletonWave,
2421
- blink
2422
- };
2423
- function isValidEasing(easingName) {
2424
- return easingName in easing;
2425
- }
2426
- function getEasing(easingName) {
2427
- if (typeof easingName === "function") {
2428
- return easingName;
2429
- }
2430
- if (typeof easingName === "string") {
2431
- if (easingName in easing) {
2432
- return easing[easingName];
2433
- }
2434
- if (easingName === "bounce") {
2435
- return easing.easeOutBounce;
2436
- }
2437
- if (process.env.NODE_ENV === "development") {
2438
- console.warn(`[HUA Motion] Unknown easing "${easingName}", using default "easeOut".`);
2439
- }
2440
- }
2441
- return easeOut;
2442
- }
2443
- function applyEasing(t, easingName) {
2444
- const easingFn = getEasing(easingName);
2445
- return easingFn(t);
2446
- }
2447
- function safeApplyEasing(t, easingName) {
2448
- try {
2449
- const easingFn = getEasing(easingName);
2450
- return easingFn(t);
2451
- } catch (err) {
2452
- if (process.env.NODE_ENV === "development") {
2453
- console.error(`[HUA Motion] Failed to apply easing "${easingName}":`, err);
2454
- }
2455
- return easeOut(t);
2456
- }
2457
- }
2458
- function getAvailableEasings() {
2459
- return Object.keys(easing);
2460
- }
2461
- function isEasingFunction(value) {
2462
- return typeof value === "function";
2463
- }
2464
- var easingPresets = {
2465
- default: "easeOut",
2466
- smooth: "easeInOutCubic",
2467
- fast: "easeOutQuad",
2468
- slow: "easeInOutSine",
2469
- bouncy: "easeOutBounce",
2470
- elastic: "easeOutElastic",
2471
- fade: "easeInOut",
2472
- scale: "easeOutBack"
2473
- };
2474
- function getPresetEasing(preset) {
2475
- const easingName = easingPresets[preset];
2476
- return getEasing(easingName);
2477
- }
2478
-
2479
- // src/hooks/usePulse.ts
2480
- function usePulse(options = {}) {
2481
- const {
2482
- duration = 3e3,
2483
- intensity = 1,
2484
- repeatCount = Infinity,
2485
- repeatDelay = 0,
2486
- autoStart = false
2487
- } = options;
2488
- const ref = react.useRef(null);
2489
- const [isAnimating, setIsAnimating] = react.useState(false);
2490
- const [isVisible, setIsVisible] = react.useState(true);
2491
- const [progress, setProgress] = react.useState(0);
2492
- const motionRef = react.useRef(null);
2493
- const easingFn = react.useMemo(() => getEasing("easeInOut"), []);
2494
- const start = react.useCallback(() => {
2495
- if (!ref.current) return;
2496
- const element = ref.current;
2497
- let currentRepeat = 0;
2498
- setIsAnimating(true);
2499
- const animate = (startTime) => {
2500
- const updateMotion = (currentTime) => {
2501
- const elapsed = currentTime - startTime;
2502
- const rawProgress = Math.min(elapsed / duration, 1);
2503
- const easedProgress = easingFn(rawProgress);
2504
- const finalProgress = currentRepeat % 2 === 1 ? 1 - easedProgress : easedProgress;
2505
- const opacity = 0.3 + 0.7 * finalProgress * intensity;
2506
- element.style.opacity = opacity.toString();
2507
- setProgress(rawProgress);
2508
- if (rawProgress < 1) {
2509
- motionRef.current = requestAnimationFrame(updateMotion);
2510
- } else {
2511
- currentRepeat++;
2512
- if (repeatCount === Infinity || currentRepeat < repeatCount * 2) {
2513
- if (repeatDelay > 0) {
2514
- const delayTimeout = window.setTimeout(() => {
2515
- motionRef.current = requestAnimationFrame(() => animate(performance.now()));
2516
- }, repeatDelay);
2517
- motionRef.current = delayTimeout;
2518
- } else {
2519
- motionRef.current = requestAnimationFrame(() => animate(performance.now()));
2520
- }
2521
- } else {
2522
- setIsAnimating(false);
2523
- }
2524
- }
2525
- };
2526
- motionRef.current = requestAnimationFrame(updateMotion);
2527
- };
2528
- animate(performance.now());
2529
- }, [duration, intensity, repeatCount, repeatDelay, easingFn]);
2530
- const stop = react.useCallback(() => {
2531
- if (motionRef.current) {
2532
- cancelAnimationFrame(motionRef.current);
2533
- clearTimeout(motionRef.current);
2534
- motionRef.current = null;
2535
- }
2536
- setIsAnimating(false);
2537
- }, []);
2538
- const reset = react.useCallback(() => {
2539
- if (motionRef.current) {
2540
- cancelAnimationFrame(motionRef.current);
2541
- clearTimeout(motionRef.current);
2542
- motionRef.current = null;
2543
- }
2544
- setIsAnimating(false);
2545
- setProgress(0);
2546
- if (ref.current) {
2547
- const element = ref.current;
2548
- element.style.transition = "none";
2549
- element.style.opacity = "1";
2550
- requestAnimationFrame(() => {
2551
- element.style.transition = "";
2552
- });
2553
- }
2554
- }, []);
2555
- react.useEffect(() => {
2556
- if (autoStart) {
2557
- start();
2558
- }
2559
- }, [autoStart, start]);
2560
- react.useEffect(() => {
2561
- return () => {
2562
- if (motionRef.current) {
2563
- cancelAnimationFrame(motionRef.current);
2564
- clearTimeout(motionRef.current);
2565
- }
2566
- };
2567
- }, []);
2568
- const style = react.useMemo(() => ({
2569
- opacity: isAnimating ? 0.3 + 0.7 * progress * intensity : 1,
2570
- transition: isAnimating ? "none" : "opacity 0.3s ease-in-out"
2571
- }), [isAnimating, progress, intensity]);
2572
- return {
2573
- ref,
2574
- isVisible,
2575
- isAnimating,
2576
- style,
2577
- progress,
2578
- start,
2579
- stop,
2580
- reset
2581
- };
2582
- }
2583
- function useSpringMotion(options) {
2584
- const {
2585
- from,
2586
- to,
2587
- mass = 1,
2588
- stiffness = 100,
2589
- damping = 10,
2590
- restDelta = 0.01,
2591
- restSpeed = 0.01,
2592
- onComplete,
2593
- enabled = true,
2594
- autoStart = false
2595
- } = options;
2596
- const ref = react.useRef(null);
2597
- const [springState, setSpringState] = react.useState({
2598
- value: from,
2599
- velocity: 0,
2600
- isAnimating: false
2601
- });
2602
- const [isVisible, setIsVisible] = react.useState(true);
2603
- const [progress, setProgress] = react.useState(0);
2604
- const motionRef = react.useRef(null);
2605
- const lastTimeRef = react.useRef(0);
2606
- const calculateSpring = react.useCallback((currentValue, currentVelocity, targetValue, deltaTime) => {
2607
- const displacement = currentValue - targetValue;
2608
- const springForce = -stiffness * displacement;
2609
- const dampingForce = -damping * currentVelocity;
2610
- const totalForce = springForce + dampingForce;
2611
- const acceleration = totalForce / mass;
2612
- const newVelocity = currentVelocity + acceleration * deltaTime;
2613
- const newValue = currentValue + newVelocity * deltaTime;
2614
- return { value: newValue, velocity: newVelocity };
2615
- }, [mass, stiffness, damping]);
2616
- const animate = react.useCallback((currentTime) => {
2617
- if (!enabled || !springState.isAnimating) return;
2618
- const deltaTime = Math.min(currentTime - lastTimeRef.current, 16) / 1e3;
2619
- lastTimeRef.current = currentTime;
2620
- const { value, velocity } = calculateSpring(
2621
- springState.value,
2622
- springState.velocity,
2623
- to,
2624
- deltaTime
2625
- );
2626
- const range = Math.abs(to - from);
2627
- const currentProgress = range > 0 ? Math.min(Math.abs(value - from) / range, 1) : 1;
2628
- setProgress(currentProgress);
2629
- const isAtRest = Math.abs(value - to) < restDelta && Math.abs(velocity) < restSpeed;
2630
- if (isAtRest) {
2631
- setSpringState({
2632
- value: to,
2633
- velocity: 0,
2634
- isAnimating: false
2635
- });
2636
- setProgress(1);
2637
- onComplete?.();
2638
- return;
2639
- }
2640
- setSpringState({
2641
- value,
2642
- velocity,
2643
- isAnimating: true
2644
- });
2645
- motionRef.current = requestAnimationFrame(animate);
2646
- }, [enabled, springState.isAnimating, to, from, restDelta, restSpeed, onComplete, calculateSpring]);
2647
- const start = react.useCallback(() => {
2648
- if (springState.isAnimating) return;
2649
- setSpringState((prev) => ({
2650
- ...prev,
2651
- isAnimating: true
2652
- }));
2653
- lastTimeRef.current = performance.now();
2654
- motionRef.current = requestAnimationFrame(animate);
2655
- }, [springState.isAnimating, animate]);
2656
- const stop = react.useCallback(() => {
2657
- if (motionRef.current) {
2658
- cancelAnimationFrame(motionRef.current);
2659
- motionRef.current = null;
2660
- }
2661
- setSpringState((prev) => ({
2662
- ...prev,
2663
- isAnimating: false
2664
- }));
2665
- }, []);
2666
- const reset = react.useCallback(() => {
2667
- stop();
2668
- setSpringState({
2669
- value: from,
2670
- velocity: 0,
2671
- isAnimating: false
2672
- });
2673
- setProgress(0);
2674
- motionRef.current = null;
2675
- }, [from, stop]);
2676
- react.useEffect(() => {
2677
- if (autoStart) {
2678
- start();
2679
- }
2680
- }, [autoStart, start]);
2681
- react.useEffect(() => {
2682
- return () => {
2683
- if (motionRef.current) {
2684
- cancelAnimationFrame(motionRef.current);
2685
- }
2686
- };
2687
- }, []);
2688
- const style = react.useMemo(() => ({
2689
- "--motion-progress": `${progress}`,
2690
- "--motion-value": `${springState.value}`
2691
- }), [progress, springState.value]);
2692
- return {
2693
- ref,
2694
- isVisible,
2695
- isAnimating: springState.isAnimating,
2696
- style,
2697
- progress,
2698
- value: springState.value,
2699
- velocity: springState.velocity,
2700
- start,
2701
- stop,
2702
- reset
2703
- };
2704
- }
2705
- var defaultColors = ["#60a5fa", "#34d399", "#fbbf24", "#f87171"];
2706
- var keyframesInjected = false;
2707
- function ensureGradientKeyframes() {
2708
- if (typeof document === "undefined" || keyframesInjected) return;
2709
- const name = "gradientShift";
2710
- if (!document.head.querySelector(`style[data-gradient="${name}"]`)) {
2711
- const style = document.createElement("style");
2712
- style.setAttribute("data-gradient", name);
2713
- style.textContent = `
2714
- @keyframes ${name} {
2715
- 0%, 100% { background-position: 0% 50%; }
2716
- 50% { background-position: 100% 50%; }
2717
- }
2718
- `;
2719
- document.head.appendChild(style);
2720
- }
2721
- keyframesInjected = true;
2722
- }
2723
- function useGradient(options = {}) {
2724
- const {
2725
- colors = defaultColors,
2726
- duration = 6e3,
2727
- direction = "diagonal",
2728
- size = 300,
2729
- easing: easing2 = "ease-in-out",
2730
- autoStart = false
2731
- } = options;
2732
- const ref = react.useRef(null);
2733
- const [isAnimating, setIsAnimating] = react.useState(autoStart);
2734
- const [isVisible, setIsVisible] = react.useState(true);
2735
- const [motionProgress, setMotionProgress] = react.useState(0);
2736
- react.useEffect(() => {
2737
- ensureGradientKeyframes();
2738
- }, []);
2739
- const style = react.useMemo(() => {
2740
- const gradientDirection = direction === "horizontal" ? "90deg" : direction === "vertical" ? "180deg" : "135deg";
2741
- const background = `linear-gradient(${gradientDirection}, ${colors.join(", ")})`;
2742
- const backgroundSize = `${size}% ${size}%`;
2743
- return {
2744
- background,
2745
- backgroundSize,
2746
- animation: isAnimating ? `gradientShift ${duration}ms ${easing2} infinite` : "none",
2747
- backgroundPosition: isAnimating ? void 0 : `${motionProgress}% 50%`
2748
- };
2749
- }, [colors, direction, size, duration, easing2, isAnimating, motionProgress]);
2750
- const start = react.useCallback(() => {
2751
- setIsAnimating(true);
2752
- }, []);
2753
- const pause = react.useCallback(() => {
2754
- setIsAnimating(false);
2755
- }, []);
2756
- const resume = react.useCallback(() => {
2757
- setIsAnimating(true);
2758
- }, []);
2759
- const reset = react.useCallback(() => {
2760
- setIsAnimating(false);
2761
- setMotionProgress(0);
2762
- }, []);
2763
- const stop = react.useCallback(() => {
2764
- setIsAnimating(false);
2765
- }, []);
2766
- react.useEffect(() => {
2767
- if (!isAnimating) {
2768
- const interval = setInterval(() => {
2769
- setMotionProgress((prev) => {
2770
- const newProgress = prev + 100 / (duration / 16);
2771
- return newProgress >= 100 ? 0 : newProgress;
2772
- });
2773
- }, 16);
2774
- return () => clearInterval(interval);
2775
- }
2776
- }, [isAnimating, duration]);
2777
- react.useEffect(() => {
2778
- setIsAnimating(autoStart);
2779
- }, [autoStart]);
2780
- return {
2781
- ref,
2782
- isVisible,
2783
- isAnimating,
2784
- style,
2785
- progress: motionProgress / 100,
2786
- start,
2787
- pause,
2788
- resume,
2789
- reset,
2790
- stop
2791
- };
2792
- }
2793
- function useHoverMotion(options = {}) {
2794
- const {
2795
- duration = 200,
2796
- easing: easing2 = "ease-out",
2797
- hoverScale = 1.05,
2798
- hoverY = -2,
2799
- hoverOpacity = 1
2800
- } = options;
2801
- const ref = react.useRef(null);
2802
- const [isHovered, setIsHovered] = react.useState(false);
2803
- const [isAnimating, setIsAnimating] = react.useState(false);
2804
- react.useEffect(() => {
2805
- const element = ref.current;
2806
- if (!element) return;
2807
- const handleMouseEnter = () => {
2808
- setIsHovered(true);
2809
- setIsAnimating(true);
2810
- };
2811
- const handleMouseLeave = () => {
2812
- setIsHovered(false);
2813
- setIsAnimating(true);
2814
- };
2815
- const handleTransitionEnd = () => {
2816
- setIsAnimating(false);
2817
- };
2818
- element.addEventListener("mouseenter", handleMouseEnter);
2819
- element.addEventListener("mouseleave", handleMouseLeave);
2820
- element.addEventListener("transitionend", handleTransitionEnd);
2821
- return () => {
2822
- element.removeEventListener("mouseenter", handleMouseEnter);
2823
- element.removeEventListener("mouseleave", handleMouseLeave);
2824
- element.removeEventListener("transitionend", handleTransitionEnd);
2825
- };
2826
- }, []);
2827
- const style = react.useMemo(() => ({
2828
- transform: isHovered ? `scale(${hoverScale}) translateY(${hoverY}px)` : "scale(1) translateY(0px)",
2829
- opacity: isHovered ? hoverOpacity : 1,
2830
- transition: `transform ${duration}ms ${easing2}, opacity ${duration}ms ${easing2}`
2831
- }), [isHovered, hoverScale, hoverY, hoverOpacity, duration, easing2]);
2832
- const start = react.useCallback(() => {
2833
- setIsHovered(true);
2834
- setIsAnimating(true);
2835
- }, []);
2836
- const stop = react.useCallback(() => {
2837
- setIsAnimating(false);
2838
- }, []);
2839
- const reset = react.useCallback(() => {
2840
- setIsHovered(false);
2841
- setIsAnimating(false);
2842
- }, []);
2843
- return {
2844
- ref,
2845
- isVisible: true,
2846
- isAnimating,
2847
- isHovered,
2848
- style,
2849
- progress: isHovered ? 1 : 0,
2850
- start,
2851
- stop,
2852
- reset
2853
- };
2854
- }
2855
- function useClickToggle(options = {}) {
2856
- const {
2857
- initialState = false,
2858
- toggleOnClick = true,
2859
- toggleOnDoubleClick = false,
2860
- toggleOnRightClick = false,
2861
- toggleOnEnter = true,
2862
- toggleOnSpace = true,
2863
- autoReset = false,
2864
- resetDelay = 3e3,
2865
- preventDefault = false,
2866
- stopPropagation = false,
2867
- showOnMount = false
2868
- } = options;
2869
- const [isActive, setIsActive] = react.useState(showOnMount ? initialState : false);
2870
- const [mounted, setMounted] = react.useState(false);
2871
- const resetTimeoutRef = react.useRef(null);
2872
- react.useEffect(() => {
2873
- setMounted(true);
2874
- }, []);
2875
- const startResetTimer = react.useCallback(() => {
2876
- if (!autoReset || resetDelay <= 0) return;
2877
- if (resetTimeoutRef.current !== null) {
2878
- clearTimeout(resetTimeoutRef.current);
2879
- }
2880
- resetTimeoutRef.current = window.setTimeout(() => {
2881
- setIsActive(false);
2882
- resetTimeoutRef.current = null;
2883
- }, resetDelay);
2884
- }, [autoReset, resetDelay]);
2885
- const toggle = react.useCallback(() => {
2886
- if (!mounted) return;
2887
- setIsActive((prev) => {
2888
- const newState = !prev;
2889
- if (newState && autoReset) {
2890
- startResetTimer();
2891
- } else if (!newState && resetTimeoutRef.current !== null) {
2892
- clearTimeout(resetTimeoutRef.current);
2893
- resetTimeoutRef.current = null;
2894
- }
2895
- return newState;
2896
- });
2897
- }, [mounted, autoReset, startResetTimer]);
2898
- const activate = react.useCallback(() => {
2899
- if (!mounted || isActive) return;
2900
- setIsActive(true);
2901
- if (autoReset) {
2902
- startResetTimer();
2903
- }
2904
- }, [mounted, isActive, autoReset, startResetTimer]);
2905
- const deactivate = react.useCallback(() => {
2906
- if (!mounted || !isActive) return;
2907
- setIsActive(false);
2908
- if (resetTimeoutRef.current !== null) {
2909
- clearTimeout(resetTimeoutRef.current);
2910
- resetTimeoutRef.current = null;
2911
- }
2912
- }, [mounted, isActive]);
2913
- const reset = react.useCallback(() => {
2914
- setIsActive(initialState);
2915
- if (resetTimeoutRef.current !== null) {
2916
- clearTimeout(resetTimeoutRef.current);
2917
- resetTimeoutRef.current = null;
2918
- }
2919
- }, [initialState]);
2920
- const handleClick = react.useCallback((event) => {
2921
- if (!toggleOnClick) return;
2922
- if (preventDefault) event.preventDefault();
2923
- if (stopPropagation) event.stopPropagation();
2924
- toggle();
2925
- }, [toggleOnClick, preventDefault, stopPropagation, toggle]);
2926
- const handleDoubleClick = react.useCallback((event) => {
2927
- if (!toggleOnDoubleClick) return;
2928
- if (preventDefault) event.preventDefault();
2929
- if (stopPropagation) event.stopPropagation();
2930
- toggle();
2931
- }, [toggleOnDoubleClick, preventDefault, stopPropagation, toggle]);
2932
- const handleContextMenu = react.useCallback((event) => {
2933
- if (!toggleOnRightClick) return;
2934
- if (preventDefault) event.preventDefault();
2935
- if (stopPropagation) event.stopPropagation();
2936
- toggle();
2937
- }, [toggleOnRightClick, preventDefault, stopPropagation, toggle]);
2938
- const handleKeyDown = react.useCallback((event) => {
2939
- const shouldToggle = event.key === "Enter" && toggleOnEnter || event.key === " " && toggleOnSpace;
2940
- if (!shouldToggle) return;
2941
- if (preventDefault) event.preventDefault();
2942
- if (stopPropagation) event.stopPropagation();
2943
- toggle();
2944
- }, [toggleOnEnter, toggleOnSpace, preventDefault, stopPropagation, toggle]);
2945
- react.useEffect(() => {
2946
- return () => {
2947
- if (resetTimeoutRef.current !== null) {
2948
- clearTimeout(resetTimeoutRef.current);
2949
- }
2950
- };
2951
- }, []);
2952
- const clickHandlers = {
2953
- ...toggleOnClick && { onClick: handleClick },
2954
- ...toggleOnDoubleClick && { onDoubleClick: handleDoubleClick },
2955
- ...toggleOnRightClick && { onContextMenu: handleContextMenu },
2956
- ...(toggleOnEnter || toggleOnSpace) && { onKeyDown: handleKeyDown }
2957
- };
2958
- return {
2959
- isActive,
2960
- mounted,
2961
- toggle,
2962
- activate,
2963
- deactivate,
2964
- reset,
2965
- clickHandlers
2966
- };
2967
- }
2968
- function useFocusToggle(options = {}) {
2969
- const {
2970
- initialState = false,
2971
- toggleOnFocus = true,
2972
- toggleOnBlur = false,
2973
- toggleOnFocusIn = false,
2974
- toggleOnFocusOut = false,
2975
- autoReset = false,
2976
- resetDelay = 3e3,
2977
- preventDefault = false,
2978
- stopPropagation = false,
2979
- showOnMount = false
2980
- } = options;
2981
- const [isActive, setIsActive] = react.useState(showOnMount ? initialState : false);
2982
- const [mounted, setMounted] = react.useState(false);
2983
- const resetTimeoutRef = react.useRef(null);
2984
- const elementRef = react.useRef(null);
2985
- react.useEffect(() => {
2986
- setMounted(true);
2987
- }, []);
2988
- const startResetTimer = react.useCallback(() => {
2989
- if (!autoReset || resetDelay <= 0) return;
2990
- if (resetTimeoutRef.current !== null) {
2991
- clearTimeout(resetTimeoutRef.current);
2992
- }
2993
- resetTimeoutRef.current = window.setTimeout(() => {
2994
- setIsActive(false);
2995
- resetTimeoutRef.current = null;
2996
- }, resetDelay);
2997
- }, [autoReset, resetDelay]);
2998
- const toggle = react.useCallback(() => {
2999
- if (!mounted) return;
3000
- setIsActive((prev) => {
3001
- const newState = !prev;
3002
- if (newState && autoReset) {
3003
- startResetTimer();
3004
- } else if (!newState && resetTimeoutRef.current !== null) {
3005
- clearTimeout(resetTimeoutRef.current);
3006
- resetTimeoutRef.current = null;
3007
- }
3008
- return newState;
3009
- });
3010
- }, [mounted, autoReset, startResetTimer]);
3011
- const activate = react.useCallback(() => {
3012
- if (!mounted || isActive) return;
3013
- setIsActive(true);
3014
- if (autoReset) {
3015
- startResetTimer();
3016
- }
3017
- }, [mounted, isActive, autoReset, startResetTimer]);
3018
- const deactivate = react.useCallback(() => {
3019
- if (!mounted || !isActive) return;
3020
- setIsActive(false);
3021
- if (resetTimeoutRef.current !== null) {
3022
- clearTimeout(resetTimeoutRef.current);
3023
- resetTimeoutRef.current = null;
3024
- }
3025
- }, [mounted, isActive]);
3026
- const reset = react.useCallback(() => {
3027
- setIsActive(initialState);
3028
- if (resetTimeoutRef.current !== null) {
3029
- clearTimeout(resetTimeoutRef.current);
3030
- resetTimeoutRef.current = null;
3031
- }
3032
- }, [initialState]);
3033
- const handleFocus = react.useCallback((event) => {
3034
- if (!toggleOnFocus) return;
3035
- if (preventDefault) event.preventDefault();
3036
- if (stopPropagation) event.stopPropagation();
3037
- activate();
3038
- }, [toggleOnFocus, preventDefault, stopPropagation, activate]);
3039
- const handleBlur = react.useCallback((event) => {
3040
- if (!toggleOnBlur) return;
3041
- if (preventDefault) event.preventDefault();
3042
- if (stopPropagation) event.stopPropagation();
3043
- deactivate();
3044
- }, [toggleOnBlur, preventDefault, stopPropagation, deactivate]);
3045
- const handleFocusIn = react.useCallback((event) => {
3046
- if (!toggleOnFocusIn) return;
3047
- if (preventDefault) event.preventDefault();
3048
- if (stopPropagation) event.stopPropagation();
3049
- activate();
3050
- }, [toggleOnFocusIn, preventDefault, stopPropagation, activate]);
3051
- const handleFocusOut = react.useCallback((event) => {
3052
- if (!toggleOnFocusOut) return;
3053
- if (preventDefault) event.preventDefault();
3054
- if (stopPropagation) event.stopPropagation();
3055
- deactivate();
3056
- }, [toggleOnFocusOut, preventDefault, stopPropagation, deactivate]);
3057
- react.useEffect(() => {
3058
- return () => {
3059
- if (resetTimeoutRef.current !== null) {
3060
- clearTimeout(resetTimeoutRef.current);
3061
- }
3062
- };
3063
- }, []);
3064
- const focusHandlers = {
3065
- ...toggleOnFocus && { onFocus: handleFocus },
3066
- ...toggleOnBlur && { onBlur: handleBlur },
3067
- ...toggleOnFocusIn && { onFocusIn: handleFocusIn },
3068
- ...toggleOnFocusOut && { onFocusOut: handleFocusOut }
3069
- };
3070
- return {
3071
- isActive,
3072
- mounted,
3073
- toggle,
3074
- activate,
3075
- deactivate,
3076
- reset,
3077
- focusHandlers,
3078
- ref: elementRef
3079
- };
3080
- }
3081
- function useScrollReveal(options = {}) {
3082
- const {
3083
- threshold = 0.1,
3084
- rootMargin = "0px",
3085
- triggerOnce = true,
3086
- delay = 0,
3087
- duration = 700,
3088
- easing: easing2 = "ease-out",
3089
- motionType = "fadeIn",
3090
- onComplete,
3091
- onStart,
3092
- onStop,
3093
- onReset
3094
- } = options;
3095
- const ref = react.useRef(null);
3096
- const [isVisible, setIsVisible] = react.useState(false);
3097
- const [isAnimating, setIsAnimating] = react.useState(false);
3098
- const [hasTriggered, setHasTriggered] = react.useState(false);
3099
- const [progress, setProgress] = react.useState(0);
3100
- const observerCallback = react.useCallback((entries) => {
3101
- entries.forEach((entry) => {
3102
- if (entry.isIntersecting && (!triggerOnce || !hasTriggered)) {
3103
- setIsAnimating(true);
3104
- onStart?.();
3105
- setTimeout(() => {
3106
- setIsVisible(true);
3107
- setHasTriggered(true);
3108
- setProgress(1);
3109
- setIsAnimating(false);
3110
- onComplete?.();
3111
- }, delay);
3112
- }
3113
- });
3114
- }, [triggerOnce, hasTriggered, delay, onStart, onComplete]);
3115
- react.useEffect(() => {
3116
- if (!ref.current) return;
3117
- const observer = new IntersectionObserver(observerCallback, {
3118
- threshold,
3119
- rootMargin
3120
- });
3121
- observer.observe(ref.current);
3122
- return () => {
3123
- observer.disconnect();
3124
- };
3125
- }, [observerCallback, threshold, rootMargin]);
3126
- const style = react.useMemo(() => {
3127
- const baseTransition = `all ${duration}ms ${easing2}`;
3128
- if (!isVisible) {
3129
- switch (motionType) {
3130
- case "fadeIn":
3131
- return {
3132
- opacity: 0,
3133
- transition: baseTransition
3134
- };
3135
- case "slideUp":
3136
- return {
3137
- opacity: 0,
3138
- transform: "translateY(32px)",
3139
- transition: baseTransition
3140
- };
3141
- case "slideLeft":
3142
- return {
3143
- opacity: 0,
3144
- transform: "translateX(-32px)",
3145
- transition: baseTransition
3146
- };
3147
- case "slideRight":
3148
- return {
3149
- opacity: 0,
3150
- transform: "translateX(32px)",
3151
- transition: baseTransition
3152
- };
3153
- case "scaleIn":
3154
- return {
3155
- opacity: 0,
3156
- transform: "scale(0.95)",
3157
- transition: baseTransition
3158
- };
3159
- case "bounceIn":
3160
- return {
3161
- opacity: 0,
3162
- transform: "scale(0.75)",
3163
- transition: baseTransition
3164
- };
3165
- default:
3166
- return {
3167
- opacity: 0,
3168
- transition: baseTransition
3169
- };
3170
- }
3171
- }
3172
- return {
3173
- opacity: 1,
3174
- transform: "none",
3175
- transition: baseTransition
3176
- };
3177
- }, [isVisible, motionType, duration, easing2]);
3178
- const start = react.useCallback(() => {
3179
- setIsAnimating(true);
3180
- onStart?.();
3181
- setTimeout(() => {
3182
- setIsVisible(true);
3183
- setProgress(1);
3184
- setIsAnimating(false);
3185
- onComplete?.();
3186
- }, delay);
3187
- }, [delay, onStart, onComplete]);
3188
- const reset = react.useCallback(() => {
3189
- setIsVisible(false);
3190
- setIsAnimating(false);
3191
- setProgress(0);
3192
- setHasTriggered(false);
3193
- onReset?.();
3194
- }, [onReset]);
3195
- const stop = react.useCallback(() => {
3196
- setIsAnimating(false);
3197
- onStop?.();
3198
- }, [onStop]);
3199
- return {
3200
- ref,
3201
- isVisible,
3202
- isAnimating,
3203
- progress,
3204
- style,
3205
- start,
3206
- reset,
3207
- stop
3208
- };
3209
- }
3210
- function useScrollProgress(options = {}) {
3211
- const {
3212
- target,
3213
- offset = 0,
3214
- showOnMount = false
3215
- } = options;
3216
- const [progress, setProgress] = react.useState(showOnMount ? 0 : 0);
3217
- const [mounted, setMounted] = react.useState(false);
3218
- react.useEffect(() => {
3219
- setMounted(true);
3220
- }, []);
3221
- react.useEffect(() => {
3222
- if (!mounted) return;
3223
- const calculateProgress = () => {
3224
- if (typeof window !== "undefined") {
3225
- const scrollTop = window.pageYOffset;
3226
- const scrollHeight = target || document.documentElement.scrollHeight - window.innerHeight;
3227
- const adjustedScrollTop = Math.max(0, scrollTop - offset);
3228
- const progressPercent = Math.min(100, Math.max(0, adjustedScrollTop / scrollHeight * 100));
3229
- setProgress(progressPercent);
3230
- }
3231
- };
3232
- calculateProgress();
3233
- window.addEventListener("scroll", calculateProgress, { passive: true });
3234
- window.addEventListener("resize", calculateProgress, { passive: true });
3235
- return () => {
3236
- window.removeEventListener("scroll", calculateProgress);
3237
- window.removeEventListener("resize", calculateProgress);
3238
- };
3239
- }, [target, offset, mounted]);
3240
- return {
3241
- progress,
3242
- mounted
3243
- };
3244
- }
3245
- function useMotionState(options = {}) {
3246
- const {
3247
- initialState = "idle",
3248
- autoStart = false,
3249
- loop = false,
3250
- direction = "forward",
3251
- duration = 1e3,
3252
- delay = 0,
3253
- showOnMount = false
3254
- } = options;
3255
- const [state, setState] = react.useState(showOnMount ? initialState : "idle");
3256
- const [currentDirection, setCurrentDirection] = react.useState(direction);
3257
- const [progress, setProgress] = react.useState(0);
3258
- const [elapsed, setElapsed] = react.useState(0);
3259
- const [mounted, setMounted] = react.useState(false);
3260
- const motionRef = react.useRef(null);
3261
- const startTimeRef = react.useRef(null);
3262
- const pauseTimeRef = react.useRef(null);
3263
- const totalPausedTimeRef = react.useRef(0);
3264
- react.useEffect(() => {
3265
- setMounted(true);
3266
- }, []);
3267
- const animate = react.useCallback((timestamp) => {
3268
- if (!startTimeRef.current) {
3269
- startTimeRef.current = timestamp;
3270
- }
3271
- const adjustedTimestamp = timestamp - totalPausedTimeRef.current;
3272
- const elapsedTime = adjustedTimestamp - startTimeRef.current;
3273
- const newElapsed = Math.max(0, elapsedTime - delay);
3274
- setElapsed(newElapsed);
3275
- let newProgress = 0;
3276
- if (newElapsed >= 0) {
3277
- newProgress = Math.min(100, newElapsed / duration * 100);
3278
- }
3279
- if (currentDirection === "reverse") {
3280
- newProgress = 100 - newProgress;
3281
- } else if (currentDirection === "alternate") {
3282
- const cycle = Math.floor(newElapsed / duration);
3283
- const cycleProgress = newElapsed % duration / duration;
3284
- newProgress = cycle % 2 === 0 ? cycleProgress * 100 : (1 - cycleProgress) * 100;
3285
- }
3286
- setProgress(newProgress);
3287
- if (newElapsed >= duration) {
3288
- if (loop) {
3289
- startTimeRef.current = timestamp || performance.now();
3290
- totalPausedTimeRef.current = 0;
3291
- setElapsed(0);
3292
- setProgress(currentDirection === "reverse" ? 100 : 0);
3293
- } else {
3294
- setState("completed");
3295
- setProgress(currentDirection === "reverse" ? 0 : 100);
3296
- if (motionRef.current) {
3297
- cancelAnimationFrame(motionRef.current);
3298
- motionRef.current = null;
3299
- }
3300
- return;
3301
- }
3302
- }
3303
- if (state === "playing") {
3304
- motionRef.current = requestAnimationFrame(animate);
3305
- }
3306
- }, [state, duration, delay, loop, currentDirection]);
3307
- const play = react.useCallback(() => {
3308
- if (!mounted) return;
3309
- if (state === "completed") {
3310
- reset();
3311
- }
3312
- setState("playing");
3313
- if (pauseTimeRef.current) {
3314
- totalPausedTimeRef.current += performance.now() - pauseTimeRef.current;
3315
- pauseTimeRef.current = null;
3316
- } else {
3317
- startTimeRef.current = null;
3318
- totalPausedTimeRef.current = 0;
3319
- }
3320
- if (!motionRef.current) {
3321
- motionRef.current = requestAnimationFrame(animate);
3322
- }
3323
- }, [mounted, state, animate]);
3324
- const pause = react.useCallback(() => {
3325
- if (state !== "playing") return;
3326
- setState("paused");
3327
- pauseTimeRef.current = performance.now();
3328
- if (motionRef.current) {
3329
- cancelAnimationFrame(motionRef.current);
3330
- motionRef.current = null;
3331
- }
3332
- }, [state]);
3333
- const stop = react.useCallback(() => {
3334
- setState("idle");
3335
- setProgress(0);
3336
- setElapsed(0);
3337
- startTimeRef.current = null;
3338
- pauseTimeRef.current = null;
3339
- totalPausedTimeRef.current = 0;
3340
- if (motionRef.current) {
3341
- cancelAnimationFrame(motionRef.current);
3342
- motionRef.current = null;
3343
- }
3344
- }, []);
3345
- const reset = react.useCallback(() => {
3346
- setState("idle");
3347
- setProgress(0);
3348
- setElapsed(0);
3349
- setCurrentDirection(direction);
3350
- startTimeRef.current = null;
3351
- pauseTimeRef.current = null;
3352
- totalPausedTimeRef.current = 0;
3353
- if (motionRef.current) {
3354
- cancelAnimationFrame(motionRef.current);
3355
- motionRef.current = null;
3356
- }
3357
- }, [direction]);
3358
- const reverse = react.useCallback(() => {
3359
- const newDirection = currentDirection === "forward" ? "reverse" : "forward";
3360
- setCurrentDirection(newDirection);
3361
- if (state === "playing") {
3362
- const remainingTime = duration - elapsed;
3363
- startTimeRef.current = performance.now() - remainingTime;
3364
- totalPausedTimeRef.current = 0;
3365
- }
3366
- }, [currentDirection, state, duration, elapsed]);
3367
- const seek = react.useCallback((targetProgress) => {
3368
- const clampedProgress = Math.max(0, Math.min(100, targetProgress));
3369
- setProgress(clampedProgress);
3370
- let targetElapsed = 0;
3371
- if (currentDirection === "reverse") {
3372
- targetElapsed = (100 - clampedProgress) / 100 * duration;
3373
- } else if (currentDirection === "alternate") {
3374
- targetElapsed = clampedProgress / 100 * duration;
3375
- } else {
3376
- targetElapsed = clampedProgress / 100 * duration;
3377
- }
3378
- setElapsed(targetElapsed);
3379
- if (state === "playing" && startTimeRef.current) {
3380
- const currentTime = performance.now();
3381
- startTimeRef.current = currentTime - targetElapsed - totalPausedTimeRef.current;
3382
- }
3383
- }, [currentDirection, duration, state]);
3384
- const setMotionState = react.useCallback((newState) => {
3385
- setState(newState);
3386
- if (newState === "playing" && !motionRef.current) {
3387
- motionRef.current = requestAnimationFrame(animate);
3388
- } else if (newState !== "playing" && motionRef.current) {
3389
- cancelAnimationFrame(motionRef.current);
3390
- motionRef.current = null;
3391
- }
3392
- }, [animate]);
3393
- react.useEffect(() => {
3394
- if (mounted && autoStart && state === "idle") {
3395
- play();
3396
- }
3397
- }, [mounted, autoStart, state, play]);
3398
- react.useEffect(() => {
3399
- return () => {
3400
- if (motionRef.current) {
3401
- cancelAnimationFrame(motionRef.current);
3402
- }
3403
- };
3404
- }, []);
3405
- return {
3406
- state,
3407
- direction: currentDirection,
3408
- progress,
3409
- elapsed,
3410
- remaining: Math.max(0, duration - elapsed),
3411
- mounted,
3412
- play,
3413
- pause,
3414
- stop,
3415
- reset,
3416
- reverse,
3417
- seek,
3418
- setState: setMotionState
3419
- };
3420
- }
3421
- function useRepeat(options = {}) {
3422
- const {
3423
- duration = 1e3,
3424
- delay = 0,
3425
- autoStart = true,
3426
- type = "pulse",
3427
- intensity = 0.1
3428
- } = options;
3429
- const ref = react.useRef(null);
3430
- const [scale, setScale] = react.useState(1);
3431
- const [opacity, setOpacity] = react.useState(1);
3432
- const [isAnimating, setIsAnimating] = react.useState(false);
3433
- const [progress, setProgress] = react.useState(0);
3434
- const animationTimers = react.useRef([]);
3435
- const isRunning = react.useRef(false);
3436
- const clearAllTimers = react.useCallback(() => {
3437
- animationTimers.current.forEach((id) => clearTimeout(id));
3438
- animationTimers.current = [];
3439
- }, []);
3440
- const addTimer = react.useCallback((callback, ms) => {
3441
- const id = window.setTimeout(callback, ms);
3442
- animationTimers.current.push(id);
3443
- return id;
3444
- }, []);
3445
- const animate = react.useCallback(() => {
3446
- if (!isRunning.current) return;
3447
- setIsAnimating(true);
3448
- setProgress(0);
3449
- switch (type) {
3450
- case "pulse":
3451
- setScale(1 + intensity);
3452
- addTimer(() => {
3453
- if (!isRunning.current) return;
3454
- setScale(1);
3455
- setProgress(0.5);
3456
- }, duration / 2);
3457
- break;
3458
- case "bounce":
3459
- setScale(1 + intensity);
3460
- addTimer(() => {
3461
- if (!isRunning.current) return;
3462
- setScale(1 - intensity);
3463
- setProgress(0.33);
3464
- }, duration / 3);
3465
- addTimer(() => {
3466
- if (!isRunning.current) return;
3467
- setScale(1);
3468
- setProgress(0.66);
3469
- }, duration);
3470
- break;
3471
- case "wave":
3472
- setScale(1 + intensity);
3473
- addTimer(() => {
3474
- if (!isRunning.current) return;
3475
- setScale(1 - intensity);
3476
- setProgress(0.5);
3477
- }, duration / 2);
3478
- addTimer(() => {
3479
- if (!isRunning.current) return;
3480
- setScale(1);
3481
- setProgress(0.75);
3482
- }, duration);
3483
- break;
3484
- case "fade":
3485
- setOpacity(0.5);
3486
- addTimer(() => {
3487
- if (!isRunning.current) return;
3488
- setOpacity(1);
3489
- setProgress(0.5);
3490
- }, duration / 2);
3491
- break;
3492
- }
3493
- addTimer(() => {
3494
- if (!isRunning.current) return;
3495
- setProgress(1);
3496
- setIsAnimating(false);
3497
- animate();
3498
- }, duration);
3499
- }, [type, intensity, duration, addTimer]);
3500
- const start = react.useCallback(() => {
3501
- isRunning.current = true;
3502
- clearAllTimers();
3503
- if (delay > 0) {
3504
- addTimer(() => animate(), delay);
3505
- } else {
3506
- animate();
3507
- }
3508
- }, [delay, animate, clearAllTimers, addTimer]);
3509
- const stop = react.useCallback(() => {
3510
- isRunning.current = false;
3511
- clearAllTimers();
3512
- setIsAnimating(false);
3513
- setScale(1);
3514
- setOpacity(1);
3515
- setProgress(0);
3516
- }, [clearAllTimers]);
3517
- const reset = react.useCallback(() => {
3518
- stop();
3519
- }, [stop]);
3520
- react.useEffect(() => {
3521
- if (autoStart) {
3522
- start();
3523
- }
3524
- return () => {
3525
- isRunning.current = false;
3526
- clearAllTimers();
3527
- };
3528
- }, []);
3529
- const style = react.useMemo(() => ({
3530
- transform: `scale(${scale})`,
3531
- opacity,
3532
- transition: `transform ${duration / 2}ms ease-in-out, opacity ${duration / 2}ms ease-in-out`
3533
- }), [scale, opacity, duration]);
3534
- return {
3535
- ref,
3536
- isVisible: true,
3537
- isAnimating,
3538
- style,
3539
- progress,
3540
- start,
3541
- stop,
3542
- reset
3543
- };
3544
- }
3545
- function useToggleMotion(options = {}) {
3546
- const { duration = 300, delay = 0, easing: easing2 = "ease-in-out" } = options;
3547
- const ref = react.useRef(null);
3548
- const [isVisible, setIsVisible] = react.useState(false);
3549
- const [isAnimating, setIsAnimating] = react.useState(false);
3550
- const show = react.useCallback(() => {
3551
- setIsVisible(true);
3552
- setIsAnimating(true);
3553
- setTimeout(() => setIsAnimating(false), duration + delay);
3554
- }, [duration, delay]);
3555
- const hide = react.useCallback(() => {
3556
- setIsVisible(false);
3557
- setIsAnimating(true);
3558
- setTimeout(() => setIsAnimating(false), duration + delay);
3559
- }, [duration, delay]);
3560
- const toggle = react.useCallback(() => {
3561
- if (isVisible) {
3562
- hide();
3563
- } else {
3564
- show();
3565
- }
3566
- }, [isVisible, show, hide]);
3567
- const start = react.useCallback(() => show(), [show]);
3568
- const stop = react.useCallback(() => setIsAnimating(false), []);
3569
- const reset = react.useCallback(() => {
3570
- setIsVisible(false);
3571
- setIsAnimating(false);
3572
- }, []);
3573
- const style = react.useMemo(() => ({
3574
- opacity: isVisible ? 1 : 0,
3575
- transform: isVisible ? "translateY(0) scale(1)" : "translateY(10px) scale(0.95)",
3576
- transition: `opacity ${duration}ms ${easing2} ${delay}ms, transform ${duration}ms ${easing2} ${delay}ms`
3577
- }), [isVisible, duration, easing2, delay]);
3578
- return {
3579
- ref,
3580
- isVisible,
3581
- isAnimating,
3582
- style,
3583
- progress: isVisible ? 1 : 0,
3584
- start,
3585
- stop,
3586
- reset,
3587
- toggle,
3588
- show,
3589
- hide
3590
- };
3591
- }
3592
-
3593
- // src/hooks/useSlideDown.ts
3594
- function useSlideDown(options = {}) {
3595
- return useSlideUp({ ...options, direction: "down" });
3596
- }
3597
- function useInView(options = {}) {
3598
- const {
3599
- threshold = 0,
3600
- rootMargin = "0px",
3601
- triggerOnce = false,
3602
- initialInView = false
3603
- } = options;
3604
- const ref = react.useRef(null);
3605
- const [inView, setInView] = react.useState(initialInView);
3606
- const [entry, setEntry] = react.useState(null);
3607
- const frozenRef = react.useRef(false);
3608
- const handleIntersect = react.useCallback(
3609
- (entries) => {
3610
- const [observerEntry] = entries;
3611
- if (frozenRef.current) return;
3612
- setEntry(observerEntry);
3613
- setInView(observerEntry.isIntersecting);
3614
- if (triggerOnce && observerEntry.isIntersecting) {
3615
- frozenRef.current = true;
3616
- }
3617
- },
3618
- [triggerOnce]
3619
- );
3620
- react.useEffect(() => {
3621
- const element = ref.current;
3622
- if (!element) return;
3623
- const observer = new IntersectionObserver(handleIntersect, {
3624
- threshold,
3625
- rootMargin
3626
- });
3627
- observer.observe(element);
3628
- return () => {
3629
- observer.disconnect();
3630
- };
3631
- }, [threshold, rootMargin, handleIntersect]);
3632
- return {
3633
- ref,
3634
- inView,
3635
- entry
3636
- };
3637
- }
3638
- function useMouse(options = {}) {
3639
- const { targetRef, throttle = 0 } = options;
3640
- const [state, setState] = react.useState({
3641
- x: 0,
3642
- y: 0,
3643
- elementX: 0,
3644
- elementY: 0,
3645
- isOver: false
3646
- });
3647
- const lastUpdateRef = react.useRef(0);
3648
- const rafIdRef = react.useRef(null);
3649
- const updateMousePosition = react.useCallback(
3650
- (clientX, clientY, isOver) => {
3651
- const now = Date.now();
3652
- if (throttle > 0 && now - lastUpdateRef.current < throttle) {
3653
- return;
3654
- }
3655
- lastUpdateRef.current = now;
3656
- let elementX = 0;
3657
- let elementY = 0;
3658
- if (targetRef?.current) {
3659
- const rect = targetRef.current.getBoundingClientRect();
3660
- elementX = (clientX - rect.left) / rect.width;
3661
- elementY = (clientY - rect.top) / rect.height;
3662
- elementX = Math.max(0, Math.min(1, elementX));
3663
- elementY = Math.max(0, Math.min(1, elementY));
3664
- }
3665
- setState({
3666
- x: clientX,
3667
- y: clientY,
3668
- elementX,
3669
- elementY,
3670
- isOver
3671
- });
3672
- },
3673
- [targetRef, throttle]
3674
- );
3675
- react.useEffect(() => {
3676
- const target = targetRef?.current;
3677
- const handleMouseMove = (e) => {
3678
- if (rafIdRef.current) {
3679
- cancelAnimationFrame(rafIdRef.current);
3680
- }
3681
- rafIdRef.current = requestAnimationFrame(() => {
3682
- const isOver = target ? target.contains(e.target) : true;
3683
- updateMousePosition(e.clientX, e.clientY, isOver);
3684
- });
3685
- };
3686
- const handleMouseEnter = () => {
3687
- setState((prev) => ({ ...prev, isOver: true }));
3688
- };
3689
- const handleMouseLeave = () => {
3690
- setState((prev) => ({ ...prev, isOver: false }));
3691
- };
3692
- if (target) {
3693
- target.addEventListener("mousemove", handleMouseMove);
3694
- target.addEventListener("mouseenter", handleMouseEnter);
3695
- target.addEventListener("mouseleave", handleMouseLeave);
3696
- } else {
3697
- window.addEventListener("mousemove", handleMouseMove);
3698
- }
3699
- return () => {
3700
- if (rafIdRef.current) {
3701
- cancelAnimationFrame(rafIdRef.current);
3702
- }
3703
- if (target) {
3704
- target.removeEventListener("mousemove", handleMouseMove);
3705
- target.removeEventListener("mouseenter", handleMouseEnter);
3706
- target.removeEventListener("mouseleave", handleMouseLeave);
3707
- } else {
3708
- window.removeEventListener("mousemove", handleMouseMove);
3709
- }
3710
- };
3711
- }, [targetRef, updateMousePosition]);
3712
- return state;
3713
- }
3714
- function useReducedMotion() {
3715
- const [prefersReducedMotion, setPrefersReducedMotion] = react.useState(false);
3716
- react.useEffect(() => {
3717
- if (typeof window === "undefined") return;
3718
- const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
3719
- setPrefersReducedMotion(mediaQuery.matches);
3720
- const handleChange = (event) => {
3721
- setPrefersReducedMotion(event.matches);
3722
- };
3723
- mediaQuery.addEventListener("change", handleChange);
3724
- return () => {
3725
- mediaQuery.removeEventListener("change", handleChange);
3726
- };
3727
- }, []);
3728
- return {
3729
- prefersReducedMotion
3730
- };
3731
- }
3732
- function useWindowSize(options = {}) {
3733
- const {
3734
- debounce = 100,
3735
- initialWidth = 0,
3736
- initialHeight = 0
3737
- } = options;
3738
- const [state, setState] = react.useState({
3739
- width: initialWidth,
3740
- height: initialHeight,
3741
- isMounted: false
3742
- });
3743
- const timeoutRef = react.useRef(null);
3744
- const updateSize = react.useCallback(() => {
3745
- if (typeof window === "undefined") return;
3746
- setState({
3747
- width: window.innerWidth,
3748
- height: window.innerHeight,
3749
- isMounted: true
3750
- });
3751
- }, []);
3752
- const handleResize = react.useCallback(() => {
3753
- if (timeoutRef.current) {
3754
- clearTimeout(timeoutRef.current);
3755
- }
3756
- if (debounce > 0) {
3757
- timeoutRef.current = window.setTimeout(updateSize, debounce);
3758
- } else {
3759
- updateSize();
3760
- }
3761
- }, [debounce, updateSize]);
3762
- react.useEffect(() => {
3763
- updateSize();
3764
- window.addEventListener("resize", handleResize);
3765
- return () => {
3766
- if (timeoutRef.current) {
3767
- clearTimeout(timeoutRef.current);
3768
- }
3769
- window.removeEventListener("resize", handleResize);
3770
- };
3771
- }, [handleResize, updateSize]);
3772
- return state;
3773
- }
3774
- function useGesture(options = {}) {
3775
- const {
3776
- enabled = true,
3777
- threshold = 10,
3778
- timeout = 300,
3779
- swipeThreshold = 50,
3780
- swipeVelocity = 0.3,
3781
- swipeDirections = ["up", "down", "left", "right"],
3782
- pinchThreshold = 10,
3783
- minScale = 0.1,
3784
- maxScale = 10,
3785
- rotateThreshold = 5,
3786
- panThreshold = 10,
3787
- onSwipe,
3788
- onPinch,
3789
- onRotate,
3790
- onPan,
3791
- onTap,
3792
- onDoubleTap,
3793
- onLongPress,
3794
- onStart,
3795
- onMove,
3796
- onEnd
3797
- } = options;
3798
- const [isActive, setIsActive] = react.useState(false);
3799
- const [gesture, setGesture] = react.useState(null);
3800
- const [scale, setScale] = react.useState(1);
3801
- const [rotation, setRotation] = react.useState(0);
3802
- const [deltaX, setDeltaX] = react.useState(0);
3803
- const [deltaY, setDeltaY] = react.useState(0);
3804
- const [distance, setDistance] = react.useState(0);
3805
- const [velocity, setVelocity] = react.useState(0);
3806
- const stateRef = react.useRef({
3807
- isActive: false,
3808
- startX: 0,
3809
- startY: 0,
3810
- currentX: 0,
3811
- currentY: 0,
3812
- deltaX: 0,
3813
- deltaY: 0,
3814
- distance: 0,
3815
- velocity: 0,
3816
- startTime: 0,
3817
- currentTime: 0,
3818
- scale: 1,
3819
- rotation: 0,
3820
- startDistance: 0,
3821
- startAngle: 0,
3822
- touchCount: 0
3823
- });
3824
- const timeoutRef = react.useRef(null);
3825
- const longPressRef = react.useRef(null);
3826
- const lastTapRef = react.useRef(0);
3827
- const getDistance = react.useCallback((x1, y1, x2, y2) => {
3828
- return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
3829
- }, []);
3830
- const getAngle = react.useCallback((x1, y1, x2, y2) => {
3831
- return Math.atan2(y2 - y1, x2 - x1) * 180 / Math.PI;
3832
- }, []);
3833
- const getSwipeDirection = react.useCallback((deltaX2, deltaY2) => {
3834
- const absX = Math.abs(deltaX2);
3835
- const absY = Math.abs(deltaY2);
3836
- if (absX > absY) {
3837
- return deltaX2 > 0 ? "right" : "left";
3838
- } else {
3839
- return deltaY2 > 0 ? "down" : "up";
3840
- }
3841
- }, []);
3842
- const startGesture = react.useCallback((x, y, touchCount = 1) => {
3843
- if (!enabled) return;
3844
- const now = performance.now();
3845
- stateRef.current = {
3846
- ...stateRef.current,
3847
- isActive: true,
3848
- startX: x,
3849
- startY: y,
3850
- currentX: x,
3851
- currentY: y,
3852
- deltaX: 0,
3853
- deltaY: 0,
3854
- distance: 0,
3855
- velocity: 0,
3856
- startTime: now,
3857
- currentTime: now,
3858
- touchCount
3859
- };
3860
- setIsActive(true);
3861
- setGesture(null);
3862
- onStart?.(x, y);
3863
- if (onLongPress) {
3864
- longPressRef.current = window.setTimeout(() => {
3865
- onLongPress(x, y);
3866
- }, 500);
3867
- }
3868
- }, [enabled, onStart, onLongPress]);
3869
- const updateGesture = react.useCallback((x, y, touches) => {
3870
- if (!enabled || !stateRef.current.isActive) return;
3871
- const now = performance.now();
3872
- const state = stateRef.current;
3873
- const deltaX2 = x - state.startX;
3874
- const deltaY2 = y - state.startY;
3875
- const distance2 = getDistance(state.startX, state.startY, x, y);
3876
- const velocity2 = distance2 / (now - state.startTime) * 1e3;
3877
- state.currentX = x;
3878
- state.currentY = y;
3879
- state.deltaX = deltaX2;
3880
- state.deltaY = deltaY2;
3881
- state.distance = distance2;
3882
- state.velocity = velocity2;
3883
- state.currentTime = now;
3884
- setDeltaX(deltaX2);
3885
- setDeltaY(deltaY2);
3886
- setDistance(distance2);
3887
- setVelocity(velocity2);
3888
- if (touches && touches.length === 2) {
3889
- const touch1 = touches[0];
3890
- const touch2 = touches[1];
3891
- const currentDistance = getDistance(touch1.clientX, touch1.clientY, touch2.clientX, touch2.clientY);
3892
- const currentAngle = getAngle(touch1.clientX, touch1.clientY, touch2.clientX, touch2.clientY);
3893
- if (state.startDistance === 0) {
3894
- state.startDistance = currentDistance;
3895
- state.startAngle = currentAngle;
3896
- } else {
3897
- const scaleDelta = currentDistance / state.startDistance;
3898
- const newScale = Math.max(minScale, Math.min(maxScale, scale * scaleDelta));
3899
- if (Math.abs(scaleDelta - 1) * 100 > pinchThreshold) {
3900
- setScale(newScale);
3901
- setGesture("pinch");
3902
- onPinch?.(newScale, scaleDelta - 1);
3903
- }
3904
- const angleDelta = currentAngle - state.startAngle;
3905
- if (Math.abs(angleDelta) > rotateThreshold) {
3906
- const newRotation = rotation + angleDelta;
3907
- setRotation(newRotation);
3908
- setGesture("rotate");
3909
- onRotate?.(newRotation, angleDelta);
3910
- }
3911
- }
3912
- }
3913
- if (distance2 > panThreshold) {
3914
- setGesture("pan");
3915
- onPan?.(deltaX2, deltaY2, x - state.startX, y - state.startY);
3916
- }
3917
- onMove?.(x, y);
3918
- }, [enabled, getDistance, getAngle, scale, rotation, minScale, maxScale, pinchThreshold, rotateThreshold, panThreshold, onPinch, onRotate, onPan, onMove]);
3919
- const endGesture = react.useCallback((x, y) => {
3920
- if (!enabled || !stateRef.current.isActive) return;
3921
- const state = stateRef.current;
3922
- const now = performance.now();
3923
- const deltaX2 = x - state.startX;
3924
- const deltaY2 = y - state.startY;
3925
- const distance2 = getDistance(state.startX, state.startY, x, y);
3926
- const velocity2 = distance2 / (now - state.startTime) * 1e3;
3927
- if (longPressRef.current) {
3928
- clearTimeout(longPressRef.current);
3929
- longPressRef.current = null;
3930
- }
3931
- if (distance2 > swipeThreshold && velocity2 > swipeVelocity) {
3932
- const direction = getSwipeDirection(deltaX2, deltaY2);
3933
- if (direction && swipeDirections.includes(direction)) {
3934
- setGesture(`swipe-${direction}`);
3935
- onSwipe?.(direction, distance2, velocity2);
3936
- }
3937
- }
3938
- if (distance2 < threshold && now - state.startTime < timeout) {
3939
- const timeSinceLastTap = now - lastTapRef.current;
3940
- if (timeSinceLastTap < 300) {
3941
- onDoubleTap?.(x, y);
3942
- lastTapRef.current = 0;
3943
- } else {
3944
- onTap?.(x, y);
3945
- lastTapRef.current = now;
3946
- }
3947
- }
3948
- state.isActive = false;
3949
- setIsActive(false);
3950
- onEnd?.(x, y);
3951
- timeoutRef.current = window.setTimeout(() => {
3952
- setGesture(null);
3953
- setDeltaX(0);
3954
- setDeltaY(0);
3955
- setDistance(0);
3956
- setVelocity(0);
3957
- }, 100);
3958
- }, [enabled, getDistance, getSwipeDirection, swipeThreshold, swipeVelocity, swipeDirections, threshold, timeout, onSwipe, onTap, onDoubleTap, onEnd]);
3959
- const onTouchStart = react.useCallback((e) => {
3960
- if (e.cancelable) {
3961
- e.preventDefault();
3962
- }
3963
- const touch = e.touches[0];
3964
- startGesture(touch.clientX, touch.clientY, e.touches.length);
3965
- }, [startGesture]);
3966
- const onTouchMove = react.useCallback((e) => {
3967
- if (e.cancelable) {
3968
- e.preventDefault();
3969
- }
3970
- const touch = e.touches[0];
3971
- updateGesture(touch.clientX, touch.clientY, Array.from(e.touches));
3972
- }, [updateGesture]);
3973
- const onTouchEnd = react.useCallback((e) => {
3974
- if (e.cancelable) {
3975
- e.preventDefault();
3976
- }
3977
- const touch = e.changedTouches[0];
3978
- endGesture(touch.clientX, touch.clientY);
3979
- }, [endGesture]);
3980
- const onMouseDown = react.useCallback((e) => {
3981
- e.preventDefault();
3982
- startGesture(e.clientX, e.clientY, 1);
3983
- }, [startGesture]);
3984
- const onMouseMove = react.useCallback((e) => {
3985
- e.preventDefault();
3986
- updateGesture(e.clientX, e.clientY);
3987
- }, [updateGesture]);
3988
- const onMouseUp = react.useCallback((e) => {
3989
- e.preventDefault();
3990
- endGesture(e.clientX, e.clientY);
3991
- }, [endGesture]);
3992
- const start = react.useCallback(() => {
3993
- setIsActive(true);
3994
- }, []);
3995
- const stop = react.useCallback(() => {
3996
- setIsActive(false);
3997
- setGesture(null);
3998
- if (longPressRef.current) {
3999
- clearTimeout(longPressRef.current);
4000
- longPressRef.current = null;
4001
- }
4002
- }, []);
4003
- const reset = react.useCallback(() => {
4004
- setIsActive(false);
4005
- setGesture(null);
4006
- setScale(1);
4007
- setRotation(0);
4008
- setDeltaX(0);
4009
- setDeltaY(0);
4010
- setDistance(0);
4011
- setVelocity(0);
4012
- if (longPressRef.current) {
4013
- clearTimeout(longPressRef.current);
4014
- longPressRef.current = null;
4015
- }
4016
- }, []);
4017
- react.useEffect(() => {
4018
- return () => {
4019
- if (timeoutRef.current) {
4020
- clearTimeout(timeoutRef.current);
4021
- }
4022
- if (longPressRef.current) {
4023
- clearTimeout(longPressRef.current);
4024
- }
4025
- };
4026
- }, []);
4027
- return {
4028
- isActive,
4029
- gesture,
4030
- scale,
4031
- rotation,
4032
- deltaX,
4033
- deltaY,
4034
- distance,
4035
- velocity,
4036
- start,
4037
- stop,
4038
- reset,
4039
- onTouchStart,
4040
- onTouchMove,
4041
- onTouchEnd,
4042
- onMouseDown,
4043
- onMouseMove,
4044
- onMouseUp
4045
- };
4046
- }
4047
- function useGestureMotion(options) {
4048
- const {
4049
- gestureType,
4050
- duration = 300,
4051
- easing: easing2 = "ease-out",
4052
- sensitivity = 1,
4053
- enabled = true,
4054
- onGestureStart,
4055
- onGestureEnd
4056
- } = options;
4057
- const elementRef = react.useRef(null);
4058
- const [gestureState, setGestureState] = react.useState({
4059
- isActive: false,
4060
- x: 0,
4061
- y: 0,
4062
- deltaX: 0,
4063
- deltaY: 0,
4064
- scale: 1,
4065
- rotation: 0
4066
- });
4067
- const [motionStyle, setMotionStyle] = react.useState({});
4068
- const startPoint = react.useRef({ x: 0, y: 0 });
4069
- const isDragging = react.useRef(false);
4070
- const updateMotionStyle = react.useCallback(() => {
4071
- if (!enabled) return;
4072
- const { isActive, deltaX, deltaY, scale, rotation } = gestureState;
4073
- let transform = "";
4074
- switch (gestureType) {
4075
- case "hover":
4076
- transform = isActive ? `scale(${1 + sensitivity * 0.05}) translateY(-${sensitivity * 2}px)` : "scale(1) translateY(0)";
4077
- break;
4078
- case "drag":
4079
- transform = isActive ? `translate(${deltaX * sensitivity}px, ${deltaY * sensitivity}px)` : "translate(0, 0)";
4080
- break;
4081
- case "pinch":
4082
- transform = `scale(${scale})`;
4083
- break;
4084
- case "swipe":
4085
- transform = isActive ? `translateX(${deltaX * sensitivity}px) rotateY(${deltaX * 0.1}deg)` : "translateX(0) rotateY(0)";
4086
- break;
4087
- case "tilt":
4088
- transform = isActive ? `rotateX(${deltaY * 0.1}deg) rotateY(${deltaX * 0.1}deg)` : "rotateX(0) rotateY(0)";
4089
- break;
4090
- }
4091
- setMotionStyle({
4092
- transform,
4093
- transition: isActive ? "none" : `all ${duration}ms ${easing2}`,
4094
- cursor: gestureType === "drag" && isActive ? "grabbing" : "pointer"
4095
- });
4096
- }, [gestureState, gestureType, enabled, duration, easing2, sensitivity]);
4097
- const handleMouseDown = react.useCallback((e) => {
4098
- if (!enabled || gestureType !== "drag") return;
4099
- isDragging.current = true;
4100
- startPoint.current = { x: e.clientX, y: e.clientY };
4101
- setGestureState((prev) => ({ ...prev, isActive: true }));
4102
- onGestureStart?.();
4103
- }, [enabled, gestureType, onGestureStart]);
4104
- const handleMouseMove = react.useCallback((e) => {
4105
- if (!enabled || !isDragging.current) return;
4106
- const deltaX = e.clientX - startPoint.current.x;
4107
- const deltaY = e.clientY - startPoint.current.y;
4108
- setGestureState((prev) => ({
4109
- ...prev,
4110
- x: e.clientX,
4111
- y: e.clientY,
4112
- deltaX,
4113
- deltaY
4114
- }));
4115
- }, [enabled]);
4116
- const handleMouseUp = react.useCallback(() => {
4117
- if (!enabled) return;
4118
- isDragging.current = false;
4119
- setGestureState((prev) => ({ ...prev, isActive: false }));
4120
- onGestureEnd?.();
4121
- }, [enabled, onGestureEnd]);
4122
- const handleMouseEnter = react.useCallback(() => {
4123
- if (!enabled || gestureType !== "hover") return;
4124
- setGestureState((prev) => ({ ...prev, isActive: true }));
4125
- onGestureStart?.();
4126
- }, [enabled, gestureType, onGestureStart]);
4127
- const handleMouseLeave = react.useCallback(() => {
4128
- if (!enabled || gestureType !== "hover") return;
4129
- setGestureState((prev) => ({ ...prev, isActive: false }));
4130
- onGestureEnd?.();
4131
- }, [enabled, gestureType, onGestureEnd]);
4132
- const handleTouchStart = react.useCallback((e) => {
4133
- if (!enabled) return;
4134
- const touch = e.touches[0];
4135
- startPoint.current = { x: touch.clientX, y: touch.clientY };
4136
- setGestureState((prev) => ({ ...prev, isActive: true }));
4137
- onGestureStart?.();
4138
- }, [enabled, onGestureStart]);
4139
- const handleTouchMove = react.useCallback((e) => {
4140
- if (!enabled) return;
4141
- const touch = e.touches[0];
4142
- const deltaX = touch.clientX - startPoint.current.x;
4143
- const deltaY = touch.clientY - startPoint.current.y;
4144
- setGestureState((prev) => ({
4145
- ...prev,
4146
- x: touch.clientX,
4147
- y: touch.clientY,
4148
- deltaX,
4149
- deltaY
4150
- }));
4151
- }, [enabled]);
4152
- const handleTouchEnd = react.useCallback(() => {
4153
- if (!enabled) return;
4154
- setGestureState((prev) => ({ ...prev, isActive: false }));
4155
- onGestureEnd?.();
4156
- }, [enabled, onGestureEnd]);
4157
- react.useEffect(() => {
4158
- if (!elementRef.current) return;
4159
- const element = elementRef.current;
4160
- if (gestureType === "hover") {
4161
- element.addEventListener("mouseenter", handleMouseEnter);
4162
- element.addEventListener("mouseleave", handleMouseLeave);
4163
- } else if (gestureType === "drag") {
4164
- element.addEventListener("mousedown", handleMouseDown);
4165
- document.addEventListener("mousemove", handleMouseMove);
4166
- document.addEventListener("mouseup", handleMouseUp);
4167
- }
4168
- element.addEventListener("touchstart", handleTouchStart);
4169
- element.addEventListener("touchmove", handleTouchMove);
4170
- element.addEventListener("touchend", handleTouchEnd);
4171
- return () => {
4172
- element.removeEventListener("mouseenter", handleMouseEnter);
4173
- element.removeEventListener("mouseleave", handleMouseLeave);
4174
- element.removeEventListener("mousedown", handleMouseDown);
4175
- document.removeEventListener("mousemove", handleMouseMove);
4176
- document.removeEventListener("mouseup", handleMouseUp);
4177
- element.removeEventListener("touchstart", handleTouchStart);
4178
- element.removeEventListener("touchmove", handleTouchMove);
4179
- element.removeEventListener("touchend", handleTouchEnd);
4180
- };
4181
- }, [gestureType, handleMouseEnter, handleMouseLeave, handleMouseDown, handleMouseMove, handleMouseUp, handleTouchStart, handleTouchMove, handleTouchEnd]);
4182
- react.useEffect(() => {
4183
- updateMotionStyle();
4184
- }, [updateMotionStyle]);
4185
- return {
4186
- ref: elementRef,
4187
- gestureState,
4188
- motionStyle,
4189
- isActive: gestureState.isActive
4190
- };
4191
- }
4192
-
4193
- exports.MOTION_PRESETS = MOTION_PRESETS;
4194
- exports.MotionEngine = MotionEngine;
4195
- exports.PAGE_MOTIONS = PAGE_MOTIONS;
4196
- exports.PerformanceOptimizer = PerformanceOptimizer;
4197
- exports.TransitionEffects = TransitionEffects;
4198
- exports.applyEasing = applyEasing;
4199
- exports.easeIn = easeIn;
4200
- exports.easeInOut = easeInOut;
4201
- exports.easeInOutQuad = easeInOutQuad;
4202
- exports.easeInQuad = easeInQuad;
4203
- exports.easeOut = easeOut;
4204
- exports.easeOutQuad = easeOutQuad;
4205
- exports.easingPresets = easingPresets;
4206
- exports.getAvailableEasings = getAvailableEasings;
4207
- exports.getEasing = getEasing;
4208
- exports.getMotionPreset = getMotionPreset;
4209
- exports.getPagePreset = getPagePreset;
4210
- exports.getPresetEasing = getPresetEasing;
4211
- exports.isEasingFunction = isEasingFunction;
4212
- exports.isValidEasing = isValidEasing;
4213
- exports.linear = linear;
4214
- exports.mergeWithPreset = mergeWithPreset;
4215
- exports.motionEngine = motionEngine;
4216
- exports.performanceOptimizer = performanceOptimizer;
4217
- exports.safeApplyEasing = safeApplyEasing;
4218
- exports.transitionEffects = transitionEffects;
4219
- exports.useBounceIn = useBounceIn;
4220
- exports.useClickToggle = useClickToggle;
4221
- exports.useFadeIn = useFadeIn;
4222
- exports.useFocusToggle = useFocusToggle;
4223
- exports.useGesture = useGesture;
4224
- exports.useGestureMotion = useGestureMotion;
4225
- exports.useGradient = useGradient;
4226
- exports.useHoverMotion = useHoverMotion;
4227
- exports.useInView = useInView;
4228
- exports.useMotionState = useMotionState;
4229
- exports.useMouse = useMouse;
4230
- exports.usePageMotions = usePageMotions;
4231
- exports.usePulse = usePulse;
4232
- exports.useReducedMotion = useReducedMotion;
4233
- exports.useRepeat = useRepeat;
4234
- exports.useScaleIn = useScaleIn;
4235
- exports.useScrollProgress = useScrollProgress;
4236
- exports.useScrollReveal = useScrollReveal;
4237
- exports.useSimplePageMotion = useSimplePageMotion;
4238
- exports.useSlideDown = useSlideDown;
4239
- exports.useSlideLeft = useSlideLeft;
4240
- exports.useSlideRight = useSlideRight;
4241
- exports.useSlideUp = useSlideUp;
4242
- exports.useSmartMotion = useSmartMotion;
4243
- exports.useSpringMotion = useSpringMotion;
4244
- exports.useToggleMotion = useToggleMotion;
4245
- exports.useUnifiedMotion = useUnifiedMotion;
4246
- exports.useWindowSize = useWindowSize;
4247
- //# sourceMappingURL=index.js.map
4248
- //# sourceMappingURL=index.js.map