@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.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
2
+ import { createContext, useState, useRef, useCallback, useEffect, useMemo, createElement, useContext } from 'react';
3
3
  import { jsx } from 'react/jsx-runtime';
4
4
 
5
5
  // src/core/MotionEngine.ts
@@ -195,17 +195,11 @@ var TransitionEffects = class _TransitionEffects {
195
195
  return _TransitionEffects.instance;
196
196
  }
197
197
  /**
198
- * 페이드 인/아웃 전환
198
+ * 공통 전환 실행 헬퍼
199
199
  */
200
- async fade(element, options) {
200
+ async executeTransition(element, options, config) {
201
201
  const transitionId = this.generateTransitionId();
202
- const initialOpacity = parseFloat(getComputedStyle(element).opacity) || 1;
203
- const targetOpacity = options.direction === "reverse" ? 0 : 1;
204
- if (options.direction === "reverse") {
205
- element.style.opacity = initialOpacity.toString();
206
- } else {
207
- element.style.opacity = "0";
208
- }
202
+ config.setup();
209
203
  this.enableGPUAcceleration(element);
210
204
  let resolveTransition;
211
205
  const completed = new Promise((resolve) => {
@@ -214,19 +208,17 @@ var TransitionEffects = class _TransitionEffects {
214
208
  const motionId = await motionEngine.motion(
215
209
  element,
216
210
  [
217
- { progress: 0, properties: { opacity: options.direction === "reverse" ? initialOpacity : 0 } },
218
- { progress: 1, properties: { opacity: targetOpacity } }
211
+ { progress: 0, properties: config.keyframes[0] },
212
+ { progress: 1, properties: config.keyframes[1] }
219
213
  ],
220
214
  {
221
215
  duration: options.duration,
222
216
  easing: options.easing || this.getDefaultEasing(),
223
217
  delay: options.delay,
224
218
  onStart: options.onTransitionStart,
225
- onUpdate: (progress) => {
226
- const currentOpacity = options.direction === "reverse" ? initialOpacity * (1 - progress) : targetOpacity * progress;
227
- element.style.opacity = currentOpacity.toString();
228
- },
219
+ onUpdate: config.onUpdate,
229
220
  onComplete: () => {
221
+ config.onCleanup();
230
222
  options.onTransitionComplete?.();
231
223
  this.activeTransitions.delete(transitionId);
232
224
  resolveTransition();
@@ -236,224 +228,170 @@ var TransitionEffects = class _TransitionEffects {
236
228
  this.activeTransitions.set(transitionId, motionId);
237
229
  return completed;
238
230
  }
231
+ /**
232
+ * 페이드 인/아웃 전환
233
+ */
234
+ async fade(element, options) {
235
+ const initialOpacity = parseFloat(getComputedStyle(element).opacity) || 1;
236
+ const targetOpacity = options.direction === "reverse" ? 0 : 1;
237
+ return this.executeTransition(element, options, {
238
+ setup: () => {
239
+ if (options.direction === "reverse") {
240
+ element.style.opacity = initialOpacity.toString();
241
+ } else {
242
+ element.style.opacity = "0";
243
+ }
244
+ },
245
+ keyframes: [
246
+ { opacity: options.direction === "reverse" ? initialOpacity : 0 },
247
+ { opacity: targetOpacity }
248
+ ],
249
+ onUpdate: (progress) => {
250
+ const currentOpacity = options.direction === "reverse" ? initialOpacity * (1 - progress) : targetOpacity * progress;
251
+ element.style.opacity = currentOpacity.toString();
252
+ },
253
+ onCleanup: () => {
254
+ }
255
+ });
256
+ }
239
257
  /**
240
258
  * 슬라이드 전환
241
259
  */
242
260
  async slide(element, options) {
243
- const transitionId = this.generateTransitionId();
244
261
  const distance = options.distance || 100;
245
262
  const initialTransform = getComputedStyle(element).transform;
246
263
  const isReverse = options.direction === "reverse";
247
- if (!isReverse) {
248
- element.style.transform = `translateX(${distance}px)`;
249
- }
250
- this.enableGPUAcceleration(element);
251
- let resolveTransition;
252
- const completed = new Promise((resolve) => {
253
- resolveTransition = resolve;
254
- });
255
- const motionId = await motionEngine.motion(
256
- element,
257
- [
258
- { progress: 0, properties: { translateX: isReverse ? 0 : distance } },
259
- { progress: 1, properties: { translateX: isReverse ? distance : 0 } }
260
- ],
261
- {
262
- duration: options.duration,
263
- easing: options.easing || this.getDefaultEasing(),
264
- delay: options.delay,
265
- onStart: options.onTransitionStart,
266
- onUpdate: (progress) => {
267
- const currentTranslateX = isReverse ? distance * progress : distance * (1 - progress);
268
- element.style.transform = `translateX(${currentTranslateX}px)`;
269
- },
270
- onComplete: () => {
271
- element.style.transform = initialTransform;
272
- options.onTransitionComplete?.();
273
- this.activeTransitions.delete(transitionId);
274
- resolveTransition();
264
+ return this.executeTransition(element, options, {
265
+ setup: () => {
266
+ if (!isReverse) {
267
+ element.style.transform = `translateX(${distance}px)`;
275
268
  }
269
+ },
270
+ keyframes: [
271
+ { translateX: isReverse ? 0 : distance },
272
+ { translateX: isReverse ? distance : 0 }
273
+ ],
274
+ onUpdate: (progress) => {
275
+ const currentTranslateX = isReverse ? distance * progress : distance * (1 - progress);
276
+ element.style.transform = `translateX(${currentTranslateX}px)`;
277
+ },
278
+ onCleanup: () => {
279
+ element.style.transform = initialTransform;
276
280
  }
277
- );
278
- this.activeTransitions.set(transitionId, motionId);
279
- return completed;
281
+ });
280
282
  }
281
283
  /**
282
284
  * 스케일 전환
283
285
  */
284
286
  async scale(element, options) {
285
- const transitionId = this.generateTransitionId();
286
287
  const scaleValue = options.scale || 0.8;
287
288
  const initialTransform = getComputedStyle(element).transform;
288
289
  const isReverse = options.direction === "reverse";
289
- if (!isReverse) {
290
- element.style.transform = `scale(${scaleValue})`;
291
- }
292
- this.enableGPUAcceleration(element);
293
- let resolveTransition;
294
- const completed = new Promise((resolve) => {
295
- resolveTransition = resolve;
296
- });
297
- const motionId = await motionEngine.motion(
298
- element,
299
- [
300
- { progress: 0, properties: { scale: isReverse ? 1 : scaleValue } },
301
- { progress: 1, properties: { scale: isReverse ? scaleValue : 1 } }
302
- ],
303
- {
304
- duration: options.duration,
305
- easing: options.easing || this.getDefaultEasing(),
306
- delay: options.delay,
307
- onStart: options.onTransitionStart,
308
- onUpdate: (progress) => {
309
- const currentScale = isReverse ? 1 - (1 - scaleValue) * progress : scaleValue + (1 - scaleValue) * progress;
310
- element.style.transform = `scale(${currentScale})`;
311
- },
312
- onComplete: () => {
313
- element.style.transform = initialTransform;
314
- options.onTransitionComplete?.();
315
- this.activeTransitions.delete(transitionId);
316
- resolveTransition();
290
+ return this.executeTransition(element, options, {
291
+ setup: () => {
292
+ if (!isReverse) {
293
+ element.style.transform = `scale(${scaleValue})`;
317
294
  }
295
+ },
296
+ keyframes: [
297
+ { scale: isReverse ? 1 : scaleValue },
298
+ { scale: isReverse ? scaleValue : 1 }
299
+ ],
300
+ onUpdate: (progress) => {
301
+ const currentScale = isReverse ? 1 - (1 - scaleValue) * progress : scaleValue + (1 - scaleValue) * progress;
302
+ element.style.transform = `scale(${currentScale})`;
303
+ },
304
+ onCleanup: () => {
305
+ element.style.transform = initialTransform;
318
306
  }
319
- );
320
- this.activeTransitions.set(transitionId, motionId);
321
- return completed;
307
+ });
322
308
  }
323
309
  /**
324
310
  * 플립 전환 (3D 회전)
325
311
  */
326
312
  async flip(element, options) {
327
- const transitionId = this.generateTransitionId();
328
313
  const perspective = options.perspective || 1e3;
329
314
  const initialTransform = getComputedStyle(element).transform;
330
315
  const isReverse = options.direction === "reverse";
331
- element.style.perspective = `${perspective}px`;
332
- element.style.transformStyle = "preserve-3d";
333
- if (!isReverse) {
334
- element.style.transform = `rotateY(90deg)`;
335
- }
336
- this.enableGPUAcceleration(element);
337
- let resolveTransition;
338
- const completed = new Promise((resolve) => {
339
- resolveTransition = resolve;
340
- });
341
- const motionId = await motionEngine.motion(
342
- element,
343
- [
344
- { progress: 0, properties: { rotateY: isReverse ? 0 : 90 } },
345
- { progress: 1, properties: { rotateY: isReverse ? 90 : 0 } }
346
- ],
347
- {
348
- duration: options.duration,
349
- easing: options.easing || this.getDefaultEasing(),
350
- delay: options.delay,
351
- onStart: options.onTransitionStart,
352
- onUpdate: (progress) => {
353
- const currentRotateY = isReverse ? 90 * progress : 90 * (1 - progress);
354
- element.style.transform = `rotateY(${currentRotateY}deg)`;
355
- },
356
- onComplete: () => {
357
- element.style.transform = initialTransform;
358
- element.style.perspective = "";
359
- element.style.transformStyle = "";
360
- options.onTransitionComplete?.();
361
- this.activeTransitions.delete(transitionId);
362
- resolveTransition();
316
+ return this.executeTransition(element, options, {
317
+ setup: () => {
318
+ element.style.perspective = `${perspective}px`;
319
+ element.style.transformStyle = "preserve-3d";
320
+ if (!isReverse) {
321
+ element.style.transform = `rotateY(90deg)`;
363
322
  }
323
+ },
324
+ keyframes: [
325
+ { rotateY: isReverse ? 0 : 90 },
326
+ { rotateY: isReverse ? 90 : 0 }
327
+ ],
328
+ onUpdate: (progress) => {
329
+ const currentRotateY = isReverse ? 90 * progress : 90 * (1 - progress);
330
+ element.style.transform = `rotateY(${currentRotateY}deg)`;
331
+ },
332
+ onCleanup: () => {
333
+ element.style.transform = initialTransform;
334
+ element.style.perspective = "";
335
+ element.style.transformStyle = "";
364
336
  }
365
- );
366
- this.activeTransitions.set(transitionId, motionId);
367
- return completed;
337
+ });
368
338
  }
369
339
  /**
370
340
  * 큐브 전환 (3D 큐브 회전)
371
341
  */
372
342
  async cube(element, options) {
373
- const transitionId = this.generateTransitionId();
374
343
  const perspective = options.perspective || 1200;
375
344
  const initialTransform = getComputedStyle(element).transform;
376
345
  const isReverse = options.direction === "reverse";
377
- element.style.perspective = `${perspective}px`;
378
- element.style.transformStyle = "preserve-3d";
379
- if (!isReverse) {
380
- element.style.transform = `rotateX(90deg) rotateY(45deg)`;
381
- }
382
- this.enableGPUAcceleration(element);
383
- let resolveTransition;
384
- const completed = new Promise((resolve) => {
385
- resolveTransition = resolve;
386
- });
387
- const motionId = await motionEngine.motion(
388
- element,
389
- [
390
- { progress: 0, properties: { rotateX: isReverse ? 0 : 90, rotateY: isReverse ? 0 : 45 } },
391
- { progress: 1, properties: { rotateX: isReverse ? 90 : 0, rotateY: isReverse ? 45 : 0 } }
392
- ],
393
- {
394
- duration: options.duration,
395
- easing: options.easing || this.getDefaultEasing(),
396
- delay: options.delay,
397
- onStart: options.onTransitionStart,
398
- onUpdate: (progress) => {
399
- const currentRotateX = isReverse ? 90 * progress : 90 * (1 - progress);
400
- const currentRotateY = isReverse ? 45 * progress : 45 * (1 - progress);
401
- element.style.transform = `rotateX(${currentRotateX}deg) rotateY(${currentRotateY}deg)`;
402
- },
403
- onComplete: () => {
404
- element.style.transform = initialTransform;
405
- element.style.perspective = "";
406
- element.style.transformStyle = "";
407
- options.onTransitionComplete?.();
408
- this.activeTransitions.delete(transitionId);
409
- resolveTransition();
346
+ return this.executeTransition(element, options, {
347
+ setup: () => {
348
+ element.style.perspective = `${perspective}px`;
349
+ element.style.transformStyle = "preserve-3d";
350
+ if (!isReverse) {
351
+ element.style.transform = `rotateX(90deg) rotateY(45deg)`;
410
352
  }
353
+ },
354
+ keyframes: [
355
+ { rotateX: isReverse ? 0 : 90, rotateY: isReverse ? 0 : 45 },
356
+ { rotateX: isReverse ? 90 : 0, rotateY: isReverse ? 45 : 0 }
357
+ ],
358
+ onUpdate: (progress) => {
359
+ const currentRotateX = isReverse ? 90 * progress : 90 * (1 - progress);
360
+ const currentRotateY = isReverse ? 45 * progress : 45 * (1 - progress);
361
+ element.style.transform = `rotateX(${currentRotateX}deg) rotateY(${currentRotateY}deg)`;
362
+ },
363
+ onCleanup: () => {
364
+ element.style.transform = initialTransform;
365
+ element.style.perspective = "";
366
+ element.style.transformStyle = "";
411
367
  }
412
- );
413
- this.activeTransitions.set(transitionId, motionId);
414
- return completed;
368
+ });
415
369
  }
416
370
  /**
417
371
  * 모프 전환 (복합 변형)
418
372
  */
419
373
  async morph(element, options) {
420
- const transitionId = this.generateTransitionId();
421
374
  const initialTransform = getComputedStyle(element).transform;
422
375
  const isReverse = options.direction === "reverse";
423
- if (!isReverse) {
424
- element.style.transform = `scale(0.9) rotate(5deg)`;
425
- }
426
- this.enableGPUAcceleration(element);
427
- let resolveTransition;
428
- const completed = new Promise((resolve) => {
429
- resolveTransition = resolve;
430
- });
431
- const motionId = await motionEngine.motion(
432
- element,
433
- [
434
- { progress: 0, properties: { scale: isReverse ? 1 : 0.9, rotate: isReverse ? 0 : 5 } },
435
- { progress: 1, properties: { scale: isReverse ? 0.9 : 1, rotate: isReverse ? 5 : 0 } }
436
- ],
437
- {
438
- duration: options.duration,
439
- easing: options.easing || this.getDefaultEasing(),
440
- delay: options.delay,
441
- onStart: options.onTransitionStart,
442
- onUpdate: (progress) => {
443
- const currentScale = isReverse ? 1 - 0.1 * progress : 0.9 + 0.1 * progress;
444
- const currentRotate = isReverse ? 5 * progress : 5 * (1 - progress);
445
- element.style.transform = `scale(${currentScale}) rotate(${currentRotate}deg)`;
446
- },
447
- onComplete: () => {
448
- element.style.transform = initialTransform;
449
- options.onTransitionComplete?.();
450
- this.activeTransitions.delete(transitionId);
451
- resolveTransition();
376
+ return this.executeTransition(element, options, {
377
+ setup: () => {
378
+ if (!isReverse) {
379
+ element.style.transform = `scale(0.9) rotate(5deg)`;
452
380
  }
381
+ },
382
+ keyframes: [
383
+ { scale: isReverse ? 1 : 0.9, rotate: isReverse ? 0 : 5 },
384
+ { scale: isReverse ? 0.9 : 1, rotate: isReverse ? 5 : 0 }
385
+ ],
386
+ onUpdate: (progress) => {
387
+ const currentScale = isReverse ? 1 - 0.1 * progress : 0.9 + 0.1 * progress;
388
+ const currentRotate = isReverse ? 5 * progress : 5 * (1 - progress);
389
+ element.style.transform = `scale(${currentScale}) rotate(${currentRotate}deg)`;
390
+ },
391
+ onCleanup: () => {
392
+ element.style.transform = initialTransform;
453
393
  }
454
- );
455
- this.activeTransitions.set(transitionId, motionId);
456
- return completed;
394
+ });
457
395
  }
458
396
  /**
459
397
  * 전환 중지
@@ -515,390 +453,91 @@ var TransitionEffects = class _TransitionEffects {
515
453
  };
516
454
  var transitionEffects = TransitionEffects.getInstance();
517
455
 
518
- // src/core/PerformanceOptimizer.ts
519
- var PerformanceOptimizer = class _PerformanceOptimizer {
520
- constructor() {
521
- this.performanceObserver = null;
522
- this.layerRegistry = /* @__PURE__ */ new Set();
523
- this.isMonitoring = false;
524
- this.config = {
525
- enableGPUAcceleration: true,
526
- enableLayerSeparation: true,
527
- enableMemoryOptimization: true,
528
- targetFPS: 60,
529
- maxLayerCount: 100,
530
- memoryThreshold: 50 * 1024 * 1024
531
- // 50MB
532
- };
533
- this.metrics = {
534
- fps: 0,
535
- layerCount: 0,
536
- activeMotions: 0
537
- };
538
- this.initializePerformanceMonitoring();
456
+ // src/presets/index.ts
457
+ var MOTION_PRESETS = {
458
+ hero: {
459
+ entrance: "fadeIn",
460
+ delay: 200,
461
+ duration: 800,
462
+ hover: false,
463
+ click: false
464
+ },
465
+ title: {
466
+ entrance: "slideUp",
467
+ delay: 400,
468
+ duration: 700,
469
+ hover: false,
470
+ click: false
471
+ },
472
+ button: {
473
+ entrance: "scaleIn",
474
+ delay: 600,
475
+ duration: 300,
476
+ hover: true,
477
+ click: true
478
+ },
479
+ card: {
480
+ entrance: "slideUp",
481
+ delay: 800,
482
+ duration: 500,
483
+ hover: true,
484
+ click: false
485
+ },
486
+ text: {
487
+ entrance: "fadeIn",
488
+ delay: 200,
489
+ duration: 600,
490
+ hover: false,
491
+ click: false
492
+ },
493
+ image: {
494
+ entrance: "scaleIn",
495
+ delay: 400,
496
+ duration: 600,
497
+ hover: true,
498
+ click: false
539
499
  }
540
- static getInstance() {
541
- if (!_PerformanceOptimizer.instance) {
542
- _PerformanceOptimizer.instance = new _PerformanceOptimizer();
543
- }
544
- return _PerformanceOptimizer.instance;
545
- }
546
- /**
547
- * 성능 모니터링 초기화
548
- */
549
- initializePerformanceMonitoring() {
550
- if (typeof PerformanceObserver !== "undefined") {
551
- try {
552
- this.performanceObserver = new PerformanceObserver((list) => {
553
- const entries = list.getEntries();
554
- this.updatePerformanceMetrics(entries);
555
- });
556
- this.performanceObserver.observe({ entryTypes: ["measure", "navigation"] });
557
- } catch (error) {
558
- if (process.env.NODE_ENV === "development") {
559
- console.warn("Performance monitoring not supported:", error);
560
- }
561
- }
562
- }
563
- }
564
- /**
565
- * 성능 메트릭 업데이트
566
- */
567
- updatePerformanceMetrics(entries) {
568
- entries.forEach((entry) => {
569
- if (entry.entryType === "measure") {
570
- this.calculateFPS();
571
- }
572
- });
573
- }
574
- /**
575
- * FPS 계산
576
- */
577
- calculateFPS() {
578
- const now = performance.now();
579
- const deltaTime = now - (this.lastFrameTime || now);
580
- this.lastFrameTime = now;
581
- if (deltaTime > 0) {
582
- this.metrics.fps = Math.round(1e3 / deltaTime);
583
- }
584
- }
585
- /**
586
- * GPU 가속 활성화
587
- */
588
- enableGPUAcceleration(element) {
589
- if (!this.config.enableGPUAcceleration) return;
590
- try {
591
- element.style.willChange = "transform, opacity";
592
- element.style.transform = "translateZ(0)";
593
- element.style.backfaceVisibility = "hidden";
594
- element.style.transformStyle = "preserve-3d";
595
- this.registerLayer(element);
596
- } catch (error) {
597
- if (process.env.NODE_ENV === "development") {
598
- console.warn("GPU acceleration failed:", error);
599
- }
600
- }
601
- }
602
- /**
603
- * 레이어 분리 및 최적화
604
- */
605
- createOptimizedLayer(element) {
606
- if (!this.config.enableLayerSeparation) return;
607
- try {
608
- element.style.transform = "translateZ(0)";
609
- element.style.backfaceVisibility = "hidden";
610
- element.style.perspective = "1000px";
611
- this.registerLayer(element);
612
- this.checkLayerLimit();
613
- } catch (error) {
614
- if (process.env.NODE_ENV === "development") {
615
- console.warn("Layer optimization failed:", error);
616
- }
617
- }
618
- }
619
- /**
620
- * 레이어 등록
621
- */
622
- registerLayer(element) {
623
- if (this.layerRegistry.has(element)) return;
624
- this.layerRegistry.add(element);
625
- this.metrics.layerCount = this.layerRegistry.size;
626
- this.checkMemoryUsage();
627
- }
628
- /**
629
- * 레이어 제거
630
- */
631
- removeLayer(element) {
632
- if (this.layerRegistry.has(element)) {
633
- this.layerRegistry.delete(element);
634
- this.metrics.layerCount = this.layerRegistry.size;
635
- element.style.willChange = "auto";
636
- element.style.transform = "";
637
- element.style.backfaceVisibility = "";
638
- element.style.transformStyle = "";
639
- element.style.perspective = "";
640
- }
641
- }
642
- /**
643
- * 레이어 수 제한 체크
644
- */
645
- checkLayerLimit() {
646
- if (this.metrics.layerCount > this.config.maxLayerCount) {
647
- if (process.env.NODE_ENV === "development") {
648
- console.warn(`Layer count (${this.metrics.layerCount}) exceeds limit (${this.config.maxLayerCount})`);
649
- }
650
- this.cleanupOldLayers();
651
- }
652
- }
653
- /**
654
- * 오래된 레이어 정리
655
- */
656
- cleanupOldLayers() {
657
- const layersToRemove = Array.from(this.layerRegistry).slice(0, 10);
658
- layersToRemove.forEach((layer) => {
659
- this.removeLayer(layer);
660
- });
661
- }
662
- /**
663
- * 메모리 사용량 체크
664
- */
665
- checkMemoryUsage() {
666
- if (!this.config.enableMemoryOptimization) return;
667
- if ("memory" in performance) {
668
- const memory = performance.memory;
669
- this.metrics.memoryUsage = memory.usedJSHeapSize;
670
- if (memory.usedJSHeapSize > this.config.memoryThreshold) {
671
- if (process.env.NODE_ENV === "development") {
672
- console.warn("Memory usage high, cleaning up...");
673
- }
674
- this.cleanupMemory();
675
- }
676
- }
677
- }
678
- /**
679
- * 메모리 정리
680
- */
681
- cleanupMemory() {
682
- if ("gc" in window) {
683
- try {
684
- window.gc();
685
- } catch (error) {
686
- }
687
- }
688
- this.cleanupOldLayers();
689
- }
690
- /**
691
- * 성능 최적화 설정 업데이트
692
- */
693
- updateConfig(newConfig) {
694
- this.config = { ...this.config, ...newConfig };
695
- if (!this.config.enableGPUAcceleration) {
696
- this.disableAllGPUAcceleration();
697
- }
698
- if (!this.config.enableLayerSeparation) {
699
- this.disableAllLayers();
700
- }
701
- }
702
- /**
703
- * 모든 GPU 가속 비활성화
704
- */
705
- disableAllGPUAcceleration() {
706
- this.layerRegistry.forEach((element) => {
707
- element.style.willChange = "auto";
708
- element.style.transform = "";
709
- });
710
- }
711
- /**
712
- * 모든 레이어 비활성화
713
- */
714
- disableAllLayers() {
715
- this.layerRegistry.forEach((element) => {
716
- this.removeLayer(element);
717
- });
718
- }
719
- /**
720
- * 성능 메트릭 가져오기
721
- */
722
- getMetrics() {
723
- return { ...this.metrics };
724
- }
725
- /**
726
- * 성능 모니터링 시작
727
- */
728
- startMonitoring() {
729
- if (this.isMonitoring) return;
730
- this.isMonitoring = true;
731
- this.monitoringInterval = setInterval(() => {
732
- this.updateMetrics();
733
- }, 1e3);
734
- }
735
- /**
736
- * 성능 모니터링 중지
737
- */
738
- stopMonitoring() {
739
- if (!this.isMonitoring) return;
740
- this.isMonitoring = false;
741
- if (this.monitoringInterval) {
742
- clearInterval(this.monitoringInterval);
743
- this.monitoringInterval = void 0;
744
- }
745
- }
746
- /**
747
- * 메트릭 업데이트
748
- */
749
- updateMetrics() {
750
- if ("memory" in performance) {
751
- const memory = performance.memory;
752
- this.metrics.memoryUsage = memory.usedJSHeapSize;
753
- }
754
- this.metrics.layerCount = this.layerRegistry.size;
755
- }
756
- /**
757
- * 성능 리포트 생성
758
- */
759
- generateReport() {
760
- const metrics = this.getMetrics();
761
- return `
762
- === HUA Motion Performance Report ===
763
- FPS: ${metrics.fps}
764
- Active Layers: ${metrics.layerCount}
765
- Memory Usage: ${this.formatBytes(metrics.memoryUsage || 0)}
766
- Active Motions: ${metrics.activeMotions}
767
- GPU Acceleration: ${this.config.enableGPUAcceleration ? "Enabled" : "Disabled"}
768
- Layer Separation: ${this.config.enableLayerSeparation ? "Enabled" : "Disabled"}
769
- Memory Optimization: ${this.config.enableMemoryOptimization ? "Enabled" : "Disabled"}
770
- =====================================
771
- `.trim();
772
- }
773
- /**
774
- * 바이트 단위 포맷팅
775
- */
776
- formatBytes(bytes) {
777
- if (bytes === 0) return "0 B";
778
- const k = 1024;
779
- const sizes = ["B", "KB", "MB", "GB"];
780
- const i = Math.floor(Math.log(bytes) / Math.log(k));
781
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
782
- }
783
- /**
784
- * 성능 최적화 권장사항
785
- */
786
- getOptimizationRecommendations() {
787
- const recommendations = [];
788
- const metrics = this.getMetrics();
789
- if (metrics.fps < this.config.targetFPS) {
790
- 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.");
791
- }
792
- if (metrics.layerCount > this.config.maxLayerCount * 0.8) {
793
- recommendations.push("\uB808\uC774\uC5B4 \uC218\uAC00 \uB9CE\uC2B5\uB2C8\uB2E4. \uBD88\uD544\uC694\uD55C \uB808\uC774\uC5B4\uB97C \uC815\uB9AC\uD558\uC138\uC694.");
794
- }
795
- if (metrics.memoryUsage && metrics.memoryUsage > this.config.memoryThreshold * 0.8) {
796
- recommendations.push("\uBA54\uBAA8\uB9AC \uC0AC\uC6A9\uB7C9\uC774 \uB192\uC2B5\uB2C8\uB2E4. \uBA54\uBAA8\uB9AC \uC815\uB9AC\uB97C \uACE0\uB824\uD558\uC138\uC694.");
797
- }
798
- return recommendations;
799
- }
800
- /**
801
- * 정리
802
- */
803
- destroy() {
804
- this.stopMonitoring();
805
- if (this.performanceObserver) {
806
- this.performanceObserver.disconnect();
807
- this.performanceObserver = null;
808
- }
809
- this.layerRegistry.forEach((element) => {
810
- this.removeLayer(element);
811
- });
812
- this.layerRegistry.clear();
813
- }
814
- };
815
- var performanceOptimizer = PerformanceOptimizer.getInstance();
816
-
817
- // src/presets/index.ts
818
- var MOTION_PRESETS = {
819
- hero: {
820
- entrance: "fadeIn",
821
- delay: 200,
822
- duration: 800,
823
- hover: false,
824
- click: false
825
- },
826
- title: {
827
- entrance: "slideUp",
828
- delay: 400,
829
- duration: 700,
830
- hover: false,
831
- click: false
832
- },
833
- button: {
834
- entrance: "scaleIn",
835
- delay: 600,
836
- duration: 300,
837
- hover: true,
838
- click: true
839
- },
840
- card: {
841
- entrance: "slideUp",
842
- delay: 800,
843
- duration: 500,
844
- hover: true,
845
- click: false
846
- },
847
- text: {
848
- entrance: "fadeIn",
849
- delay: 200,
850
- duration: 600,
851
- hover: false,
852
- click: false
853
- },
854
- image: {
855
- entrance: "scaleIn",
856
- delay: 400,
857
- duration: 600,
858
- hover: true,
859
- click: false
860
- }
861
- };
862
- var PAGE_MOTIONS = {
863
- // 홈페이지
864
- home: {
865
- hero: { type: "hero" },
866
- title: { type: "title" },
867
- description: { type: "text" },
868
- cta: { type: "button" },
869
- feature1: { type: "card" },
870
- feature2: { type: "card" },
871
- feature3: { type: "card" }
872
- },
873
- // 대시보드
874
- dashboard: {
875
- header: { type: "hero" },
876
- sidebar: { type: "card", entrance: "slideLeft" },
877
- main: { type: "text", entrance: "fadeIn" },
878
- card1: { type: "card" },
879
- card2: { type: "card" },
880
- card3: { type: "card" },
881
- chart: { type: "image" }
882
- },
883
- // 제품 페이지
884
- product: {
885
- hero: { type: "hero" },
886
- title: { type: "title" },
887
- image: { type: "image" },
888
- description: { type: "text" },
889
- price: { type: "text" },
890
- buyButton: { type: "button" },
891
- features: { type: "card" }
892
- },
893
- // 블로그
894
- blog: {
895
- header: { type: "hero" },
896
- title: { type: "title" },
897
- content: { type: "text" },
898
- sidebar: { type: "card", entrance: "slideRight" },
899
- related1: { type: "card" },
900
- related2: { type: "card" },
901
- related3: { type: "card" }
500
+ };
501
+ var PAGE_MOTIONS = {
502
+ // 홈페이지
503
+ home: {
504
+ hero: { type: "hero" },
505
+ title: { type: "title" },
506
+ description: { type: "text" },
507
+ cta: { type: "button" },
508
+ feature1: { type: "card" },
509
+ feature2: { type: "card" },
510
+ feature3: { type: "card" }
511
+ },
512
+ // 대시보드
513
+ dashboard: {
514
+ header: { type: "hero" },
515
+ sidebar: { type: "card", entrance: "slideLeft" },
516
+ main: { type: "text", entrance: "fadeIn" },
517
+ card1: { type: "card" },
518
+ card2: { type: "card" },
519
+ card3: { type: "card" },
520
+ chart: { type: "image" }
521
+ },
522
+ // 제품 페이지
523
+ product: {
524
+ hero: { type: "hero" },
525
+ title: { type: "title" },
526
+ image: { type: "image" },
527
+ description: { type: "text" },
528
+ price: { type: "text" },
529
+ buyButton: { type: "button" },
530
+ features: { type: "card" }
531
+ },
532
+ // 블로그
533
+ blog: {
534
+ header: { type: "hero" },
535
+ title: { type: "title" },
536
+ content: { type: "text" },
537
+ sidebar: { type: "card", entrance: "slideRight" },
538
+ related1: { type: "card" },
539
+ related2: { type: "card" },
540
+ related3: { type: "card" }
902
541
  }
903
542
  };
904
543
  function mergeWithPreset(preset, custom = {}) {
@@ -1660,39 +1299,261 @@ function useSmartMotion(options = {}) {
1660
1299
  isClicked: state.isClicked
1661
1300
  };
1662
1301
  }
1663
- function getInitialStyle(type, distance) {
1664
- switch (type) {
1665
- case "slideUp":
1666
- return { opacity: 0, transform: `translateY(${distance}px)` };
1667
- case "slideLeft":
1668
- return { opacity: 0, transform: `translateX(${distance}px)` };
1669
- case "slideRight":
1670
- return { opacity: 0, transform: `translateX(-${distance}px)` };
1671
- case "scaleIn":
1672
- return { opacity: 0, transform: "scale(0)" };
1673
- case "bounceIn":
1674
- return { opacity: 0, transform: "scale(0)" };
1675
- case "fadeIn":
1676
- default:
1677
- return { opacity: 0, transform: "none" };
1302
+
1303
+ // src/profiles/neutral.ts
1304
+ var neutral = {
1305
+ name: "neutral",
1306
+ base: {
1307
+ duration: 700,
1308
+ easing: "ease-out",
1309
+ threshold: 0.1,
1310
+ triggerOnce: true
1311
+ },
1312
+ entrance: {
1313
+ slide: {
1314
+ distance: 32,
1315
+ easing: "ease-out"
1316
+ },
1317
+ fade: {
1318
+ initialOpacity: 0
1319
+ },
1320
+ scale: {
1321
+ from: 0.95
1322
+ },
1323
+ bounce: {
1324
+ intensity: 0.3,
1325
+ easing: "cubic-bezier(0.34, 1.56, 0.64, 1)"
1326
+ }
1327
+ },
1328
+ stagger: {
1329
+ perItem: 100,
1330
+ baseDelay: 0
1331
+ },
1332
+ interaction: {
1333
+ hover: {
1334
+ scale: 1.05,
1335
+ y: -2,
1336
+ duration: 200,
1337
+ easing: "ease-out"
1338
+ }
1339
+ },
1340
+ spring: {
1341
+ mass: 1,
1342
+ stiffness: 100,
1343
+ damping: 10,
1344
+ restDelta: 0.01,
1345
+ restSpeed: 0.01
1346
+ },
1347
+ reducedMotion: "fade-only"
1348
+ };
1349
+
1350
+ // src/profiles/hua.ts
1351
+ var hua = {
1352
+ name: "hua",
1353
+ base: {
1354
+ duration: 640,
1355
+ easing: "cubic-bezier(0.22, 0.68, 0.35, 1.10)",
1356
+ threshold: 0.12,
1357
+ triggerOnce: true
1358
+ },
1359
+ entrance: {
1360
+ slide: {
1361
+ distance: 28,
1362
+ easing: "cubic-bezier(0.22, 0.68, 0.35, 1.14)"
1363
+ },
1364
+ fade: {
1365
+ initialOpacity: 0
1366
+ },
1367
+ scale: {
1368
+ from: 0.97
1369
+ },
1370
+ bounce: {
1371
+ intensity: 0.2,
1372
+ easing: "cubic-bezier(0.22, 0.68, 0.35, 1.12)"
1373
+ }
1374
+ },
1375
+ stagger: {
1376
+ perItem: 80,
1377
+ baseDelay: 0
1378
+ },
1379
+ interaction: {
1380
+ hover: {
1381
+ scale: 1.008,
1382
+ y: -1,
1383
+ duration: 180,
1384
+ easing: "cubic-bezier(0.22, 0.68, 0.35, 1.10)"
1385
+ }
1386
+ },
1387
+ spring: {
1388
+ mass: 1,
1389
+ stiffness: 180,
1390
+ damping: 18,
1391
+ restDelta: 5e-3,
1392
+ restSpeed: 5e-3
1393
+ },
1394
+ reducedMotion: "fade-only"
1395
+ };
1396
+
1397
+ // src/profiles/index.ts
1398
+ var PROFILES = {
1399
+ neutral,
1400
+ hua
1401
+ };
1402
+ function resolveProfile(profile) {
1403
+ if (typeof profile === "string") {
1404
+ return PROFILES[profile] ?? neutral;
1678
1405
  }
1406
+ return profile;
1679
1407
  }
1680
- function getVisibleStyle() {
1681
- return { opacity: 1, transform: "none" };
1682
- }
1683
- function getEasingForType(type, easing2) {
1684
- if (easing2) return easing2;
1685
- if (type === "bounceIn") return "cubic-bezier(0.34, 1.56, 0.64, 1)";
1686
- return "ease-out";
1408
+ function mergeProfileOverrides(base, overrides) {
1409
+ return deepMerge(
1410
+ base,
1411
+ overrides
1412
+ );
1687
1413
  }
1688
- function getMultiEffectInitialStyle(effects, defaultDistance) {
1689
- const style = {};
1690
- const transforms = [];
1691
- if (effects.fade) {
1692
- style.opacity = 0;
1414
+ function deepMerge(target, source) {
1415
+ const result = { ...target };
1416
+ for (const key of Object.keys(source)) {
1417
+ const sourceVal = source[key];
1418
+ const targetVal = result[key];
1419
+ if (sourceVal !== null && sourceVal !== void 0 && typeof sourceVal === "object" && !Array.isArray(sourceVal) && typeof targetVal === "object" && targetVal !== null && !Array.isArray(targetVal)) {
1420
+ result[key] = deepMerge(
1421
+ targetVal,
1422
+ sourceVal
1423
+ );
1424
+ } else if (sourceVal !== void 0) {
1425
+ result[key] = sourceVal;
1426
+ }
1693
1427
  }
1694
- if (effects.slide) {
1695
- const config = typeof effects.slide === "object" ? effects.slide : {};
1428
+ return result;
1429
+ }
1430
+
1431
+ // src/profiles/MotionProfileContext.ts
1432
+ var MotionProfileContext = createContext(neutral);
1433
+ function MotionProfileProvider({
1434
+ profile = "neutral",
1435
+ overrides,
1436
+ children
1437
+ }) {
1438
+ const resolved = useMemo(() => {
1439
+ const base = resolveProfile(profile);
1440
+ return overrides ? mergeProfileOverrides(base, overrides) : base;
1441
+ }, [profile, overrides]);
1442
+ return createElement(
1443
+ MotionProfileContext.Provider,
1444
+ { value: resolved },
1445
+ children
1446
+ );
1447
+ }
1448
+ function useMotionProfile() {
1449
+ return useContext(MotionProfileContext);
1450
+ }
1451
+
1452
+ // src/utils/sharedIntersectionObserver.ts
1453
+ var pool = /* @__PURE__ */ new Map();
1454
+ function makeKey(threshold, rootMargin) {
1455
+ const th = Array.isArray(threshold) ? threshold.join(",") : String(threshold);
1456
+ return `${th}|${rootMargin}`;
1457
+ }
1458
+ function handleIntersections(group, entries) {
1459
+ for (const entry of entries) {
1460
+ const subs = group.elements.get(entry.target);
1461
+ if (!subs) continue;
1462
+ const snapshot = [...subs];
1463
+ for (const sub of snapshot) {
1464
+ sub.callback(entry);
1465
+ if (sub.once && entry.isIntersecting) {
1466
+ removeSub(group, entry.target, sub);
1467
+ }
1468
+ }
1469
+ }
1470
+ }
1471
+ function removeSub(group, element, sub) {
1472
+ const subs = group.elements.get(element);
1473
+ if (!subs) return;
1474
+ const idx = subs.indexOf(sub);
1475
+ if (idx >= 0) subs.splice(idx, 1);
1476
+ if (subs.length === 0) {
1477
+ group.elements.delete(element);
1478
+ group.observer.unobserve(element);
1479
+ if (group.elements.size === 0) {
1480
+ group.observer.disconnect();
1481
+ for (const [key, g] of pool) {
1482
+ if (g === group) {
1483
+ pool.delete(key);
1484
+ break;
1485
+ }
1486
+ }
1487
+ }
1488
+ }
1489
+ }
1490
+ function observeElement(element, callback, options, once) {
1491
+ if (typeof IntersectionObserver === "undefined") {
1492
+ return () => {
1493
+ };
1494
+ }
1495
+ const threshold = options?.threshold ?? 0;
1496
+ const rootMargin = options?.rootMargin ?? "0px";
1497
+ const key = makeKey(threshold, rootMargin);
1498
+ let group = pool.get(key);
1499
+ if (!group) {
1500
+ const observer = new IntersectionObserver(
1501
+ (entries) => handleIntersections(group, entries),
1502
+ { threshold, rootMargin }
1503
+ );
1504
+ group = { observer, elements: /* @__PURE__ */ new Map() };
1505
+ pool.set(key, group);
1506
+ }
1507
+ const sub = { callback, once: once ?? false };
1508
+ const subs = group.elements.get(element);
1509
+ if (subs) {
1510
+ subs.push(sub);
1511
+ } else {
1512
+ group.elements.set(element, [sub]);
1513
+ group.observer.observe(element);
1514
+ }
1515
+ let unsubscribed = false;
1516
+ return () => {
1517
+ if (unsubscribed) return;
1518
+ unsubscribed = true;
1519
+ removeSub(group, element, sub);
1520
+ };
1521
+ }
1522
+
1523
+ // src/hooks/useUnifiedMotion.ts
1524
+ function getInitialStyle(type, distance) {
1525
+ switch (type) {
1526
+ case "slideUp":
1527
+ return { opacity: 0, transform: `translateY(${distance}px)` };
1528
+ case "slideLeft":
1529
+ return { opacity: 0, transform: `translateX(${distance}px)` };
1530
+ case "slideRight":
1531
+ return { opacity: 0, transform: `translateX(-${distance}px)` };
1532
+ case "scaleIn":
1533
+ return { opacity: 0, transform: "scale(0)" };
1534
+ case "bounceIn":
1535
+ return { opacity: 0, transform: "scale(0)" };
1536
+ case "fadeIn":
1537
+ default:
1538
+ return { opacity: 0, transform: "none" };
1539
+ }
1540
+ }
1541
+ function getVisibleStyle() {
1542
+ return { opacity: 1, transform: "none" };
1543
+ }
1544
+ function getEasingForType(type, easing2) {
1545
+ if (easing2) return easing2;
1546
+ if (type === "bounceIn") return "cubic-bezier(0.34, 1.56, 0.64, 1)";
1547
+ return "ease-out";
1548
+ }
1549
+ function getMultiEffectInitialStyle(effects, defaultDistance) {
1550
+ const style = {};
1551
+ const transforms = [];
1552
+ if (effects.fade) {
1553
+ style.opacity = 0;
1554
+ }
1555
+ if (effects.slide) {
1556
+ const config = typeof effects.slide === "object" ? effects.slide : {};
1696
1557
  const direction = config.direction ?? "up";
1697
1558
  const distance = config.distance ?? defaultDistance;
1698
1559
  switch (direction) {
@@ -1753,16 +1614,17 @@ function getMultiEffectEasing(effects, easing2) {
1753
1614
  return "ease-out";
1754
1615
  }
1755
1616
  function useUnifiedMotion(options) {
1617
+ const profile = useMotionProfile();
1756
1618
  const {
1757
1619
  type,
1758
1620
  effects,
1759
- duration = 600,
1621
+ duration = profile.base.duration,
1760
1622
  autoStart = true,
1761
1623
  delay = 0,
1762
1624
  easing: easing2,
1763
- threshold = 0.1,
1764
- triggerOnce = true,
1765
- distance = 50,
1625
+ threshold = profile.base.threshold,
1626
+ triggerOnce = profile.base.triggerOnce,
1627
+ distance = profile.entrance.slide.distance,
1766
1628
  onComplete,
1767
1629
  onStart,
1768
1630
  onStop,
@@ -1774,7 +1636,6 @@ function useUnifiedMotion(options) {
1774
1636
  const [isVisible, setIsVisible] = useState(false);
1775
1637
  const [isAnimating, setIsAnimating] = useState(false);
1776
1638
  const [progress, setProgress] = useState(0);
1777
- const observerRef = useRef(null);
1778
1639
  const timeoutRef = useRef(null);
1779
1640
  const startRef = useRef(() => {
1780
1641
  });
@@ -1807,23 +1668,14 @@ function useUnifiedMotion(options) {
1807
1668
  }, [stop, onReset]);
1808
1669
  useEffect(() => {
1809
1670
  if (!ref.current || !autoStart) return;
1810
- observerRef.current = new IntersectionObserver(
1811
- (entries) => {
1812
- entries.forEach((entry) => {
1813
- if (entry.isIntersecting) {
1814
- startRef.current();
1815
- if (triggerOnce) {
1816
- observerRef.current?.disconnect();
1817
- }
1818
- }
1819
- });
1671
+ return observeElement(
1672
+ ref.current,
1673
+ (entry) => {
1674
+ if (entry.isIntersecting) startRef.current();
1820
1675
  },
1821
- { threshold }
1676
+ { threshold },
1677
+ triggerOnce
1822
1678
  );
1823
- observerRef.current.observe(ref.current);
1824
- return () => {
1825
- observerRef.current?.disconnect();
1826
- };
1827
1679
  }, [autoStart, threshold, triggerOnce]);
1828
1680
  useEffect(() => {
1829
1681
  return () => stop();
@@ -1863,14 +1715,15 @@ function useUnifiedMotion(options) {
1863
1715
  };
1864
1716
  }
1865
1717
  function useFadeIn(options = {}) {
1718
+ const profile = useMotionProfile();
1866
1719
  const {
1867
1720
  delay = 0,
1868
- duration = 700,
1869
- threshold = 0.1,
1870
- triggerOnce = true,
1871
- easing: easing2 = "ease-out",
1721
+ duration = profile.base.duration,
1722
+ threshold = profile.base.threshold,
1723
+ triggerOnce = profile.base.triggerOnce,
1724
+ easing: easing2 = profile.base.easing,
1872
1725
  autoStart = true,
1873
- initialOpacity = 0,
1726
+ initialOpacity = profile.entrance.fade.initialOpacity,
1874
1727
  targetOpacity = 1,
1875
1728
  onComplete,
1876
1729
  onStart,
@@ -1882,7 +1735,6 @@ function useFadeIn(options = {}) {
1882
1735
  const [isAnimating, setIsAnimating] = useState(false);
1883
1736
  const [progress, setProgress] = useState(0);
1884
1737
  const [nodeReady, setNodeReady] = useState(false);
1885
- const observerRef = useRef(null);
1886
1738
  const motionRef = useRef(null);
1887
1739
  const timeoutRef = useRef(null);
1888
1740
  const startRef = useRef(() => {
@@ -1941,31 +1793,15 @@ function useFadeIn(options = {}) {
1941
1793
  }, [stop, onReset]);
1942
1794
  useEffect(() => {
1943
1795
  if (!ref.current || !autoStart) return;
1944
- observerRef.current = new IntersectionObserver(
1945
- (entries) => {
1946
- entries.forEach((entry) => {
1947
- if (entry.isIntersecting) {
1948
- startRef.current();
1949
- if (triggerOnce) {
1950
- observerRef.current?.disconnect();
1951
- }
1952
- }
1953
- });
1796
+ return observeElement(
1797
+ ref.current,
1798
+ (entry) => {
1799
+ if (entry.isIntersecting) startRef.current();
1954
1800
  },
1955
- { threshold }
1801
+ { threshold },
1802
+ triggerOnce
1956
1803
  );
1957
- observerRef.current.observe(ref.current);
1958
- return () => {
1959
- if (observerRef.current) {
1960
- observerRef.current.disconnect();
1961
- }
1962
- };
1963
1804
  }, [autoStart, threshold, triggerOnce, nodeReady]);
1964
- useEffect(() => {
1965
- if (!autoStart) {
1966
- start();
1967
- }
1968
- }, [autoStart, start]);
1969
1805
  useEffect(() => {
1970
1806
  return () => {
1971
1807
  stop();
@@ -1991,15 +1827,16 @@ function useFadeIn(options = {}) {
1991
1827
  };
1992
1828
  }
1993
1829
  function useSlideUp(options = {}) {
1830
+ const profile = useMotionProfile();
1994
1831
  const {
1995
1832
  delay = 0,
1996
- duration = 700,
1997
- threshold = 0.1,
1998
- triggerOnce = true,
1999
- easing: easing2 = "ease-out",
1833
+ duration = profile.base.duration,
1834
+ threshold = profile.base.threshold,
1835
+ triggerOnce = profile.base.triggerOnce,
1836
+ easing: easing2 = profile.entrance.slide.easing,
2000
1837
  autoStart = true,
2001
1838
  direction = "up",
2002
- distance = 50,
1839
+ distance = profile.entrance.slide.distance,
2003
1840
  onComplete,
2004
1841
  onStart,
2005
1842
  onStop,
@@ -2010,7 +1847,6 @@ function useSlideUp(options = {}) {
2010
1847
  const [isAnimating, setIsAnimating] = useState(false);
2011
1848
  const [progress, setProgress] = useState(0);
2012
1849
  const [nodeReady, setNodeReady] = useState(false);
2013
- const observerRef = useRef(null);
2014
1850
  const timeoutRef = useRef(null);
2015
1851
  const startRef = useRef(() => {
2016
1852
  });
@@ -2078,31 +1914,15 @@ function useSlideUp(options = {}) {
2078
1914
  }, [stop, onReset]);
2079
1915
  useEffect(() => {
2080
1916
  if (!ref.current || !autoStart) return;
2081
- observerRef.current = new IntersectionObserver(
2082
- (entries) => {
2083
- entries.forEach((entry) => {
2084
- if (entry.isIntersecting) {
2085
- startRef.current();
2086
- if (triggerOnce) {
2087
- observerRef.current?.disconnect();
2088
- }
2089
- }
2090
- });
1917
+ return observeElement(
1918
+ ref.current,
1919
+ (entry) => {
1920
+ if (entry.isIntersecting) startRef.current();
2091
1921
  },
2092
- { threshold }
1922
+ { threshold },
1923
+ triggerOnce
2093
1924
  );
2094
- observerRef.current.observe(ref.current);
2095
- return () => {
2096
- if (observerRef.current) {
2097
- observerRef.current.disconnect();
2098
- }
2099
- };
2100
1925
  }, [autoStart, threshold, triggerOnce, nodeReady]);
2101
- useEffect(() => {
2102
- if (!autoStart) {
2103
- start();
2104
- }
2105
- }, [autoStart, start]);
2106
1926
  useEffect(() => {
2107
1927
  return () => {
2108
1928
  stop();
@@ -2145,15 +1965,16 @@ function useSlideRight(options = {}) {
2145
1965
  return useSlideUp({ ...options, direction: "right" });
2146
1966
  }
2147
1967
  function useScaleIn(options = {}) {
1968
+ const profile = useMotionProfile();
2148
1969
  const {
2149
1970
  initialScale = 0,
2150
1971
  targetScale = 1,
2151
- duration = 700,
1972
+ duration = profile.base.duration,
2152
1973
  delay = 0,
2153
1974
  autoStart = true,
2154
- easing: easing2 = "ease-out",
2155
- threshold = 0.1,
2156
- triggerOnce = true,
1975
+ easing: easing2 = profile.base.easing,
1976
+ threshold = profile.base.threshold,
1977
+ triggerOnce = profile.base.triggerOnce,
2157
1978
  onComplete,
2158
1979
  onStart,
2159
1980
  onStop,
@@ -2165,7 +1986,6 @@ function useScaleIn(options = {}) {
2165
1986
  const [isAnimating, setIsAnimating] = useState(false);
2166
1987
  const [isVisible, setIsVisible] = useState(autoStart ? false : true);
2167
1988
  const [progress, setProgress] = useState(autoStart ? 0 : 1);
2168
- const observerRef = useRef(null);
2169
1989
  const timeoutRef = useRef(null);
2170
1990
  const startRef = useRef(() => {
2171
1991
  });
@@ -2213,25 +2033,14 @@ function useScaleIn(options = {}) {
2213
2033
  }, [stop, initialScale, onReset]);
2214
2034
  useEffect(() => {
2215
2035
  if (!ref.current || !autoStart) return;
2216
- observerRef.current = new IntersectionObserver(
2217
- (entries) => {
2218
- entries.forEach((entry) => {
2219
- if (entry.isIntersecting) {
2220
- startRef.current();
2221
- if (triggerOnce) {
2222
- observerRef.current?.disconnect();
2223
- }
2224
- }
2225
- });
2036
+ return observeElement(
2037
+ ref.current,
2038
+ (entry) => {
2039
+ if (entry.isIntersecting) startRef.current();
2226
2040
  },
2227
- { threshold }
2041
+ { threshold },
2042
+ triggerOnce
2228
2043
  );
2229
- observerRef.current.observe(ref.current);
2230
- return () => {
2231
- if (observerRef.current) {
2232
- observerRef.current.disconnect();
2233
- }
2234
- };
2235
2044
  }, [autoStart, threshold, triggerOnce]);
2236
2045
  useEffect(() => {
2237
2046
  return () => {
@@ -2259,15 +2068,15 @@ function useScaleIn(options = {}) {
2259
2068
  };
2260
2069
  }
2261
2070
  function useBounceIn(options = {}) {
2071
+ const profile = useMotionProfile();
2262
2072
  const {
2263
2073
  duration = 600,
2264
2074
  delay = 0,
2265
2075
  autoStart = true,
2266
- intensity = 0.3,
2267
- threshold = 0.1,
2268
- triggerOnce = true,
2269
- easing: easing2 = "cubic-bezier(0.34, 1.56, 0.64, 1)",
2270
- // 바운스 이징
2076
+ intensity = profile.entrance.bounce.intensity,
2077
+ threshold = profile.base.threshold,
2078
+ triggerOnce = profile.base.triggerOnce,
2079
+ easing: easing2 = profile.entrance.bounce.easing,
2271
2080
  onComplete,
2272
2081
  onStart,
2273
2082
  onStop,
@@ -2279,7 +2088,6 @@ function useBounceIn(options = {}) {
2279
2088
  const [isAnimating, setIsAnimating] = useState(false);
2280
2089
  const [isVisible, setIsVisible] = useState(autoStart ? false : true);
2281
2090
  const [progress, setProgress] = useState(autoStart ? 0 : 1);
2282
- const observerRef = useRef(null);
2283
2091
  const timeoutRef = useRef(null);
2284
2092
  const bounceTimeoutRef = useRef(null);
2285
2093
  const startRef = useRef(() => {
@@ -2336,25 +2144,14 @@ function useBounceIn(options = {}) {
2336
2144
  }, [stop, onReset]);
2337
2145
  useEffect(() => {
2338
2146
  if (!ref.current || !autoStart) return;
2339
- observerRef.current = new IntersectionObserver(
2340
- (entries) => {
2341
- entries.forEach((entry) => {
2342
- if (entry.isIntersecting) {
2343
- startRef.current();
2344
- if (triggerOnce) {
2345
- observerRef.current?.disconnect();
2346
- }
2347
- }
2348
- });
2147
+ return observeElement(
2148
+ ref.current,
2149
+ (entry) => {
2150
+ if (entry.isIntersecting) startRef.current();
2349
2151
  },
2350
- { threshold }
2152
+ { threshold },
2153
+ triggerOnce
2351
2154
  );
2352
- observerRef.current.observe(ref.current);
2353
- return () => {
2354
- if (observerRef.current) {
2355
- observerRef.current.disconnect();
2356
- }
2357
- };
2358
2155
  }, [autoStart, threshold, triggerOnce]);
2359
2156
  useEffect(() => {
2360
2157
  return () => {
@@ -2679,18 +2476,33 @@ function usePulse(options = {}) {
2679
2476
  reset
2680
2477
  };
2681
2478
  }
2479
+
2480
+ // src/utils/springPhysics.ts
2481
+ function calculateSpring(currentValue, currentVelocity, targetValue, deltaTime, config) {
2482
+ const { stiffness, damping, mass } = config;
2483
+ const displacement = currentValue - targetValue;
2484
+ const springForce = -stiffness * displacement;
2485
+ const dampingForce = -damping * currentVelocity;
2486
+ const acceleration = (springForce + dampingForce) / mass;
2487
+ const newVelocity = currentVelocity + acceleration * deltaTime;
2488
+ const newValue = currentValue + newVelocity * deltaTime;
2489
+ return { value: newValue, velocity: newVelocity };
2490
+ }
2491
+
2492
+ // src/hooks/useSpringMotion.ts
2682
2493
  function useSpringMotion(options) {
2494
+ const profile = useMotionProfile();
2683
2495
  const {
2684
2496
  from,
2685
2497
  to,
2686
- mass = 1,
2687
- stiffness = 100,
2688
- damping = 10,
2689
- restDelta = 0.01,
2690
- restSpeed = 0.01,
2498
+ mass = profile.spring.mass,
2499
+ stiffness = profile.spring.stiffness,
2500
+ damping = profile.spring.damping,
2501
+ restDelta = profile.spring.restDelta,
2502
+ restSpeed = profile.spring.restSpeed,
2691
2503
  onComplete,
2692
2504
  enabled = true,
2693
- autoStart = false
2505
+ autoStart = true
2694
2506
  } = options;
2695
2507
  const ref = useRef(null);
2696
2508
  const [springState, setSpringState] = useState({
@@ -2702,16 +2514,7 @@ function useSpringMotion(options) {
2702
2514
  const [progress, setProgress] = useState(0);
2703
2515
  const motionRef = useRef(null);
2704
2516
  const lastTimeRef = useRef(0);
2705
- const calculateSpring = useCallback((currentValue, currentVelocity, targetValue, deltaTime) => {
2706
- const displacement = currentValue - targetValue;
2707
- const springForce = -stiffness * displacement;
2708
- const dampingForce = -damping * currentVelocity;
2709
- const totalForce = springForce + dampingForce;
2710
- const acceleration = totalForce / mass;
2711
- const newVelocity = currentVelocity + acceleration * deltaTime;
2712
- const newValue = currentValue + newVelocity * deltaTime;
2713
- return { value: newValue, velocity: newVelocity };
2714
- }, [mass, stiffness, damping]);
2517
+ const springConfig = useMemo(() => ({ stiffness, damping, mass }), [stiffness, damping, mass]);
2715
2518
  const animate = useCallback((currentTime) => {
2716
2519
  if (!enabled || !springState.isAnimating) return;
2717
2520
  const deltaTime = Math.min(currentTime - lastTimeRef.current, 16) / 1e3;
@@ -2720,7 +2523,8 @@ function useSpringMotion(options) {
2720
2523
  springState.value,
2721
2524
  springState.velocity,
2722
2525
  to,
2723
- deltaTime
2526
+ deltaTime,
2527
+ springConfig
2724
2528
  );
2725
2529
  const range = Math.abs(to - from);
2726
2530
  const currentProgress = range > 0 ? Math.min(Math.abs(value - from) / range, 1) : 1;
@@ -2742,7 +2546,7 @@ function useSpringMotion(options) {
2742
2546
  isAnimating: true
2743
2547
  });
2744
2548
  motionRef.current = requestAnimationFrame(animate);
2745
- }, [enabled, springState.isAnimating, to, from, restDelta, restSpeed, onComplete, calculateSpring]);
2549
+ }, [enabled, springState.isAnimating, to, from, restDelta, restSpeed, onComplete, springConfig]);
2746
2550
  const start = useCallback(() => {
2747
2551
  if (springState.isAnimating) return;
2748
2552
  setSpringState((prev) => ({
@@ -2890,11 +2694,12 @@ function useGradient(options = {}) {
2890
2694
  };
2891
2695
  }
2892
2696
  function useHoverMotion(options = {}) {
2697
+ const profile = useMotionProfile();
2893
2698
  const {
2894
- duration = 200,
2895
- easing: easing2 = "ease-out",
2896
- hoverScale = 1.05,
2897
- hoverY = -2,
2699
+ duration = profile.interaction.hover.duration,
2700
+ easing: easing2 = profile.interaction.hover.easing,
2701
+ hoverScale = profile.interaction.hover.scale,
2702
+ hoverY = profile.interaction.hover.y,
2898
2703
  hoverOpacity = 1
2899
2704
  } = options;
2900
2705
  const ref = useRef(null);
@@ -3178,13 +2983,14 @@ function useFocusToggle(options = {}) {
3178
2983
  };
3179
2984
  }
3180
2985
  function useScrollReveal(options = {}) {
2986
+ const profile = useMotionProfile();
3181
2987
  const {
3182
- threshold = 0.1,
2988
+ threshold = profile.base.threshold,
3183
2989
  rootMargin = "0px",
3184
- triggerOnce = true,
2990
+ triggerOnce = profile.base.triggerOnce,
3185
2991
  delay = 0,
3186
- duration = 700,
3187
- easing: easing2 = "ease-out",
2992
+ duration = profile.base.duration,
2993
+ easing: easing2 = profile.base.easing,
3188
2994
  motionType = "fadeIn",
3189
2995
  onComplete,
3190
2996
  onStart,
@@ -3213,15 +3019,14 @@ function useScrollReveal(options = {}) {
3213
3019
  }, [triggerOnce, hasTriggered, delay, onStart, onComplete]);
3214
3020
  useEffect(() => {
3215
3021
  if (!ref.current) return;
3216
- const observer = new IntersectionObserver(observerCallback, {
3217
- threshold,
3218
- rootMargin
3219
- });
3220
- observer.observe(ref.current);
3221
- return () => {
3222
- observer.disconnect();
3223
- };
3022
+ return observeElement(
3023
+ ref.current,
3024
+ (entry) => observerCallback([entry]),
3025
+ { threshold, rootMargin }
3026
+ );
3224
3027
  }, [observerCallback, threshold, rootMargin]);
3028
+ const slideDistance = profile.entrance.slide.distance;
3029
+ const scaleFrom = profile.entrance.scale.from;
3225
3030
  const style = useMemo(() => {
3226
3031
  const baseTransition = `all ${duration}ms ${easing2}`;
3227
3032
  if (!isVisible) {
@@ -3234,25 +3039,25 @@ function useScrollReveal(options = {}) {
3234
3039
  case "slideUp":
3235
3040
  return {
3236
3041
  opacity: 0,
3237
- transform: "translateY(32px)",
3042
+ transform: `translateY(${slideDistance}px)`,
3238
3043
  transition: baseTransition
3239
3044
  };
3240
3045
  case "slideLeft":
3241
3046
  return {
3242
3047
  opacity: 0,
3243
- transform: "translateX(-32px)",
3048
+ transform: `translateX(-${slideDistance}px)`,
3244
3049
  transition: baseTransition
3245
3050
  };
3246
3051
  case "slideRight":
3247
3052
  return {
3248
3053
  opacity: 0,
3249
- transform: "translateX(32px)",
3054
+ transform: `translateX(${slideDistance}px)`,
3250
3055
  transition: baseTransition
3251
3056
  };
3252
3057
  case "scaleIn":
3253
3058
  return {
3254
3059
  opacity: 0,
3255
- transform: "scale(0.95)",
3060
+ transform: `scale(${scaleFrom})`,
3256
3061
  transition: baseTransition
3257
3062
  };
3258
3063
  case "bounceIn":
@@ -3273,7 +3078,7 @@ function useScrollReveal(options = {}) {
3273
3078
  transform: "none",
3274
3079
  transition: baseTransition
3275
3080
  };
3276
- }, [isVisible, motionType, duration, easing2]);
3081
+ }, [isVisible, motionType, duration, easing2, slideDistance, scaleFrom]);
3277
3082
  const start = useCallback(() => {
3278
3083
  setIsAnimating(true);
3279
3084
  onStart?.();
@@ -3306,6 +3111,38 @@ function useScrollReveal(options = {}) {
3306
3111
  stop
3307
3112
  };
3308
3113
  }
3114
+
3115
+ // src/utils/sharedScroll.ts
3116
+ var subscribers = /* @__PURE__ */ new Set();
3117
+ var listening = false;
3118
+ var rafId = 0;
3119
+ function tick() {
3120
+ subscribers.forEach((cb) => cb());
3121
+ }
3122
+ function onScroll() {
3123
+ cancelAnimationFrame(rafId);
3124
+ rafId = requestAnimationFrame(tick);
3125
+ }
3126
+ function subscribeScroll(cb) {
3127
+ subscribers.add(cb);
3128
+ if (!listening && subscribers.size > 0) {
3129
+ window.addEventListener("scroll", onScroll, { passive: true });
3130
+ window.addEventListener("resize", onScroll, { passive: true });
3131
+ listening = true;
3132
+ }
3133
+ return () => {
3134
+ subscribers.delete(cb);
3135
+ if (listening && subscribers.size === 0) {
3136
+ window.removeEventListener("scroll", onScroll);
3137
+ window.removeEventListener("resize", onScroll);
3138
+ cancelAnimationFrame(rafId);
3139
+ listening = false;
3140
+ }
3141
+ };
3142
+ }
3143
+
3144
+ // src/hooks/useScrollProgress.ts
3145
+ var PROGRESS_THRESHOLD = 0.1;
3309
3146
  function useScrollProgress(options = {}) {
3310
3147
  const {
3311
3148
  target,
@@ -3314,27 +3151,24 @@ function useScrollProgress(options = {}) {
3314
3151
  } = options;
3315
3152
  const [progress, setProgress] = useState(showOnMount ? 0 : 0);
3316
3153
  const [mounted, setMounted] = useState(false);
3154
+ const progressRef = useRef(progress);
3317
3155
  useEffect(() => {
3318
3156
  setMounted(true);
3319
3157
  }, []);
3320
3158
  useEffect(() => {
3321
3159
  if (!mounted) return;
3322
3160
  const calculateProgress = () => {
3323
- if (typeof window !== "undefined") {
3324
- const scrollTop = window.pageYOffset;
3325
- const scrollHeight = target || document.documentElement.scrollHeight - window.innerHeight;
3326
- const adjustedScrollTop = Math.max(0, scrollTop - offset);
3327
- const progressPercent = Math.min(100, Math.max(0, adjustedScrollTop / scrollHeight * 100));
3328
- setProgress(progressPercent);
3161
+ const scrollTop = window.pageYOffset;
3162
+ const scrollHeight = target || document.documentElement.scrollHeight - window.innerHeight;
3163
+ const adjustedScrollTop = Math.max(0, scrollTop - offset);
3164
+ const next = Math.min(100, Math.max(0, adjustedScrollTop / scrollHeight * 100));
3165
+ if (Math.abs(next - progressRef.current) > PROGRESS_THRESHOLD) {
3166
+ progressRef.current = next;
3167
+ setProgress(next);
3329
3168
  }
3330
3169
  };
3331
3170
  calculateProgress();
3332
- window.addEventListener("scroll", calculateProgress, { passive: true });
3333
- window.addEventListener("resize", calculateProgress, { passive: true });
3334
- return () => {
3335
- window.removeEventListener("scroll", calculateProgress);
3336
- window.removeEventListener("resize", calculateProgress);
3337
- };
3171
+ return subscribeScroll(calculateProgress);
3338
3172
  }, [target, offset, mounted]);
3339
3173
  return {
3340
3174
  progress,
@@ -3719,14 +3553,11 @@ function useInView(options = {}) {
3719
3553
  useEffect(() => {
3720
3554
  const element = ref.current;
3721
3555
  if (!element) return;
3722
- const observer = new IntersectionObserver(handleIntersect, {
3723
- threshold,
3724
- rootMargin
3725
- });
3726
- observer.observe(element);
3727
- return () => {
3728
- observer.disconnect();
3729
- };
3556
+ return observeElement(
3557
+ element,
3558
+ (entry2) => handleIntersect([entry2]),
3559
+ { threshold, rootMargin }
3560
+ );
3730
3561
  }, [threshold, rootMargin, handleIntersect]);
3731
3562
  return {
3732
3563
  ref,
@@ -4122,170 +3953,1432 @@ function useGesture(options = {}) {
4122
3953
  clearTimeout(longPressRef.current);
4123
3954
  }
4124
3955
  };
4125
- }, []);
3956
+ }, []);
3957
+ return {
3958
+ isActive,
3959
+ gesture,
3960
+ scale,
3961
+ rotation,
3962
+ deltaX,
3963
+ deltaY,
3964
+ distance,
3965
+ velocity,
3966
+ start,
3967
+ stop,
3968
+ reset,
3969
+ onTouchStart,
3970
+ onTouchMove,
3971
+ onTouchEnd,
3972
+ onMouseDown,
3973
+ onMouseMove,
3974
+ onMouseUp
3975
+ };
3976
+ }
3977
+ function useGestureMotion(options) {
3978
+ const {
3979
+ gestureType,
3980
+ duration = 300,
3981
+ easing: easing2 = "ease-out",
3982
+ sensitivity = 1,
3983
+ enabled = true,
3984
+ onGestureStart,
3985
+ onGestureEnd
3986
+ } = options;
3987
+ const elementRef = useRef(null);
3988
+ const [gestureState, setGestureState] = useState({
3989
+ isActive: false,
3990
+ x: 0,
3991
+ y: 0,
3992
+ deltaX: 0,
3993
+ deltaY: 0,
3994
+ scale: 1,
3995
+ rotation: 0
3996
+ });
3997
+ const [motionStyle, setMotionStyle] = useState({});
3998
+ const startPoint = useRef({ x: 0, y: 0 });
3999
+ const isDragging = useRef(false);
4000
+ const updateMotionStyle = useCallback(() => {
4001
+ if (!enabled) return;
4002
+ const { isActive, deltaX, deltaY, scale, rotation } = gestureState;
4003
+ let transform = "";
4004
+ switch (gestureType) {
4005
+ case "hover":
4006
+ transform = isActive ? `scale(${1 + sensitivity * 0.05}) translateY(-${sensitivity * 2}px)` : "scale(1) translateY(0)";
4007
+ break;
4008
+ case "drag":
4009
+ transform = isActive ? `translate(${deltaX * sensitivity}px, ${deltaY * sensitivity}px)` : "translate(0, 0)";
4010
+ break;
4011
+ case "pinch":
4012
+ transform = `scale(${scale})`;
4013
+ break;
4014
+ case "swipe":
4015
+ transform = isActive ? `translateX(${deltaX * sensitivity}px) rotateY(${deltaX * 0.1}deg)` : "translateX(0) rotateY(0)";
4016
+ break;
4017
+ case "tilt":
4018
+ transform = isActive ? `rotateX(${deltaY * 0.1}deg) rotateY(${deltaX * 0.1}deg)` : "rotateX(0) rotateY(0)";
4019
+ break;
4020
+ }
4021
+ setMotionStyle({
4022
+ transform,
4023
+ transition: isActive ? "none" : `all ${duration}ms ${easing2}`,
4024
+ cursor: gestureType === "drag" && isActive ? "grabbing" : "pointer"
4025
+ });
4026
+ }, [gestureState, gestureType, enabled, duration, easing2, sensitivity]);
4027
+ const handleMouseDown = useCallback((e) => {
4028
+ if (!enabled || gestureType !== "drag") return;
4029
+ isDragging.current = true;
4030
+ startPoint.current = { x: e.clientX, y: e.clientY };
4031
+ setGestureState((prev) => ({ ...prev, isActive: true }));
4032
+ onGestureStart?.();
4033
+ }, [enabled, gestureType, onGestureStart]);
4034
+ const handleMouseMove = useCallback((e) => {
4035
+ if (!enabled || !isDragging.current) return;
4036
+ const deltaX = e.clientX - startPoint.current.x;
4037
+ const deltaY = e.clientY - startPoint.current.y;
4038
+ setGestureState((prev) => ({
4039
+ ...prev,
4040
+ x: e.clientX,
4041
+ y: e.clientY,
4042
+ deltaX,
4043
+ deltaY
4044
+ }));
4045
+ }, [enabled]);
4046
+ const handleMouseUp = useCallback(() => {
4047
+ if (!enabled) return;
4048
+ isDragging.current = false;
4049
+ setGestureState((prev) => ({ ...prev, isActive: false }));
4050
+ onGestureEnd?.();
4051
+ }, [enabled, onGestureEnd]);
4052
+ const handleMouseEnter = useCallback(() => {
4053
+ if (!enabled || gestureType !== "hover") return;
4054
+ setGestureState((prev) => ({ ...prev, isActive: true }));
4055
+ onGestureStart?.();
4056
+ }, [enabled, gestureType, onGestureStart]);
4057
+ const handleMouseLeave = useCallback(() => {
4058
+ if (!enabled || gestureType !== "hover") return;
4059
+ setGestureState((prev) => ({ ...prev, isActive: false }));
4060
+ onGestureEnd?.();
4061
+ }, [enabled, gestureType, onGestureEnd]);
4062
+ const handleTouchStart = useCallback((e) => {
4063
+ if (!enabled) return;
4064
+ const touch = e.touches[0];
4065
+ startPoint.current = { x: touch.clientX, y: touch.clientY };
4066
+ setGestureState((prev) => ({ ...prev, isActive: true }));
4067
+ onGestureStart?.();
4068
+ }, [enabled, onGestureStart]);
4069
+ const handleTouchMove = useCallback((e) => {
4070
+ if (!enabled) return;
4071
+ const touch = e.touches[0];
4072
+ const deltaX = touch.clientX - startPoint.current.x;
4073
+ const deltaY = touch.clientY - startPoint.current.y;
4074
+ setGestureState((prev) => ({
4075
+ ...prev,
4076
+ x: touch.clientX,
4077
+ y: touch.clientY,
4078
+ deltaX,
4079
+ deltaY
4080
+ }));
4081
+ }, [enabled]);
4082
+ const handleTouchEnd = useCallback(() => {
4083
+ if (!enabled) return;
4084
+ setGestureState((prev) => ({ ...prev, isActive: false }));
4085
+ onGestureEnd?.();
4086
+ }, [enabled, onGestureEnd]);
4087
+ useEffect(() => {
4088
+ if (!elementRef.current) return;
4089
+ const element = elementRef.current;
4090
+ if (gestureType === "hover") {
4091
+ element.addEventListener("mouseenter", handleMouseEnter);
4092
+ element.addEventListener("mouseleave", handleMouseLeave);
4093
+ } else if (gestureType === "drag") {
4094
+ element.addEventListener("mousedown", handleMouseDown);
4095
+ document.addEventListener("mousemove", handleMouseMove);
4096
+ document.addEventListener("mouseup", handleMouseUp);
4097
+ }
4098
+ element.addEventListener("touchstart", handleTouchStart);
4099
+ element.addEventListener("touchmove", handleTouchMove);
4100
+ element.addEventListener("touchend", handleTouchEnd);
4101
+ return () => {
4102
+ element.removeEventListener("mouseenter", handleMouseEnter);
4103
+ element.removeEventListener("mouseleave", handleMouseLeave);
4104
+ element.removeEventListener("mousedown", handleMouseDown);
4105
+ document.removeEventListener("mousemove", handleMouseMove);
4106
+ document.removeEventListener("mouseup", handleMouseUp);
4107
+ element.removeEventListener("touchstart", handleTouchStart);
4108
+ element.removeEventListener("touchmove", handleTouchMove);
4109
+ element.removeEventListener("touchend", handleTouchEnd);
4110
+ };
4111
+ }, [gestureType, handleMouseEnter, handleMouseLeave, handleMouseDown, handleMouseMove, handleMouseUp, handleTouchStart, handleTouchMove, handleTouchEnd]);
4112
+ useEffect(() => {
4113
+ updateMotionStyle();
4114
+ }, [updateMotionStyle]);
4115
+ return {
4116
+ ref: elementRef,
4117
+ gestureState,
4118
+ motionStyle,
4119
+ isActive: gestureState.isActive
4120
+ };
4121
+ }
4122
+ function useButtonEffect(options = {}) {
4123
+ const {
4124
+ duration = 200,
4125
+ easing: easing2 = "ease-out",
4126
+ type = "scale",
4127
+ scaleAmount = 0.95,
4128
+ rippleColor = "rgba(255, 255, 255, 0.6)",
4129
+ rippleSize = 100,
4130
+ rippleDuration = 600,
4131
+ glowColor = "#3b82f6",
4132
+ glowSize = 20,
4133
+ glowIntensity = 0.8,
4134
+ shakeAmount = 5,
4135
+ shakeFrequency = 10,
4136
+ bounceHeight = 10,
4137
+ bounceStiffness = 0.3,
4138
+ slideDistance = 5,
4139
+ slideDirection = "down",
4140
+ hoverScale = 1.05,
4141
+ hoverRotate = 0,
4142
+ hoverTranslateY = -2,
4143
+ hoverTranslateX = 0,
4144
+ activeScale = 0.95,
4145
+ activeRotate = 0,
4146
+ activeTranslateY = 2,
4147
+ activeTranslateX = 0,
4148
+ focusScale = 1.02,
4149
+ focusGlow = true,
4150
+ disabled = false,
4151
+ disabledOpacity = 0.5,
4152
+ autoStart = false,
4153
+ onComplete,
4154
+ onStart,
4155
+ onStop,
4156
+ onReset
4157
+ } = options;
4158
+ const ref = useRef(null);
4159
+ const [isVisible, setIsVisible] = useState(false);
4160
+ const [isAnimating, setIsAnimating] = useState(false);
4161
+ const [progress, setProgress] = useState(0);
4162
+ const [buttonType] = useState(type);
4163
+ const [isPressed, setIsPressed] = useState(false);
4164
+ const [isHovered, setIsHovered] = useState(false);
4165
+ const [isFocused, setIsFocused] = useState(false);
4166
+ const [ripplePosition, setRipplePosition] = useState({ x: 0, y: 0 });
4167
+ const [currentGlowIntensity, setGlowIntensity] = useState(0);
4168
+ const [shakeOffset, setShakeOffset] = useState(0);
4169
+ const [bounceOffset, setBounceOffset] = useState(0);
4170
+ const [slideOffset, setSlideOffset] = useState(0);
4171
+ const animationRef = useRef(null);
4172
+ const startTimeRef = useRef(0);
4173
+ useCallback((event) => {
4174
+ if (!ref.current) return;
4175
+ const rect = ref.current.getBoundingClientRect();
4176
+ const x = event.clientX - rect.left;
4177
+ const y = event.clientY - rect.top;
4178
+ setRipplePosition({ x, y });
4179
+ setIsAnimating(true);
4180
+ setProgress(0);
4181
+ startTimeRef.current = Date.now();
4182
+ onStart?.();
4183
+ const animateRipple = () => {
4184
+ const elapsed = Date.now() - startTimeRef.current;
4185
+ const currentProgress = Math.min(elapsed / rippleDuration, 1);
4186
+ setProgress(currentProgress);
4187
+ if (currentProgress < 1) {
4188
+ animationRef.current = requestAnimationFrame(animateRipple);
4189
+ } else {
4190
+ setIsAnimating(false);
4191
+ onComplete?.();
4192
+ }
4193
+ };
4194
+ animationRef.current = requestAnimationFrame(animateRipple);
4195
+ }, [rippleDuration, onStart, onComplete]);
4196
+ const shakeButton = useCallback(() => {
4197
+ setIsAnimating(true);
4198
+ setProgress(0);
4199
+ startTimeRef.current = Date.now();
4200
+ onStart?.();
4201
+ const animateShake = () => {
4202
+ const elapsed = Date.now() - startTimeRef.current;
4203
+ const currentProgress = Math.min(elapsed / duration, 1);
4204
+ setProgress(currentProgress);
4205
+ const shake = shakeAmount * Math.sin(currentProgress * Math.PI * 2 * shakeFrequency) * (1 - currentProgress);
4206
+ setShakeOffset(shake);
4207
+ if (currentProgress < 1) {
4208
+ animationRef.current = requestAnimationFrame(animateShake);
4209
+ } else {
4210
+ setIsAnimating(false);
4211
+ setShakeOffset(0);
4212
+ onComplete?.();
4213
+ }
4214
+ };
4215
+ animationRef.current = requestAnimationFrame(animateShake);
4216
+ }, [duration, shakeAmount, shakeFrequency, onStart, onComplete]);
4217
+ const bounceButton = useCallback(() => {
4218
+ setIsAnimating(true);
4219
+ setProgress(0);
4220
+ startTimeRef.current = Date.now();
4221
+ onStart?.();
4222
+ const animateBounce = () => {
4223
+ const elapsed = Date.now() - startTimeRef.current;
4224
+ const currentProgress = Math.min(elapsed / duration, 1);
4225
+ setProgress(currentProgress);
4226
+ const bounce = bounceHeight * Math.sin(currentProgress * Math.PI * bounceStiffness) * Math.exp(-currentProgress * 3);
4227
+ setBounceOffset(bounce);
4228
+ if (currentProgress < 1) {
4229
+ animationRef.current = requestAnimationFrame(animateBounce);
4230
+ } else {
4231
+ setIsAnimating(false);
4232
+ setBounceOffset(0);
4233
+ onComplete?.();
4234
+ }
4235
+ };
4236
+ animationRef.current = requestAnimationFrame(animateBounce);
4237
+ }, [duration, bounceHeight, bounceStiffness, onStart, onComplete]);
4238
+ const slideButton = useCallback(() => {
4239
+ setIsAnimating(true);
4240
+ setProgress(0);
4241
+ startTimeRef.current = Date.now();
4242
+ onStart?.();
4243
+ const animateSlide = () => {
4244
+ const elapsed = Date.now() - startTimeRef.current;
4245
+ const currentProgress = Math.min(elapsed / duration, 1);
4246
+ setProgress(currentProgress);
4247
+ const slide = slideDistance * currentProgress;
4248
+ setSlideOffset(slide);
4249
+ if (currentProgress < 1) {
4250
+ animationRef.current = requestAnimationFrame(animateSlide);
4251
+ } else {
4252
+ setIsAnimating(false);
4253
+ onComplete?.();
4254
+ }
4255
+ };
4256
+ animationRef.current = requestAnimationFrame(animateSlide);
4257
+ }, [duration, slideDistance, onStart, onComplete]);
4258
+ const pressButton = useCallback(() => {
4259
+ if (disabled) return;
4260
+ setIsPressed(true);
4261
+ switch (type) {
4262
+ case "ripple":
4263
+ break;
4264
+ case "shake":
4265
+ shakeButton();
4266
+ break;
4267
+ case "bounce":
4268
+ bounceButton();
4269
+ break;
4270
+ case "slide":
4271
+ slideButton();
4272
+ break;
4273
+ }
4274
+ }, [disabled, type, shakeButton, bounceButton, slideButton]);
4275
+ const releaseButton = useCallback(() => {
4276
+ setIsPressed(false);
4277
+ }, []);
4278
+ const setButtonState = useCallback((state) => {
4279
+ switch (state) {
4280
+ case "hover":
4281
+ setIsHovered(true);
4282
+ break;
4283
+ case "active":
4284
+ setIsPressed(true);
4285
+ break;
4286
+ case "focus":
4287
+ setIsFocused(true);
4288
+ break;
4289
+ case "disabled":
4290
+ setIsHovered(false);
4291
+ setIsPressed(false);
4292
+ setIsFocused(false);
4293
+ break;
4294
+ default:
4295
+ setIsHovered(false);
4296
+ setIsPressed(false);
4297
+ setIsFocused(false);
4298
+ break;
4299
+ }
4300
+ }, []);
4301
+ const start = useCallback(() => {
4302
+ if (!isVisible) {
4303
+ setIsVisible(true);
4304
+ }
4305
+ }, [isVisible]);
4306
+ const stop = useCallback(() => {
4307
+ setIsAnimating(false);
4308
+ if (animationRef.current) {
4309
+ cancelAnimationFrame(animationRef.current);
4310
+ animationRef.current = null;
4311
+ }
4312
+ onStop?.();
4313
+ }, [onStop]);
4314
+ const reset = useCallback(() => {
4315
+ setIsVisible(false);
4316
+ setIsAnimating(false);
4317
+ setProgress(0);
4318
+ setIsPressed(false);
4319
+ setIsHovered(false);
4320
+ setIsFocused(false);
4321
+ setRipplePosition({ x: 0, y: 0 });
4322
+ setGlowIntensity(0);
4323
+ setShakeOffset(0);
4324
+ setBounceOffset(0);
4325
+ setSlideOffset(0);
4326
+ startTimeRef.current = 0;
4327
+ if (animationRef.current) {
4328
+ cancelAnimationFrame(animationRef.current);
4329
+ animationRef.current = null;
4330
+ }
4331
+ onReset?.();
4332
+ }, [onReset]);
4333
+ const pause = useCallback(() => {
4334
+ setIsAnimating(false);
4335
+ if (animationRef.current) {
4336
+ cancelAnimationFrame(animationRef.current);
4337
+ animationRef.current = null;
4338
+ }
4339
+ }, []);
4340
+ const resume = useCallback(() => {
4341
+ if (isVisible && !isAnimating) {
4342
+ setIsAnimating(true);
4343
+ }
4344
+ }, [isVisible, isAnimating]);
4345
+ useEffect(() => {
4346
+ if (autoStart) {
4347
+ start();
4348
+ }
4349
+ }, [autoStart, start]);
4350
+ useEffect(() => {
4351
+ return () => {
4352
+ if (animationRef.current) {
4353
+ cancelAnimationFrame(animationRef.current);
4354
+ }
4355
+ };
4356
+ }, []);
4357
+ const getButtonStyle = () => {
4358
+ let scale = 1;
4359
+ let rotate = 0;
4360
+ let translateY = 0;
4361
+ let translateX = 0;
4362
+ let boxShadow = "none";
4363
+ if (isPressed) {
4364
+ scale = activeScale;
4365
+ rotate = activeRotate;
4366
+ translateY = activeTranslateY;
4367
+ translateX = activeTranslateX;
4368
+ } else if (isHovered) {
4369
+ scale = hoverScale;
4370
+ rotate = hoverRotate;
4371
+ translateY = hoverTranslateY;
4372
+ translateX = hoverTranslateX;
4373
+ }
4374
+ if (isFocused && focusGlow) {
4375
+ boxShadow = `0 0 ${glowSize}px ${glowColor}`;
4376
+ }
4377
+ switch (type) {
4378
+ case "shake":
4379
+ translateX += shakeOffset;
4380
+ break;
4381
+ case "bounce":
4382
+ translateY += bounceOffset;
4383
+ break;
4384
+ case "slide":
4385
+ if (slideDirection === "left") translateX -= slideOffset;
4386
+ else if (slideDirection === "right") translateX += slideOffset;
4387
+ else if (slideDirection === "up") translateY -= slideOffset;
4388
+ else if (slideDirection === "down") translateY += slideOffset;
4389
+ break;
4390
+ }
4391
+ const baseStyle = {
4392
+ transform: `
4393
+ scale(${scale})
4394
+ rotate(${rotate}deg)
4395
+ translate(${translateX}px, ${translateY}px)
4396
+ `,
4397
+ boxShadow,
4398
+ opacity: disabled ? disabledOpacity : 1,
4399
+ transition: `all ${duration}ms ${easing2}`,
4400
+ willChange: "transform, box-shadow, opacity",
4401
+ cursor: disabled ? "not-allowed" : "pointer",
4402
+ position: "relative",
4403
+ overflow: "hidden"
4404
+ };
4405
+ return baseStyle;
4406
+ };
4407
+ const style = getButtonStyle();
4408
+ return {
4409
+ ref,
4410
+ isVisible,
4411
+ isAnimating,
4412
+ style,
4413
+ progress,
4414
+ start,
4415
+ stop,
4416
+ reset,
4417
+ pause,
4418
+ resume,
4419
+ buttonType: type,
4420
+ isPressed,
4421
+ isHovered,
4422
+ isFocused,
4423
+ ripplePosition,
4424
+ currentGlowIntensity,
4425
+ shakeOffset,
4426
+ bounceOffset,
4427
+ slideOffset,
4428
+ pressButton,
4429
+ releaseButton,
4430
+ setButtonState
4431
+ };
4432
+ }
4433
+ function useVisibilityToggle(options = {}) {
4434
+ const {
4435
+ duration = 300,
4436
+ easing: easing2 = "ease-out",
4437
+ showScale = 1,
4438
+ showOpacity = 1,
4439
+ showRotate = 0,
4440
+ showTranslateY = 0,
4441
+ showTranslateX = 0,
4442
+ hideScale = 0.8,
4443
+ hideOpacity = 0,
4444
+ hideRotate = 0,
4445
+ hideTranslateY = 20,
4446
+ hideTranslateX = 0,
4447
+ onComplete,
4448
+ onStart,
4449
+ onStop,
4450
+ onReset
4451
+ } = options;
4452
+ const ref = useRef(null);
4453
+ const [isVisible, setIsVisible] = useState(false);
4454
+ const [isAnimating, setIsAnimating] = useState(false);
4455
+ const [progress, setProgress] = useState(0);
4456
+ const toggle = useCallback(() => {
4457
+ setIsAnimating(true);
4458
+ setProgress(0);
4459
+ onStart?.();
4460
+ const newVisibility = !isVisible;
4461
+ setIsVisible(newVisibility);
4462
+ setTimeout(() => {
4463
+ setIsAnimating(false);
4464
+ setProgress(1);
4465
+ onComplete?.();
4466
+ }, duration);
4467
+ }, [isVisible, duration, onStart, onComplete]);
4468
+ const show = useCallback(() => {
4469
+ if (!isVisible) {
4470
+ setIsAnimating(true);
4471
+ setProgress(0);
4472
+ onStart?.();
4473
+ setIsVisible(true);
4474
+ setTimeout(() => {
4475
+ setIsAnimating(false);
4476
+ setProgress(1);
4477
+ onComplete?.();
4478
+ }, duration);
4479
+ }
4480
+ }, [isVisible, duration, onStart, onComplete]);
4481
+ const hide = useCallback(() => {
4482
+ if (isVisible) {
4483
+ setIsAnimating(true);
4484
+ setProgress(1);
4485
+ onStart?.();
4486
+ setIsVisible(false);
4487
+ setTimeout(() => {
4488
+ setIsAnimating(false);
4489
+ setProgress(0);
4490
+ onComplete?.();
4491
+ }, duration);
4492
+ }
4493
+ }, [isVisible, duration, onStart, onComplete]);
4494
+ const start = useCallback(() => {
4495
+ if (!isVisible) {
4496
+ toggle();
4497
+ }
4498
+ }, [isVisible, toggle]);
4499
+ const stop = useCallback(() => {
4500
+ setIsAnimating(false);
4501
+ onStop?.();
4502
+ }, [onStop]);
4503
+ const reset = useCallback(() => {
4504
+ setIsVisible(false);
4505
+ setIsAnimating(false);
4506
+ setProgress(0);
4507
+ onReset?.();
4508
+ }, [onReset]);
4509
+ const pause = useCallback(() => {
4510
+ setIsAnimating(false);
4511
+ }, []);
4512
+ const resume = useCallback(() => {
4513
+ if (isVisible) {
4514
+ setIsAnimating(true);
4515
+ }
4516
+ }, [isVisible]);
4517
+ const style = {
4518
+ transform: `
4519
+ scale(${isVisible ? showScale : hideScale})
4520
+ rotate(${isVisible ? showRotate : hideRotate}deg)
4521
+ translate(${isVisible ? showTranslateX : hideTranslateX}px, ${isVisible ? showTranslateY : hideTranslateY}px)
4522
+ `,
4523
+ opacity: isVisible ? showOpacity : hideOpacity,
4524
+ transition: `all ${duration}ms ${easing2}`,
4525
+ willChange: "transform, opacity"
4526
+ };
4527
+ return {
4528
+ ref,
4529
+ isVisible,
4530
+ isAnimating,
4531
+ style,
4532
+ progress,
4533
+ start,
4534
+ stop,
4535
+ reset,
4536
+ pause,
4537
+ resume,
4538
+ // 추가 메서드
4539
+ toggle,
4540
+ show,
4541
+ hide
4542
+ };
4543
+ }
4544
+ function useScrollToggle(options = {}) {
4545
+ const {
4546
+ duration = 300,
4547
+ easing: easing2 = "ease-out",
4548
+ showScale = 1,
4549
+ showOpacity = 1,
4550
+ showRotate = 0,
4551
+ showTranslateY = 0,
4552
+ showTranslateX = 0,
4553
+ hideScale = 0.8,
4554
+ hideOpacity = 0,
4555
+ hideRotate = 0,
4556
+ hideTranslateY = 20,
4557
+ hideTranslateX = 0,
4558
+ scrollThreshold = 0.1,
4559
+ scrollDirection = "both",
4560
+ onComplete,
4561
+ onStart,
4562
+ onStop,
4563
+ onReset
4564
+ } = options;
4565
+ const ref = useRef(null);
4566
+ const [isVisible, setIsVisible] = useState(false);
4567
+ const [isAnimating, setIsAnimating] = useState(false);
4568
+ const [progress, setProgress] = useState(0);
4569
+ const isVisibleRef = useRef(false);
4570
+ const lastScrollYRef = useRef(0);
4571
+ useEffect(() => {
4572
+ isVisibleRef.current = isVisible;
4573
+ }, [isVisible]);
4574
+ const handleScroll = useCallback(() => {
4575
+ if (!ref.current) return;
4576
+ const currentScrollY = window.scrollY;
4577
+ const rect = ref.current.getBoundingClientRect();
4578
+ const threshold = window.innerHeight * scrollThreshold;
4579
+ const isScrollingDown = currentScrollY > lastScrollYRef.current;
4580
+ const isScrollingUp = currentScrollY < lastScrollYRef.current;
4581
+ let shouldToggle = false;
4582
+ if (scrollDirection === "both") {
4583
+ shouldToggle = rect.top <= threshold;
4584
+ } else if (scrollDirection === "down" && isScrollingDown) {
4585
+ shouldToggle = rect.top <= threshold;
4586
+ } else if (scrollDirection === "up" && isScrollingUp) {
4587
+ shouldToggle = rect.top <= threshold;
4588
+ }
4589
+ if (shouldToggle && !isVisibleRef.current) {
4590
+ isVisibleRef.current = true;
4591
+ setIsVisible(true);
4592
+ setIsAnimating(true);
4593
+ setProgress(0);
4594
+ onStart?.();
4595
+ setTimeout(() => {
4596
+ setIsAnimating(false);
4597
+ setProgress(1);
4598
+ onComplete?.();
4599
+ }, duration);
4600
+ } else if (!shouldToggle && isVisibleRef.current) {
4601
+ isVisibleRef.current = false;
4602
+ setIsVisible(false);
4603
+ setIsAnimating(true);
4604
+ setProgress(1);
4605
+ setTimeout(() => {
4606
+ setIsAnimating(false);
4607
+ setProgress(0);
4608
+ }, duration);
4609
+ }
4610
+ lastScrollYRef.current = currentScrollY;
4611
+ }, [scrollDirection, scrollThreshold, duration, onStart, onComplete]);
4612
+ useEffect(() => {
4613
+ handleScroll();
4614
+ return subscribeScroll(handleScroll);
4615
+ }, [handleScroll]);
4616
+ const start = useCallback(() => {
4617
+ if (!isVisibleRef.current) {
4618
+ isVisibleRef.current = true;
4619
+ setIsVisible(true);
4620
+ setIsAnimating(true);
4621
+ setProgress(0);
4622
+ onStart?.();
4623
+ setTimeout(() => {
4624
+ setIsAnimating(false);
4625
+ setProgress(1);
4626
+ onComplete?.();
4627
+ }, duration);
4628
+ }
4629
+ }, [duration, onStart, onComplete]);
4630
+ const stop = useCallback(() => {
4631
+ setIsAnimating(false);
4632
+ onStop?.();
4633
+ }, [onStop]);
4634
+ const reset = useCallback(() => {
4635
+ isVisibleRef.current = false;
4636
+ setIsVisible(false);
4637
+ setIsAnimating(false);
4638
+ setProgress(0);
4639
+ onReset?.();
4640
+ }, [onReset]);
4641
+ const pause = useCallback(() => {
4642
+ setIsAnimating(false);
4643
+ }, []);
4644
+ const resume = useCallback(() => {
4645
+ if (isVisibleRef.current) {
4646
+ setIsAnimating(true);
4647
+ }
4648
+ }, []);
4649
+ const style = {
4650
+ transform: `
4651
+ scale(${isVisible ? showScale : hideScale})
4652
+ rotate(${isVisible ? showRotate : hideRotate}deg)
4653
+ translate(${isVisible ? showTranslateX : hideTranslateX}px, ${isVisible ? showTranslateY : hideTranslateY}px)
4654
+ `,
4655
+ opacity: isVisible ? showOpacity : hideOpacity,
4656
+ transition: `all ${duration}ms ${easing2}`,
4657
+ willChange: "transform, opacity"
4658
+ };
4659
+ return {
4660
+ ref,
4661
+ isVisible,
4662
+ isAnimating,
4663
+ style,
4664
+ progress,
4665
+ start,
4666
+ stop,
4667
+ reset,
4668
+ pause,
4669
+ resume
4670
+ };
4671
+ }
4672
+ function useCardList(options = {}) {
4673
+ const {
4674
+ duration = 500,
4675
+ easing: easing2 = "ease-out",
4676
+ staggerDelay = 100,
4677
+ cardScale = 1,
4678
+ cardOpacity = 1,
4679
+ cardRotate = 0,
4680
+ cardTranslateY = 0,
4681
+ cardTranslateX = 0,
4682
+ initialScale = 0.8,
4683
+ initialOpacity = 0,
4684
+ initialRotate = 0,
4685
+ initialTranslateY = 30,
4686
+ initialTranslateX = 0,
4687
+ gridColumns = 3,
4688
+ gridGap = 20,
4689
+ onComplete,
4690
+ onStart,
4691
+ onStop,
4692
+ onReset
4693
+ } = options;
4694
+ const ref = useRef(null);
4695
+ const [isVisible, setIsVisible] = useState(false);
4696
+ const [isAnimating, setIsAnimating] = useState(false);
4697
+ const [progress, setProgress] = useState(0);
4698
+ const [cardCount, setCardCount] = useState(0);
4699
+ const startRef = useRef(() => {
4700
+ });
4701
+ useEffect(() => {
4702
+ if (ref.current) {
4703
+ const cards = ref.current.querySelectorAll("[data-card]");
4704
+ setCardCount(cards.length);
4705
+ }
4706
+ }, []);
4707
+ const cardStyles = Array.from({ length: cardCount }, (_, index) => {
4708
+ const delay = isVisible ? index * staggerDelay : 0;
4709
+ const isCardVisible = isVisible && delay <= progress * (cardCount * staggerDelay);
4710
+ return {
4711
+ transform: `
4712
+ scale(${isCardVisible ? cardScale : initialScale})
4713
+ rotate(${isCardVisible ? cardRotate : initialRotate}deg)
4714
+ translate(${isCardVisible ? cardTranslateX : initialTranslateX}px, ${isCardVisible ? cardTranslateY : initialTranslateY}px)
4715
+ `,
4716
+ opacity: isCardVisible ? cardOpacity : initialOpacity,
4717
+ transition: `all ${duration}ms ${easing2} ${delay}ms`,
4718
+ willChange: "transform, opacity"
4719
+ };
4720
+ });
4721
+ const start = useCallback(() => {
4722
+ if (isAnimating) return;
4723
+ setIsAnimating(true);
4724
+ setProgress(0);
4725
+ onStart?.();
4726
+ const totalDuration = cardCount * staggerDelay + duration;
4727
+ const interval = setInterval(() => {
4728
+ setProgress((prev) => {
4729
+ const newProgress = prev + 0.1;
4730
+ if (newProgress >= 1) {
4731
+ setIsVisible(true);
4732
+ setIsAnimating(false);
4733
+ onComplete?.();
4734
+ clearInterval(interval);
4735
+ return 1;
4736
+ }
4737
+ return newProgress;
4738
+ });
4739
+ }, totalDuration / 10);
4740
+ }, [isAnimating, cardCount, staggerDelay, duration, onStart, onComplete]);
4741
+ startRef.current = start;
4742
+ const stop = useCallback(() => {
4743
+ setIsAnimating(false);
4744
+ onStop?.();
4745
+ }, [onStop]);
4746
+ const reset = useCallback(() => {
4747
+ setIsVisible(false);
4748
+ setIsAnimating(false);
4749
+ setProgress(0);
4750
+ onReset?.();
4751
+ }, [onReset]);
4752
+ const pause = useCallback(() => {
4753
+ setIsAnimating(false);
4754
+ }, []);
4755
+ const resume = useCallback(() => {
4756
+ if (!isVisible && !isAnimating) {
4757
+ start();
4758
+ }
4759
+ }, [isVisible, isAnimating, start]);
4760
+ useEffect(() => {
4761
+ if (!ref.current) return;
4762
+ return observeElement(
4763
+ ref.current,
4764
+ (entry) => {
4765
+ if (entry.isIntersecting) startRef.current();
4766
+ },
4767
+ { threshold: 0.1 }
4768
+ );
4769
+ }, []);
4770
+ const gridStyle = {
4771
+ display: "grid",
4772
+ gridTemplateColumns: `repeat(${gridColumns}, 1fr)`,
4773
+ gap: `${gridGap}px`,
4774
+ width: "100%"
4775
+ };
4776
+ return {
4777
+ ref,
4778
+ isVisible,
4779
+ isAnimating,
4780
+ style: gridStyle,
4781
+ progress,
4782
+ start,
4783
+ stop,
4784
+ reset,
4785
+ pause,
4786
+ resume,
4787
+ cardStyles,
4788
+ staggerDelay,
4789
+ gridColumns,
4790
+ gridGap
4791
+ };
4792
+ }
4793
+ function useLoadingSpinner(options = {}) {
4794
+ const {
4795
+ duration = 1e3,
4796
+ easing: easing2 = "linear",
4797
+ type = "rotate",
4798
+ rotationSpeed = 1,
4799
+ pulseSpeed = 1,
4800
+ bounceHeight = 20,
4801
+ waveCount = 3,
4802
+ dotCount = 3,
4803
+ barCount = 4,
4804
+ color = "#3b82f6",
4805
+ backgroundColor = "transparent",
4806
+ size = 40,
4807
+ thickness = 4,
4808
+ autoStart = true,
4809
+ infinite = true,
4810
+ onComplete,
4811
+ onStart,
4812
+ onStop,
4813
+ onReset
4814
+ } = options;
4815
+ const ref = useRef(null);
4816
+ const [isVisible, setIsVisible] = useState(false);
4817
+ const [isAnimating, setIsAnimating] = useState(false);
4818
+ const [progress, setProgress] = useState(0);
4819
+ const [isLoading, setIsLoading] = useState(false);
4820
+ const [rotationAngle, setRotationAngle] = useState(0);
4821
+ const [pulseScale, setPulseScale] = useState(1);
4822
+ const [bounceOffset, setBounceOffset] = useState(0);
4823
+ const [waveProgress, setWaveProgress] = useState(0);
4824
+ const [dotProgress, setDotProgress] = useState(0);
4825
+ const [barProgress, setBarProgress] = useState(0);
4826
+ const animationRef = useRef(null);
4827
+ const startTimeRef = useRef(0);
4828
+ const runAnimation = useCallback(() => {
4829
+ if (!isAnimating) return;
4830
+ const animate = (currentTime) => {
4831
+ if (!isAnimating) return;
4832
+ if (!startTimeRef.current) {
4833
+ startTimeRef.current = currentTime;
4834
+ }
4835
+ const elapsed = currentTime - startTimeRef.current;
4836
+ const currentProgress = elapsed / duration % 1;
4837
+ setProgress(currentProgress);
4838
+ switch (type) {
4839
+ case "rotate":
4840
+ setRotationAngle(currentProgress * 360 * rotationSpeed);
4841
+ break;
4842
+ case "pulse":
4843
+ setPulseScale(0.8 + 0.4 * Math.sin(currentProgress * Math.PI * 2 * pulseSpeed));
4844
+ break;
4845
+ case "bounce":
4846
+ setBounceOffset(bounceHeight * Math.sin(currentProgress * Math.PI * 2));
4847
+ break;
4848
+ case "wave":
4849
+ setWaveProgress(currentProgress);
4850
+ break;
4851
+ case "dots":
4852
+ setDotProgress(currentProgress);
4853
+ break;
4854
+ case "bars":
4855
+ setBarProgress(currentProgress);
4856
+ break;
4857
+ }
4858
+ if (infinite || currentProgress < 1) {
4859
+ animationRef.current = requestAnimationFrame(animate);
4860
+ } else {
4861
+ setIsAnimating(false);
4862
+ onComplete?.();
4863
+ }
4864
+ };
4865
+ animationRef.current = requestAnimationFrame(animate);
4866
+ }, [isAnimating, duration, type, rotationSpeed, pulseSpeed, bounceHeight, infinite, onComplete]);
4867
+ const startLoading = useCallback(() => {
4868
+ if (isLoading) return;
4869
+ setIsLoading(true);
4870
+ setIsAnimating(true);
4871
+ setProgress(0);
4872
+ startTimeRef.current = 0;
4873
+ onStart?.();
4874
+ runAnimation();
4875
+ }, [isLoading, onStart, runAnimation]);
4876
+ const stopLoading = useCallback(() => {
4877
+ setIsLoading(false);
4878
+ setIsAnimating(false);
4879
+ if (animationRef.current) {
4880
+ cancelAnimationFrame(animationRef.current);
4881
+ animationRef.current = null;
4882
+ }
4883
+ onStop?.();
4884
+ }, [onStop]);
4885
+ const setLoadingState = useCallback((loading) => {
4886
+ if (loading) {
4887
+ startLoading();
4888
+ } else {
4889
+ stopLoading();
4890
+ }
4891
+ }, [startLoading, stopLoading]);
4892
+ const start = useCallback(() => {
4893
+ if (!isVisible) {
4894
+ setIsVisible(true);
4895
+ startLoading();
4896
+ }
4897
+ }, [isVisible, startLoading]);
4898
+ const stop = useCallback(() => {
4899
+ stopLoading();
4900
+ }, [stopLoading]);
4901
+ const reset = useCallback(() => {
4902
+ setIsVisible(false);
4903
+ setIsAnimating(false);
4904
+ setProgress(0);
4905
+ setIsLoading(false);
4906
+ setRotationAngle(0);
4907
+ setPulseScale(1);
4908
+ setBounceOffset(0);
4909
+ setWaveProgress(0);
4910
+ setDotProgress(0);
4911
+ setBarProgress(0);
4912
+ startTimeRef.current = 0;
4913
+ if (animationRef.current) {
4914
+ cancelAnimationFrame(animationRef.current);
4915
+ animationRef.current = null;
4916
+ }
4917
+ onReset?.();
4918
+ }, [onReset]);
4919
+ const pause = useCallback(() => {
4920
+ setIsAnimating(false);
4921
+ if (animationRef.current) {
4922
+ cancelAnimationFrame(animationRef.current);
4923
+ animationRef.current = null;
4924
+ }
4925
+ }, []);
4926
+ const resume = useCallback(() => {
4927
+ if (isVisible && !isAnimating) {
4928
+ setIsAnimating(true);
4929
+ runAnimation();
4930
+ }
4931
+ }, [isVisible, isAnimating, runAnimation]);
4932
+ useEffect(() => {
4933
+ if (autoStart) {
4934
+ startLoading();
4935
+ }
4936
+ }, [autoStart, startLoading]);
4937
+ useEffect(() => {
4938
+ return () => {
4939
+ if (animationRef.current) {
4940
+ cancelAnimationFrame(animationRef.current);
4941
+ }
4942
+ };
4943
+ }, []);
4944
+ const getSpinnerStyle = () => {
4945
+ const baseStyle = {
4946
+ width: size,
4947
+ height: size,
4948
+ backgroundColor: "transparent",
4949
+ position: "relative",
4950
+ display: "inline-block"
4951
+ };
4952
+ switch (type) {
4953
+ case "rotate":
4954
+ return {
4955
+ ...baseStyle,
4956
+ border: `${thickness}px solid ${backgroundColor}`,
4957
+ borderTop: `${thickness}px solid ${color}`,
4958
+ borderRadius: "50%",
4959
+ transform: `rotate(${rotationAngle}deg)`,
4960
+ transition: `transform ${duration}ms ${easing2}`
4961
+ };
4962
+ case "pulse":
4963
+ return {
4964
+ ...baseStyle,
4965
+ backgroundColor: color,
4966
+ borderRadius: "50%",
4967
+ transform: `scale(${pulseScale})`,
4968
+ transition: `transform ${duration}ms ${easing2}`
4969
+ };
4970
+ case "bounce":
4971
+ return {
4972
+ ...baseStyle,
4973
+ backgroundColor: color,
4974
+ borderRadius: "50%",
4975
+ transform: `translateY(${bounceOffset}px)`,
4976
+ transition: `transform ${duration}ms ${easing2}`
4977
+ };
4978
+ case "wave":
4979
+ return {
4980
+ ...baseStyle,
4981
+ display: "flex",
4982
+ alignItems: "center",
4983
+ justifyContent: "space-between"
4984
+ };
4985
+ case "dots":
4986
+ return {
4987
+ ...baseStyle,
4988
+ display: "flex",
4989
+ alignItems: "center",
4990
+ justifyContent: "space-between"
4991
+ };
4992
+ case "bars":
4993
+ return {
4994
+ ...baseStyle,
4995
+ display: "flex",
4996
+ alignItems: "center",
4997
+ justifyContent: "space-between"
4998
+ };
4999
+ default:
5000
+ return baseStyle;
5001
+ }
5002
+ };
5003
+ const style = getSpinnerStyle();
5004
+ return {
5005
+ ref,
5006
+ isVisible,
5007
+ isAnimating,
5008
+ style,
5009
+ progress,
5010
+ start,
5011
+ stop,
5012
+ reset,
5013
+ pause,
5014
+ resume,
5015
+ isLoading,
5016
+ spinnerType: type,
5017
+ rotationAngle,
5018
+ pulseScale,
5019
+ bounceOffset,
5020
+ waveProgress,
5021
+ dotProgress,
5022
+ barProgress,
5023
+ startLoading,
5024
+ stopLoading,
5025
+ setLoadingState
5026
+ };
5027
+ }
5028
+ function useNavigation(options = {}) {
5029
+ const {
5030
+ duration = 300,
5031
+ easing: easing2 = "ease-out",
5032
+ type = "slide",
5033
+ slideDirection = "left",
5034
+ staggerDelay = 50,
5035
+ itemScale = 1,
5036
+ itemOpacity = 1,
5037
+ itemRotate = 0,
5038
+ itemTranslateY = 0,
5039
+ itemTranslateX = 0,
5040
+ initialScale = 0.8,
5041
+ initialOpacity = 0,
5042
+ initialRotate = -10,
5043
+ initialTranslateY = 20,
5044
+ initialTranslateX = 0,
5045
+ activeScale = 1.05,
5046
+ activeOpacity = 1,
5047
+ activeRotate = 0,
5048
+ activeTranslateY = 0,
5049
+ activeTranslateX = 0,
5050
+ hoverScale = 1.1,
5051
+ hoverOpacity = 1,
5052
+ hoverRotate = 5,
5053
+ hoverTranslateY = -5,
5054
+ hoverTranslateX = 0,
5055
+ itemCount = 5,
5056
+ autoStart = false,
5057
+ onComplete,
5058
+ onStart,
5059
+ onStop,
5060
+ onReset
5061
+ } = options;
5062
+ const ref = useRef(null);
5063
+ const [isVisible, setIsVisible] = useState(false);
5064
+ const [isAnimating, setIsAnimating] = useState(false);
5065
+ const [progress, setProgress] = useState(0);
5066
+ const [isOpen, setIsOpen] = useState(false);
5067
+ const [activeIndex, setActiveIndex] = useState(0);
5068
+ const [hoveredIndex, setHoveredIndex] = useState(null);
5069
+ const openMenu = useCallback(() => {
5070
+ if (isOpen) return;
5071
+ setIsOpen(true);
5072
+ setIsAnimating(true);
5073
+ setProgress(0);
5074
+ onStart?.();
5075
+ const totalDuration = itemCount * staggerDelay + duration;
5076
+ const interval = setInterval(() => {
5077
+ setProgress((prev) => {
5078
+ const newProgress = prev + 0.1;
5079
+ if (newProgress >= 1) {
5080
+ setIsAnimating(false);
5081
+ onComplete?.();
5082
+ clearInterval(interval);
5083
+ return 1;
5084
+ }
5085
+ return newProgress;
5086
+ });
5087
+ }, totalDuration / 10);
5088
+ }, [isOpen, itemCount, staggerDelay, duration, onStart, onComplete]);
5089
+ const closeMenu = useCallback(() => {
5090
+ if (!isOpen) return;
5091
+ setIsOpen(false);
5092
+ setIsAnimating(true);
5093
+ setProgress(1);
5094
+ setTimeout(() => {
5095
+ setIsAnimating(false);
5096
+ setProgress(0);
5097
+ }, duration);
5098
+ }, [isOpen, duration]);
5099
+ const toggleMenu = useCallback(() => {
5100
+ if (isOpen) {
5101
+ closeMenu();
5102
+ } else {
5103
+ openMenu();
5104
+ }
5105
+ }, [isOpen, openMenu, closeMenu]);
5106
+ const setActiveItem = useCallback((index) => {
5107
+ if (index >= 0 && index < itemCount) {
5108
+ setActiveIndex(index);
5109
+ }
5110
+ }, [itemCount]);
5111
+ const goToNext = useCallback(() => {
5112
+ setActiveItem((activeIndex + 1) % itemCount);
5113
+ }, [activeIndex, itemCount, setActiveItem]);
5114
+ const goToPrevious = useCallback(() => {
5115
+ setActiveItem(activeIndex === 0 ? itemCount - 1 : activeIndex - 1);
5116
+ }, [activeIndex, itemCount, setActiveItem]);
5117
+ const start = useCallback(() => {
5118
+ if (!isVisible) {
5119
+ setIsVisible(true);
5120
+ openMenu();
5121
+ }
5122
+ }, [isVisible, openMenu]);
5123
+ const stop = useCallback(() => {
5124
+ setIsAnimating(false);
5125
+ onStop?.();
5126
+ }, [onStop]);
5127
+ const reset = useCallback(() => {
5128
+ setIsVisible(false);
5129
+ setIsAnimating(false);
5130
+ setProgress(0);
5131
+ setIsOpen(false);
5132
+ setActiveIndex(0);
5133
+ setHoveredIndex(null);
5134
+ onReset?.();
5135
+ }, [onReset]);
5136
+ const pause = useCallback(() => {
5137
+ setIsAnimating(false);
5138
+ }, []);
5139
+ const resume = useCallback(() => {
5140
+ if (isVisible) {
5141
+ setIsAnimating(true);
5142
+ }
5143
+ }, [isVisible]);
5144
+ useEffect(() => {
5145
+ if (autoStart) {
5146
+ start();
5147
+ }
5148
+ }, [autoStart, start]);
5149
+ const itemStyles = Array.from({ length: itemCount }, (_, index) => {
5150
+ const delay = isOpen ? index * staggerDelay : 0;
5151
+ const isItemVisible = isOpen && delay <= progress * (itemCount * staggerDelay);
5152
+ const isActive = index === activeIndex;
5153
+ const isHovered = index === hoveredIndex;
5154
+ let scale = initialScale;
5155
+ let opacity = initialOpacity;
5156
+ let rotate = initialRotate;
5157
+ let translateY = initialTranslateY;
5158
+ let translateX = initialTranslateX;
5159
+ if (isItemVisible) {
5160
+ if (isHovered) {
5161
+ scale = hoverScale;
5162
+ opacity = hoverOpacity;
5163
+ rotate = hoverRotate;
5164
+ translateY = hoverTranslateY;
5165
+ translateX = hoverTranslateX;
5166
+ } else if (isActive) {
5167
+ scale = activeScale;
5168
+ opacity = activeOpacity;
5169
+ rotate = activeRotate;
5170
+ translateY = activeTranslateY;
5171
+ translateX = activeTranslateX;
5172
+ } else {
5173
+ scale = itemScale;
5174
+ opacity = itemOpacity;
5175
+ rotate = itemRotate;
5176
+ translateY = itemTranslateY;
5177
+ translateX = itemTranslateX;
5178
+ }
5179
+ }
5180
+ return {
5181
+ transform: `
5182
+ scale(${scale})
5183
+ rotate(${rotate}deg)
5184
+ translate(${translateX}px, ${translateY}px)
5185
+ `,
5186
+ opacity,
5187
+ transition: `all ${duration}ms ${easing2} ${delay}ms`,
5188
+ willChange: "transform, opacity",
5189
+ cursor: "pointer"
5190
+ };
5191
+ });
5192
+ const getNavigationStyle = () => {
5193
+ const baseStyle = {
5194
+ transition: `all ${duration}ms ${easing2}`,
5195
+ willChange: "transform, opacity"
5196
+ };
5197
+ switch (type) {
5198
+ case "slide":
5199
+ if (slideDirection === "left") {
5200
+ baseStyle.transform = `translateX(${isOpen ? 0 : -100}%)`;
5201
+ } else if (slideDirection === "right") {
5202
+ baseStyle.transform = `translateX(${isOpen ? 0 : 100}%)`;
5203
+ } else if (slideDirection === "up") {
5204
+ baseStyle.transform = `translateY(${isOpen ? 0 : -100}%)`;
5205
+ } else if (slideDirection === "down") {
5206
+ baseStyle.transform = `translateY(${isOpen ? 0 : 100}%)`;
5207
+ }
5208
+ break;
5209
+ case "fade":
5210
+ baseStyle.opacity = isOpen ? 1 : 0;
5211
+ break;
5212
+ case "scale":
5213
+ baseStyle.transform = `scale(${isOpen ? 1 : 0})`;
5214
+ break;
5215
+ case "rotate":
5216
+ baseStyle.transform = `rotate(${isOpen ? 0 : 180}deg)`;
5217
+ break;
5218
+ }
5219
+ return baseStyle;
5220
+ };
5221
+ const style = getNavigationStyle();
4126
5222
  return {
4127
- isActive,
4128
- gesture,
4129
- scale,
4130
- rotation,
4131
- deltaX,
4132
- deltaY,
4133
- distance,
4134
- velocity,
5223
+ ref,
5224
+ isVisible,
5225
+ isAnimating,
5226
+ style,
5227
+ progress,
4135
5228
  start,
4136
5229
  stop,
4137
5230
  reset,
4138
- onTouchStart,
4139
- onTouchMove,
4140
- onTouchEnd,
4141
- onMouseDown,
4142
- onMouseMove,
4143
- onMouseUp
5231
+ pause,
5232
+ resume,
5233
+ isOpen,
5234
+ activeIndex,
5235
+ itemStyles,
5236
+ openMenu,
5237
+ closeMenu,
5238
+ toggleMenu,
5239
+ setActiveItem,
5240
+ goToNext,
5241
+ goToPrevious
4144
5242
  };
4145
5243
  }
4146
- function useGestureMotion(options) {
5244
+ function useSkeleton(options = {}) {
4147
5245
  const {
4148
- gestureType,
4149
- duration = 300,
4150
- easing: easing2 = "ease-out",
4151
- sensitivity = 1,
4152
- enabled = true,
4153
- onGestureStart,
4154
- onGestureEnd
5246
+ delay = 0,
5247
+ duration = 1500,
5248
+ easing: easing2 = "ease-in-out",
5249
+ autoStart = true,
5250
+ backgroundColor = "#f0f0f0",
5251
+ highlightColor = "#e0e0e0",
5252
+ motionSpeed = 1500,
5253
+ height = 20,
5254
+ width = "100%",
5255
+ borderRadius = 4,
5256
+ wave = true,
5257
+ pulse: pulse2 = false,
5258
+ onComplete,
5259
+ onStart,
5260
+ onStop,
5261
+ onReset
4155
5262
  } = options;
4156
- const elementRef = useRef(null);
4157
- const [gestureState, setGestureState] = useState({
4158
- isActive: false,
4159
- x: 0,
4160
- y: 0,
4161
- deltaX: 0,
4162
- deltaY: 0,
4163
- scale: 1,
4164
- rotation: 0
4165
- });
4166
- const [motionStyle, setMotionStyle] = useState({});
4167
- const startPoint = useRef({ x: 0, y: 0 });
4168
- const isDragging = useRef(false);
4169
- const updateMotionStyle = useCallback(() => {
4170
- if (!enabled) return;
4171
- const { isActive, deltaX, deltaY, scale, rotation } = gestureState;
4172
- let transform = "";
4173
- switch (gestureType) {
4174
- case "hover":
4175
- transform = isActive ? `scale(${1 + sensitivity * 0.05}) translateY(-${sensitivity * 2}px)` : "scale(1) translateY(0)";
4176
- break;
4177
- case "drag":
4178
- transform = isActive ? `translate(${deltaX * sensitivity}px, ${deltaY * sensitivity}px)` : "translate(0, 0)";
4179
- break;
4180
- case "pinch":
4181
- transform = `scale(${scale})`;
4182
- break;
4183
- case "swipe":
4184
- transform = isActive ? `translateX(${deltaX * sensitivity}px) rotateY(${deltaX * 0.1}deg)` : "translateX(0) rotateY(0)";
4185
- break;
4186
- case "tilt":
4187
- transform = isActive ? `rotateX(${deltaY * 0.1}deg) rotateY(${deltaX * 0.1}deg)` : "rotateX(0) rotateY(0)";
4188
- break;
5263
+ const ref = useRef(null);
5264
+ const [isVisible, setIsVisible] = useState(autoStart);
5265
+ const [isAnimating, setIsAnimating] = useState(autoStart);
5266
+ const [progress, setProgress] = useState(0);
5267
+ const motionRef = useRef(null);
5268
+ const startTimeRef = useRef(null);
5269
+ const start = useCallback(() => {
5270
+ if (isAnimating) return;
5271
+ setIsVisible(true);
5272
+ setIsAnimating(true);
5273
+ setProgress(0);
5274
+ onStart?.();
5275
+ setTimeout(() => {
5276
+ startTimeRef.current = Date.now();
5277
+ const animate = () => {
5278
+ if (!startTimeRef.current) return;
5279
+ const elapsed = Date.now() - startTimeRef.current;
5280
+ const newProgress = Math.min(elapsed / duration, 1);
5281
+ setProgress(newProgress);
5282
+ if (newProgress < 1) {
5283
+ motionRef.current = requestAnimationFrame(animate);
5284
+ } else {
5285
+ setIsAnimating(false);
5286
+ onComplete?.();
5287
+ }
5288
+ };
5289
+ motionRef.current = requestAnimationFrame(animate);
5290
+ }, delay);
5291
+ }, [delay, duration, isAnimating, onStart, onComplete]);
5292
+ const stop = useCallback(() => {
5293
+ if (motionRef.current) {
5294
+ cancelAnimationFrame(motionRef.current);
5295
+ motionRef.current = null;
4189
5296
  }
4190
- setMotionStyle({
4191
- transform,
4192
- transition: isActive ? "none" : `all ${duration}ms ${easing2}`,
4193
- cursor: gestureType === "drag" && isActive ? "grabbing" : "pointer"
4194
- });
4195
- }, [gestureState, gestureType, enabled, duration, easing2, sensitivity]);
4196
- const handleMouseDown = useCallback((e) => {
4197
- if (!enabled || gestureType !== "drag") return;
4198
- isDragging.current = true;
4199
- startPoint.current = { x: e.clientX, y: e.clientY };
4200
- setGestureState((prev) => ({ ...prev, isActive: true }));
4201
- onGestureStart?.();
4202
- }, [enabled, gestureType, onGestureStart]);
4203
- const handleMouseMove = useCallback((e) => {
4204
- if (!enabled || !isDragging.current) return;
4205
- const deltaX = e.clientX - startPoint.current.x;
4206
- const deltaY = e.clientY - startPoint.current.y;
4207
- setGestureState((prev) => ({
4208
- ...prev,
4209
- x: e.clientX,
4210
- y: e.clientY,
4211
- deltaX,
4212
- deltaY
4213
- }));
4214
- }, [enabled]);
4215
- const handleMouseUp = useCallback(() => {
4216
- if (!enabled) return;
4217
- isDragging.current = false;
4218
- setGestureState((prev) => ({ ...prev, isActive: false }));
4219
- onGestureEnd?.();
4220
- }, [enabled, onGestureEnd]);
4221
- const handleMouseEnter = useCallback(() => {
4222
- if (!enabled || gestureType !== "hover") return;
4223
- setGestureState((prev) => ({ ...prev, isActive: true }));
4224
- onGestureStart?.();
4225
- }, [enabled, gestureType, onGestureStart]);
4226
- const handleMouseLeave = useCallback(() => {
4227
- if (!enabled || gestureType !== "hover") return;
4228
- setGestureState((prev) => ({ ...prev, isActive: false }));
4229
- onGestureEnd?.();
4230
- }, [enabled, gestureType, onGestureEnd]);
4231
- const handleTouchStart = useCallback((e) => {
4232
- if (!enabled) return;
4233
- const touch = e.touches[0];
4234
- startPoint.current = { x: touch.clientX, y: touch.clientY };
4235
- setGestureState((prev) => ({ ...prev, isActive: true }));
4236
- onGestureStart?.();
4237
- }, [enabled, onGestureStart]);
4238
- const handleTouchMove = useCallback((e) => {
4239
- if (!enabled) return;
4240
- const touch = e.touches[0];
4241
- const deltaX = touch.clientX - startPoint.current.x;
4242
- const deltaY = touch.clientY - startPoint.current.y;
4243
- setGestureState((prev) => ({
4244
- ...prev,
4245
- x: touch.clientX,
4246
- y: touch.clientY,
4247
- deltaX,
4248
- deltaY
4249
- }));
4250
- }, [enabled]);
4251
- const handleTouchEnd = useCallback(() => {
4252
- if (!enabled) return;
4253
- setGestureState((prev) => ({ ...prev, isActive: false }));
4254
- onGestureEnd?.();
4255
- }, [enabled, onGestureEnd]);
5297
+ startTimeRef.current = null;
5298
+ setIsAnimating(false);
5299
+ onStop?.();
5300
+ }, [onStop]);
5301
+ const reset = useCallback(() => {
5302
+ stop();
5303
+ setIsVisible(false);
5304
+ setProgress(0);
5305
+ onReset?.();
5306
+ }, [stop, onReset]);
5307
+ const pause = useCallback(() => {
5308
+ if (motionRef.current) {
5309
+ cancelAnimationFrame(motionRef.current);
5310
+ motionRef.current = null;
5311
+ }
5312
+ setIsAnimating(false);
5313
+ }, []);
5314
+ const resume = useCallback(() => {
5315
+ if (!isAnimating && isVisible) {
5316
+ start();
5317
+ }
5318
+ }, [isAnimating, isVisible, start]);
4256
5319
  useEffect(() => {
4257
- if (!elementRef.current) return;
4258
- const element = elementRef.current;
4259
- if (gestureType === "hover") {
4260
- element.addEventListener("mouseenter", handleMouseEnter);
4261
- element.addEventListener("mouseleave", handleMouseLeave);
4262
- } else if (gestureType === "drag") {
4263
- element.addEventListener("mousedown", handleMouseDown);
4264
- document.addEventListener("mousemove", handleMouseMove);
4265
- document.addEventListener("mouseup", handleMouseUp);
5320
+ if (autoStart) {
5321
+ start();
4266
5322
  }
4267
- element.addEventListener("touchstart", handleTouchStart);
4268
- element.addEventListener("touchmove", handleTouchMove);
4269
- element.addEventListener("touchend", handleTouchEnd);
5323
+ }, [autoStart, start]);
5324
+ useEffect(() => {
4270
5325
  return () => {
4271
- element.removeEventListener("mouseenter", handleMouseEnter);
4272
- element.removeEventListener("mouseleave", handleMouseLeave);
4273
- element.removeEventListener("mousedown", handleMouseDown);
4274
- document.removeEventListener("mousemove", handleMouseMove);
4275
- document.removeEventListener("mouseup", handleMouseUp);
4276
- element.removeEventListener("touchstart", handleTouchStart);
4277
- element.removeEventListener("touchmove", handleTouchMove);
4278
- element.removeEventListener("touchend", handleTouchEnd);
5326
+ if (motionRef.current) {
5327
+ cancelAnimationFrame(motionRef.current);
5328
+ }
4279
5329
  };
4280
- }, [gestureType, handleMouseEnter, handleMouseLeave, handleMouseDown, handleMouseMove, handleMouseUp, handleTouchStart, handleTouchMove, handleTouchEnd]);
5330
+ }, []);
5331
+ const getSkeletonStyle = () => {
5332
+ const baseStyle = {
5333
+ width: typeof width === "number" ? `${width}px` : width,
5334
+ height: `${height}px`,
5335
+ backgroundColor,
5336
+ borderRadius: `${borderRadius}px`,
5337
+ position: "relative",
5338
+ overflow: "hidden",
5339
+ opacity: isVisible ? 1 : 0,
5340
+ transition: `opacity ${duration}ms ${easing2}`
5341
+ };
5342
+ if (wave && isAnimating) {
5343
+ baseStyle.background = `linear-gradient(90deg, ${backgroundColor} 25%, ${highlightColor} 50%, ${backgroundColor} 75%)`;
5344
+ baseStyle.backgroundSize = "200% 100%";
5345
+ baseStyle.animation = `skeleton-wave ${motionSpeed}ms infinite linear`;
5346
+ } else if (pulse2 && isAnimating) {
5347
+ baseStyle.animation = `skeleton-pulse ${motionSpeed}ms infinite ease-in-out`;
5348
+ }
5349
+ return baseStyle;
5350
+ };
5351
+ const style = getSkeletonStyle();
4281
5352
  useEffect(() => {
4282
- updateMotionStyle();
4283
- }, [updateMotionStyle]);
5353
+ const styleSheet = document.styleSheets[0] || document.createElement("style").sheet;
5354
+ if (styleSheet && !document.getElementById("skeleton-animations")) {
5355
+ const styleElement = document.createElement("style");
5356
+ styleElement.id = "skeleton-animations";
5357
+ styleElement.textContent = `
5358
+ @keyframes skeleton-wave {
5359
+ 0% { background-position: 200% 0; }
5360
+ 100% { background-position: -200% 0; }
5361
+ }
5362
+
5363
+ @keyframes skeleton-pulse {
5364
+ 0%, 100% { opacity: 0.6; }
5365
+ 50% { opacity: 1; }
5366
+ }
5367
+ `;
5368
+ document.head.appendChild(styleElement);
5369
+ }
5370
+ }, []);
4284
5371
  return {
4285
- ref: elementRef,
4286
- gestureState,
4287
- motionStyle,
4288
- isActive: gestureState.isActive
5372
+ ref,
5373
+ isVisible,
5374
+ isAnimating,
5375
+ style,
5376
+ progress,
5377
+ start,
5378
+ stop,
5379
+ reset,
5380
+ pause,
5381
+ resume
4289
5382
  };
4290
5383
  }
4291
5384
  function useTypewriter(options) {
@@ -4440,7 +5533,7 @@ function useSmoothScroll(options = {}) {
4440
5533
  wheelMultiplier = 1,
4441
5534
  touchMultiplier = 2,
4442
5535
  direction = "vertical",
4443
- onScroll
5536
+ onScroll: onScroll2
4444
5537
  } = options;
4445
5538
  const [scroll, setScroll] = useState(0);
4446
5539
  const [progress, setProgress] = useState(0);
@@ -4468,14 +5561,14 @@ function useSmoothScroll(options = {}) {
4468
5561
  setScroll(currentRef.current);
4469
5562
  const max = getMaxScroll();
4470
5563
  setProgress(max > 0 ? currentRef.current / max : 0);
4471
- onScroll?.(currentRef.current);
5564
+ onScroll2?.(currentRef.current);
4472
5565
  if (direction === "vertical") {
4473
5566
  window.scrollTo(0, currentRef.current);
4474
5567
  } else {
4475
5568
  window.scrollTo(currentRef.current, 0);
4476
5569
  }
4477
5570
  rafRef.current = requestAnimationFrame(animate);
4478
- }, [lerp, direction, getMaxScroll, onScroll]);
5571
+ }, [lerp, direction, getMaxScroll, onScroll2]);
4479
5572
  const startAnimation = useCallback(() => {
4480
5573
  if (isRunningRef.current) return;
4481
5574
  isRunningRef.current = true;
@@ -4545,6 +5638,8 @@ function useElementProgress(options = {}) {
4545
5638
  const ref = useRef(null);
4546
5639
  const [progress, setProgress] = useState(0);
4547
5640
  const [isInView, setIsInView] = useState(false);
5641
+ const progressRef = useRef(0);
5642
+ const isInViewRef = useRef(false);
4548
5643
  useEffect(() => {
4549
5644
  const el = ref.current;
4550
5645
  if (!el || typeof window === "undefined") return;
@@ -4558,16 +5653,18 @@ function useElementProgress(options = {}) {
4558
5653
  const range = trackStart - trackEnd;
4559
5654
  const raw = range > 0 ? (trackStart - elementTop) / range : 0;
4560
5655
  const clamped = clamp ? Math.max(0, Math.min(1, raw)) : raw;
4561
- setProgress(clamped);
4562
- setIsInView(elementBottom > 0 && elementTop < vh);
5656
+ if (Math.abs(clamped - progressRef.current) > 5e-3) {
5657
+ progressRef.current = clamped;
5658
+ setProgress(clamped);
5659
+ }
5660
+ const nowInView = elementBottom > 0 && elementTop < vh;
5661
+ if (nowInView !== isInViewRef.current) {
5662
+ isInViewRef.current = nowInView;
5663
+ setIsInView(nowInView);
5664
+ }
4563
5665
  };
4564
5666
  calculate();
4565
- window.addEventListener("scroll", calculate, { passive: true });
4566
- window.addEventListener("resize", calculate, { passive: true });
4567
- return () => {
4568
- window.removeEventListener("scroll", calculate);
4569
- window.removeEventListener("resize", calculate);
4570
- };
5667
+ return subscribeScroll(calculate);
4571
5668
  }, [start, end, clamp]);
4572
5669
  return { ref, progress, isInView };
4573
5670
  }
@@ -4618,16 +5715,16 @@ function Motion({
4618
5715
  }
4619
5716
  );
4620
5717
  }
4621
- function getInitialTransform(motionType) {
5718
+ function getInitialTransform(motionType, slideDistance, scaleFrom) {
4622
5719
  switch (motionType) {
4623
5720
  case "slideUp":
4624
- return "translateY(32px)";
5721
+ return `translateY(${slideDistance}px)`;
4625
5722
  case "slideLeft":
4626
- return "translateX(-32px)";
5723
+ return `translateX(-${slideDistance}px)`;
4627
5724
  case "slideRight":
4628
- return "translateX(32px)";
5725
+ return `translateX(${slideDistance}px)`;
4629
5726
  case "scaleIn":
4630
- return "scale(0.95)";
5727
+ return `scale(${scaleFrom})`;
4631
5728
  case "bounceIn":
4632
5729
  return "scale(0.75)";
4633
5730
  case "fadeIn":
@@ -4636,36 +5733,33 @@ function getInitialTransform(motionType) {
4636
5733
  }
4637
5734
  }
4638
5735
  function useStagger(options) {
5736
+ const profile = useMotionProfile();
4639
5737
  const {
4640
5738
  count,
4641
- staggerDelay = 100,
4642
- baseDelay = 0,
4643
- duration = 700,
5739
+ staggerDelay = profile.stagger.perItem,
5740
+ baseDelay = profile.stagger.baseDelay,
5741
+ duration = profile.base.duration,
4644
5742
  motionType = "fadeIn",
4645
- threshold = 0.1,
4646
- easing: easing2 = "ease-out"
5743
+ threshold = profile.base.threshold,
5744
+ easing: easing2 = profile.base.easing
4647
5745
  } = options;
4648
5746
  const containerRef = useRef(null);
4649
5747
  const [isVisible, setIsVisible] = useState(false);
4650
5748
  useEffect(() => {
4651
5749
  if (!containerRef.current) return;
4652
- const observer = new IntersectionObserver(
4653
- (entries) => {
4654
- entries.forEach((entry) => {
4655
- if (entry.isIntersecting) {
4656
- setIsVisible(true);
4657
- observer.disconnect();
4658
- }
4659
- });
5750
+ return observeElement(
5751
+ containerRef.current,
5752
+ (entry) => {
5753
+ if (entry.isIntersecting) setIsVisible(true);
4660
5754
  },
4661
- { threshold }
5755
+ { threshold },
5756
+ true
5757
+ // once — 첫 intersection 후 자동 정리
4662
5758
  );
4663
- observer.observe(containerRef.current);
4664
- return () => {
4665
- observer.disconnect();
4666
- };
4667
5759
  }, [threshold]);
4668
- const initialTransform = useMemo(() => getInitialTransform(motionType), [motionType]);
5760
+ const slideDistance = profile.entrance.slide.distance;
5761
+ const scaleFrom = profile.entrance.scale.from;
5762
+ const initialTransform = useMemo(() => getInitialTransform(motionType, slideDistance, scaleFrom), [motionType, slideDistance, scaleFrom]);
4669
5763
  const styles = useMemo(() => {
4670
5764
  return Array.from({ length: count }, (_, i) => {
4671
5765
  const itemDelay = baseDelay + i * staggerDelay;
@@ -4690,6 +5784,6 @@ function useStagger(options) {
4690
5784
  };
4691
5785
  }
4692
5786
 
4693
- export { MOTION_PRESETS, Motion, MotionEngine, PAGE_MOTIONS, PerformanceOptimizer, TransitionEffects, applyEasing, easeIn, easeInOut, easeInOutQuad, easeInQuad, easeOut, easeOutQuad, easingPresets, getAvailableEasings, getEasing, getMotionPreset, getPagePreset, getPresetEasing, isEasingFunction, isValidEasing, linear, mergeWithPreset, motionEngine, performanceOptimizer, safeApplyEasing, transitionEffects, useBounceIn, useClickToggle, useCustomCursor, useElementProgress, useFadeIn, useFocusToggle, useGesture, useGestureMotion, useGradient, useHoverMotion, useInView, useMagneticCursor, useMotionState, useMouse, usePageMotions, usePulse, useReducedMotion, useRepeat, useScaleIn, useScrollProgress, useScrollReveal, useSimplePageMotion, useSlideDown, useSlideLeft, useSlideRight, useSlideUp, useSmartMotion, useSmoothScroll, useSpringMotion, useStagger, useToggleMotion, useTypewriter, useUnifiedMotion, useWindowSize };
5787
+ export { MOTION_PRESETS, Motion, MotionEngine, MotionProfileProvider, PAGE_MOTIONS, TransitionEffects, applyEasing, calculateSpring, easeIn, easeInOut, easeInOutQuad, easeInQuad, easeOut, easeOutQuad, easingPresets, getAvailableEasings, getEasing, getMotionPreset, getPagePreset, getPresetEasing, hua, isEasingFunction, isValidEasing, linear, mergeProfileOverrides, mergeWithPreset, motionEngine, neutral, observeElement, resolveProfile, safeApplyEasing, transitionEffects, useBounceIn, useButtonEffect, useCardList, useClickToggle, useCustomCursor, useElementProgress, useFadeIn, useFocusToggle, useGesture, useGestureMotion, useGradient, useHoverMotion, useInView, useLoadingSpinner, useMagneticCursor, useMotionProfile, useMotionState, useMouse, useNavigation, usePageMotions, usePulse, useReducedMotion, useRepeat, useScaleIn, useScrollProgress, useScrollReveal, useScrollToggle, useSimplePageMotion, useSkeleton, useSlideDown, useSlideLeft, useSlideRight, useSlideUp, useSmartMotion, useSmoothScroll, useSpringMotion, useStagger, useToggleMotion, useTypewriter, useUnifiedMotion, useVisibilityToggle, useWindowSize };
4694
5788
  //# sourceMappingURL=index.mjs.map
4695
5789
  //# sourceMappingURL=index.mjs.map