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