@hua-labs/motion-core 2.2.2 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,5 +1,6 @@
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
+ import { jsx } from 'react/jsx-runtime';
3
4
 
4
5
  // src/core/MotionEngine.ts
5
6
  var MotionEngine = class {
@@ -194,246 +195,202 @@ var TransitionEffects = class _TransitionEffects {
194
195
  return _TransitionEffects.instance;
195
196
  }
196
197
  /**
197
- * 페이드 인/아웃 전환
198
+ * 공통 전환 실행 헬퍼
198
199
  */
199
- async fade(element, options) {
200
+ async executeTransition(element, options, config) {
200
201
  const transitionId = this.generateTransitionId();
201
- return new Promise(async (resolve) => {
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";
202
+ config.setup();
203
+ this.enableGPUAcceleration(element);
204
+ let resolveTransition;
205
+ const completed = new Promise((resolve) => {
206
+ resolveTransition = resolve;
207
+ });
208
+ const motionId = await motionEngine.motion(
209
+ element,
210
+ [
211
+ { progress: 0, properties: config.keyframes[0] },
212
+ { progress: 1, properties: config.keyframes[1] }
213
+ ],
214
+ {
215
+ duration: options.duration,
216
+ easing: options.easing || this.getDefaultEasing(),
217
+ delay: options.delay,
218
+ onStart: options.onTransitionStart,
219
+ onUpdate: config.onUpdate,
220
+ onComplete: () => {
221
+ config.onCleanup();
222
+ options.onTransitionComplete?.();
223
+ this.activeTransitions.delete(transitionId);
224
+ resolveTransition();
225
+ }
208
226
  }
209
- this.enableGPUAcceleration(element);
210
- const motionId = await motionEngine.motion(
211
- element,
212
- [
213
- { progress: 0, properties: { opacity: options.direction === "reverse" ? initialOpacity : 0 } },
214
- { progress: 1, properties: { opacity: targetOpacity } }
215
- ],
216
- {
217
- duration: options.duration,
218
- easing: options.easing || this.getDefaultEasing(),
219
- delay: options.delay,
220
- onStart: options.onTransitionStart,
221
- onUpdate: (progress) => {
222
- const currentOpacity = options.direction === "reverse" ? initialOpacity * (1 - progress) : targetOpacity * progress;
223
- element.style.opacity = currentOpacity.toString();
224
- },
225
- onComplete: () => {
226
- options.onTransitionComplete?.();
227
- this.activeTransitions.delete(transitionId);
228
- resolve();
229
- }
227
+ );
228
+ this.activeTransitions.set(transitionId, motionId);
229
+ return completed;
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";
230
243
  }
231
- );
232
- this.activeTransitions.set(transitionId, motionId);
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
+ }
233
255
  });
234
256
  }
235
257
  /**
236
258
  * 슬라이드 전환
237
259
  */
238
260
  async slide(element, options) {
239
- const transitionId = this.generateTransitionId();
240
261
  const distance = options.distance || 100;
241
- return new Promise(async (resolve) => {
242
- const initialTransform = getComputedStyle(element).transform;
243
- const isReverse = options.direction === "reverse";
244
- if (!isReverse) {
245
- element.style.transform = `translateX(${distance}px)`;
246
- }
247
- this.enableGPUAcceleration(element);
248
- const motionId = await motionEngine.motion(
249
- element,
250
- [
251
- { progress: 0, properties: { translateX: isReverse ? 0 : distance } },
252
- { progress: 1, properties: { translateX: isReverse ? distance : 0 } }
253
- ],
254
- {
255
- duration: options.duration,
256
- easing: options.easing || this.getDefaultEasing(),
257
- delay: options.delay,
258
- onStart: options.onTransitionStart,
259
- onUpdate: (progress) => {
260
- const currentTranslateX = isReverse ? distance * progress : distance * (1 - progress);
261
- element.style.transform = `translateX(${currentTranslateX}px)`;
262
- },
263
- onComplete: () => {
264
- element.style.transform = initialTransform;
265
- options.onTransitionComplete?.();
266
- this.activeTransitions.delete(transitionId);
267
- resolve();
268
- }
262
+ const initialTransform = getComputedStyle(element).transform;
263
+ const isReverse = options.direction === "reverse";
264
+ return this.executeTransition(element, options, {
265
+ setup: () => {
266
+ if (!isReverse) {
267
+ element.style.transform = `translateX(${distance}px)`;
269
268
  }
270
- );
271
- this.activeTransitions.set(transitionId, motionId);
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;
280
+ }
272
281
  });
273
282
  }
274
283
  /**
275
284
  * 스케일 전환
276
285
  */
277
286
  async scale(element, options) {
278
- const transitionId = this.generateTransitionId();
279
287
  const scaleValue = options.scale || 0.8;
280
- return new Promise(async (resolve) => {
281
- const initialTransform = getComputedStyle(element).transform;
282
- const isReverse = options.direction === "reverse";
283
- if (!isReverse) {
284
- element.style.transform = `scale(${scaleValue})`;
285
- }
286
- this.enableGPUAcceleration(element);
287
- const motionId = await motionEngine.motion(
288
- element,
289
- [
290
- { progress: 0, properties: { scale: isReverse ? 1 : scaleValue } },
291
- { progress: 1, properties: { scale: isReverse ? scaleValue : 1 } }
292
- ],
293
- {
294
- duration: options.duration,
295
- easing: options.easing || this.getDefaultEasing(),
296
- delay: options.delay,
297
- onStart: options.onTransitionStart,
298
- onUpdate: (progress) => {
299
- const currentScale = isReverse ? 1 - (1 - scaleValue) * progress : scaleValue + (1 - scaleValue) * progress;
300
- element.style.transform = `scale(${currentScale})`;
301
- },
302
- onComplete: () => {
303
- element.style.transform = initialTransform;
304
- options.onTransitionComplete?.();
305
- this.activeTransitions.delete(transitionId);
306
- resolve();
307
- }
288
+ const initialTransform = getComputedStyle(element).transform;
289
+ const isReverse = options.direction === "reverse";
290
+ return this.executeTransition(element, options, {
291
+ setup: () => {
292
+ if (!isReverse) {
293
+ element.style.transform = `scale(${scaleValue})`;
308
294
  }
309
- );
310
- this.activeTransitions.set(transitionId, motionId);
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;
306
+ }
311
307
  });
312
308
  }
313
309
  /**
314
310
  * 플립 전환 (3D 회전)
315
311
  */
316
312
  async flip(element, options) {
317
- const transitionId = this.generateTransitionId();
318
313
  const perspective = options.perspective || 1e3;
319
- return new Promise(async (resolve) => {
320
- const initialTransform = getComputedStyle(element).transform;
321
- const isReverse = options.direction === "reverse";
322
- element.style.perspective = `${perspective}px`;
323
- element.style.transformStyle = "preserve-3d";
324
- if (!isReverse) {
325
- element.style.transform = `rotateY(90deg)`;
326
- }
327
- this.enableGPUAcceleration(element);
328
- const motionId = await motionEngine.motion(
329
- element,
330
- [
331
- { progress: 0, properties: { rotateY: isReverse ? 0 : 90 } },
332
- { progress: 1, properties: { rotateY: isReverse ? 90 : 0 } }
333
- ],
334
- {
335
- duration: options.duration,
336
- easing: options.easing || this.getDefaultEasing(),
337
- delay: options.delay,
338
- onStart: options.onTransitionStart,
339
- onUpdate: (progress) => {
340
- const currentRotateY = isReverse ? 90 * progress : 90 * (1 - progress);
341
- element.style.transform = `rotateY(${currentRotateY}deg)`;
342
- },
343
- onComplete: () => {
344
- element.style.transform = initialTransform;
345
- element.style.perspective = "";
346
- element.style.transformStyle = "";
347
- options.onTransitionComplete?.();
348
- this.activeTransitions.delete(transitionId);
349
- resolve();
350
- }
314
+ const initialTransform = getComputedStyle(element).transform;
315
+ const isReverse = options.direction === "reverse";
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)`;
351
322
  }
352
- );
353
- this.activeTransitions.set(transitionId, motionId);
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 = "";
336
+ }
354
337
  });
355
338
  }
356
339
  /**
357
340
  * 큐브 전환 (3D 큐브 회전)
358
341
  */
359
342
  async cube(element, options) {
360
- const transitionId = this.generateTransitionId();
361
343
  const perspective = options.perspective || 1200;
362
- return new Promise(async (resolve) => {
363
- const initialTransform = getComputedStyle(element).transform;
364
- const isReverse = options.direction === "reverse";
365
- element.style.perspective = `${perspective}px`;
366
- element.style.transformStyle = "preserve-3d";
367
- if (!isReverse) {
368
- element.style.transform = `rotateX(90deg) rotateY(45deg)`;
369
- }
370
- this.enableGPUAcceleration(element);
371
- const motionId = await motionEngine.motion(
372
- element,
373
- [
374
- { progress: 0, properties: { rotateX: isReverse ? 0 : 90, rotateY: isReverse ? 0 : 45 } },
375
- { progress: 1, properties: { rotateX: isReverse ? 90 : 0, rotateY: isReverse ? 45 : 0 } }
376
- ],
377
- {
378
- duration: options.duration,
379
- easing: options.easing || this.getDefaultEasing(),
380
- delay: options.delay,
381
- onStart: options.onTransitionStart,
382
- onUpdate: (progress) => {
383
- const currentRotateX = isReverse ? 90 * progress : 90 * (1 - progress);
384
- const currentRotateY = isReverse ? 45 * progress : 45 * (1 - progress);
385
- element.style.transform = `rotateX(${currentRotateX}deg) rotateY(${currentRotateY}deg)`;
386
- },
387
- onComplete: () => {
388
- element.style.transform = initialTransform;
389
- element.style.perspective = "";
390
- element.style.transformStyle = "";
391
- options.onTransitionComplete?.();
392
- this.activeTransitions.delete(transitionId);
393
- resolve();
394
- }
344
+ const initialTransform = getComputedStyle(element).transform;
345
+ const isReverse = options.direction === "reverse";
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)`;
395
352
  }
396
- );
397
- this.activeTransitions.set(transitionId, motionId);
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 = "";
367
+ }
398
368
  });
399
369
  }
400
370
  /**
401
371
  * 모프 전환 (복합 변형)
402
372
  */
403
373
  async morph(element, options) {
404
- const transitionId = this.generateTransitionId();
405
- return new Promise(async (resolve) => {
406
- const initialTransform = getComputedStyle(element).transform;
407
- const isReverse = options.direction === "reverse";
408
- if (!isReverse) {
409
- element.style.transform = `scale(0.9) rotate(5deg)`;
410
- }
411
- this.enableGPUAcceleration(element);
412
- const motionId = await motionEngine.motion(
413
- element,
414
- [
415
- { progress: 0, properties: { scale: isReverse ? 1 : 0.9, rotate: isReverse ? 0 : 5 } },
416
- { progress: 1, properties: { scale: isReverse ? 0.9 : 1, rotate: isReverse ? 5 : 0 } }
417
- ],
418
- {
419
- duration: options.duration,
420
- easing: options.easing || this.getDefaultEasing(),
421
- delay: options.delay,
422
- onStart: options.onTransitionStart,
423
- onUpdate: (progress) => {
424
- const currentScale = isReverse ? 1 - 0.1 * progress : 0.9 + 0.1 * progress;
425
- const currentRotate = isReverse ? 5 * progress : 5 * (1 - progress);
426
- element.style.transform = `scale(${currentScale}) rotate(${currentRotate}deg)`;
427
- },
428
- onComplete: () => {
429
- element.style.transform = initialTransform;
430
- options.onTransitionComplete?.();
431
- this.activeTransitions.delete(transitionId);
432
- resolve();
433
- }
374
+ const initialTransform = getComputedStyle(element).transform;
375
+ const isReverse = options.direction === "reverse";
376
+ return this.executeTransition(element, options, {
377
+ setup: () => {
378
+ if (!isReverse) {
379
+ element.style.transform = `scale(0.9) rotate(5deg)`;
434
380
  }
435
- );
436
- this.activeTransitions.set(transitionId, motionId);
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;
393
+ }
437
394
  });
438
395
  }
439
396
  /**
@@ -496,305 +453,6 @@ var TransitionEffects = class _TransitionEffects {
496
453
  };
497
454
  var transitionEffects = TransitionEffects.getInstance();
498
455
 
499
- // src/core/PerformanceOptimizer.ts
500
- var PerformanceOptimizer = class _PerformanceOptimizer {
501
- constructor() {
502
- this.performanceObserver = null;
503
- this.layerRegistry = /* @__PURE__ */ new Set();
504
- this.isMonitoring = false;
505
- this.config = {
506
- enableGPUAcceleration: true,
507
- enableLayerSeparation: true,
508
- enableMemoryOptimization: true,
509
- targetFPS: 60,
510
- maxLayerCount: 100,
511
- memoryThreshold: 50 * 1024 * 1024
512
- // 50MB
513
- };
514
- this.metrics = {
515
- fps: 0,
516
- layerCount: 0,
517
- activeMotions: 0
518
- };
519
- this.initializePerformanceMonitoring();
520
- }
521
- static getInstance() {
522
- if (!_PerformanceOptimizer.instance) {
523
- _PerformanceOptimizer.instance = new _PerformanceOptimizer();
524
- }
525
- return _PerformanceOptimizer.instance;
526
- }
527
- /**
528
- * 성능 모니터링 초기화
529
- */
530
- initializePerformanceMonitoring() {
531
- if (typeof PerformanceObserver !== "undefined") {
532
- try {
533
- this.performanceObserver = new PerformanceObserver((list) => {
534
- const entries = list.getEntries();
535
- this.updatePerformanceMetrics(entries);
536
- });
537
- this.performanceObserver.observe({ entryTypes: ["measure", "navigation"] });
538
- } catch (error) {
539
- if (process.env.NODE_ENV === "development") {
540
- console.warn("Performance monitoring not supported:", error);
541
- }
542
- }
543
- }
544
- }
545
- /**
546
- * 성능 메트릭 업데이트
547
- */
548
- updatePerformanceMetrics(entries) {
549
- entries.forEach((entry) => {
550
- if (entry.entryType === "measure") {
551
- this.calculateFPS();
552
- }
553
- });
554
- }
555
- /**
556
- * FPS 계산
557
- */
558
- calculateFPS() {
559
- const now = performance.now();
560
- const deltaTime = now - (this.lastFrameTime || now);
561
- this.lastFrameTime = now;
562
- if (deltaTime > 0) {
563
- this.metrics.fps = Math.round(1e3 / deltaTime);
564
- }
565
- }
566
- /**
567
- * GPU 가속 활성화
568
- */
569
- enableGPUAcceleration(element) {
570
- if (!this.config.enableGPUAcceleration) return;
571
- try {
572
- element.style.willChange = "transform, opacity";
573
- element.style.transform = "translateZ(0)";
574
- element.style.backfaceVisibility = "hidden";
575
- element.style.transformStyle = "preserve-3d";
576
- this.registerLayer(element);
577
- } catch (error) {
578
- if (process.env.NODE_ENV === "development") {
579
- console.warn("GPU acceleration failed:", error);
580
- }
581
- }
582
- }
583
- /**
584
- * 레이어 분리 및 최적화
585
- */
586
- createOptimizedLayer(element) {
587
- if (!this.config.enableLayerSeparation) return;
588
- try {
589
- element.style.transform = "translateZ(0)";
590
- element.style.backfaceVisibility = "hidden";
591
- element.style.perspective = "1000px";
592
- this.registerLayer(element);
593
- this.checkLayerLimit();
594
- } catch (error) {
595
- if (process.env.NODE_ENV === "development") {
596
- console.warn("Layer optimization failed:", error);
597
- }
598
- }
599
- }
600
- /**
601
- * 레이어 등록
602
- */
603
- registerLayer(element) {
604
- if (this.layerRegistry.has(element)) return;
605
- this.layerRegistry.add(element);
606
- this.metrics.layerCount = this.layerRegistry.size;
607
- this.checkMemoryUsage();
608
- }
609
- /**
610
- * 레이어 제거
611
- */
612
- removeLayer(element) {
613
- if (this.layerRegistry.has(element)) {
614
- this.layerRegistry.delete(element);
615
- this.metrics.layerCount = this.layerRegistry.size;
616
- element.style.willChange = "auto";
617
- element.style.transform = "";
618
- element.style.backfaceVisibility = "";
619
- element.style.transformStyle = "";
620
- element.style.perspective = "";
621
- }
622
- }
623
- /**
624
- * 레이어 수 제한 체크
625
- */
626
- checkLayerLimit() {
627
- if (this.metrics.layerCount > this.config.maxLayerCount) {
628
- if (process.env.NODE_ENV === "development") {
629
- console.warn(`Layer count (${this.metrics.layerCount}) exceeds limit (${this.config.maxLayerCount})`);
630
- }
631
- this.cleanupOldLayers();
632
- }
633
- }
634
- /**
635
- * 오래된 레이어 정리
636
- */
637
- cleanupOldLayers() {
638
- const layersToRemove = Array.from(this.layerRegistry).slice(0, 10);
639
- layersToRemove.forEach((layer) => {
640
- this.removeLayer(layer);
641
- });
642
- }
643
- /**
644
- * 메모리 사용량 체크
645
- */
646
- checkMemoryUsage() {
647
- if (!this.config.enableMemoryOptimization) return;
648
- if ("memory" in performance) {
649
- const memory = performance.memory;
650
- this.metrics.memoryUsage = memory.usedJSHeapSize;
651
- if (memory.usedJSHeapSize > this.config.memoryThreshold) {
652
- if (process.env.NODE_ENV === "development") {
653
- console.warn("Memory usage high, cleaning up...");
654
- }
655
- this.cleanupMemory();
656
- }
657
- }
658
- }
659
- /**
660
- * 메모리 정리
661
- */
662
- cleanupMemory() {
663
- if ("gc" in window) {
664
- try {
665
- window.gc();
666
- } catch (error) {
667
- }
668
- }
669
- this.cleanupOldLayers();
670
- }
671
- /**
672
- * 성능 최적화 설정 업데이트
673
- */
674
- updateConfig(newConfig) {
675
- this.config = { ...this.config, ...newConfig };
676
- if (!this.config.enableGPUAcceleration) {
677
- this.disableAllGPUAcceleration();
678
- }
679
- if (!this.config.enableLayerSeparation) {
680
- this.disableAllLayers();
681
- }
682
- }
683
- /**
684
- * 모든 GPU 가속 비활성화
685
- */
686
- disableAllGPUAcceleration() {
687
- this.layerRegistry.forEach((element) => {
688
- element.style.willChange = "auto";
689
- element.style.transform = "";
690
- });
691
- }
692
- /**
693
- * 모든 레이어 비활성화
694
- */
695
- disableAllLayers() {
696
- this.layerRegistry.forEach((element) => {
697
- this.removeLayer(element);
698
- });
699
- }
700
- /**
701
- * 성능 메트릭 가져오기
702
- */
703
- getMetrics() {
704
- return { ...this.metrics };
705
- }
706
- /**
707
- * 성능 모니터링 시작
708
- */
709
- startMonitoring() {
710
- if (this.isMonitoring) return;
711
- this.isMonitoring = true;
712
- this.monitoringInterval = setInterval(() => {
713
- this.updateMetrics();
714
- }, 1e3);
715
- }
716
- /**
717
- * 성능 모니터링 중지
718
- */
719
- stopMonitoring() {
720
- if (!this.isMonitoring) return;
721
- this.isMonitoring = false;
722
- if (this.monitoringInterval) {
723
- clearInterval(this.monitoringInterval);
724
- this.monitoringInterval = void 0;
725
- }
726
- }
727
- /**
728
- * 메트릭 업데이트
729
- */
730
- updateMetrics() {
731
- if ("memory" in performance) {
732
- const memory = performance.memory;
733
- this.metrics.memoryUsage = memory.usedJSHeapSize;
734
- }
735
- this.metrics.layerCount = this.layerRegistry.size;
736
- }
737
- /**
738
- * 성능 리포트 생성
739
- */
740
- generateReport() {
741
- const metrics = this.getMetrics();
742
- return `
743
- === HUA Motion Performance Report ===
744
- FPS: ${metrics.fps}
745
- Active Layers: ${metrics.layerCount}
746
- Memory Usage: ${this.formatBytes(metrics.memoryUsage || 0)}
747
- Active Motions: ${metrics.activeMotions}
748
- GPU Acceleration: ${this.config.enableGPUAcceleration ? "Enabled" : "Disabled"}
749
- Layer Separation: ${this.config.enableLayerSeparation ? "Enabled" : "Disabled"}
750
- Memory Optimization: ${this.config.enableMemoryOptimization ? "Enabled" : "Disabled"}
751
- =====================================
752
- `.trim();
753
- }
754
- /**
755
- * 바이트 단위 포맷팅
756
- */
757
- formatBytes(bytes) {
758
- if (bytes === 0) return "0 B";
759
- const k = 1024;
760
- const sizes = ["B", "KB", "MB", "GB"];
761
- const i = Math.floor(Math.log(bytes) / Math.log(k));
762
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
763
- }
764
- /**
765
- * 성능 최적화 권장사항
766
- */
767
- getOptimizationRecommendations() {
768
- const recommendations = [];
769
- const metrics = this.getMetrics();
770
- if (metrics.fps < this.config.targetFPS) {
771
- 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.");
772
- }
773
- if (metrics.layerCount > this.config.maxLayerCount * 0.8) {
774
- recommendations.push("\uB808\uC774\uC5B4 \uC218\uAC00 \uB9CE\uC2B5\uB2C8\uB2E4. \uBD88\uD544\uC694\uD55C \uB808\uC774\uC5B4\uB97C \uC815\uB9AC\uD558\uC138\uC694.");
775
- }
776
- if (metrics.memoryUsage && metrics.memoryUsage > this.config.memoryThreshold * 0.8) {
777
- recommendations.push("\uBA54\uBAA8\uB9AC \uC0AC\uC6A9\uB7C9\uC774 \uB192\uC2B5\uB2C8\uB2E4. \uBA54\uBAA8\uB9AC \uC815\uB9AC\uB97C \uACE0\uB824\uD558\uC138\uC694.");
778
- }
779
- return recommendations;
780
- }
781
- /**
782
- * 정리
783
- */
784
- destroy() {
785
- this.stopMonitoring();
786
- if (this.performanceObserver) {
787
- this.performanceObserver.disconnect();
788
- this.performanceObserver = null;
789
- }
790
- this.layerRegistry.forEach((element) => {
791
- this.removeLayer(element);
792
- });
793
- this.layerRegistry.clear();
794
- }
795
- };
796
- var performanceOptimizer = PerformanceOptimizer.getInstance();
797
-
798
456
  // src/presets/index.ts
799
457
  var MOTION_PRESETS = {
800
458
  hero: {
@@ -906,10 +564,10 @@ function useSimplePageMotions(config) {
906
564
  const calculateMotionValues = useCallback((isVisible, elementConfig) => {
907
565
  const preset = getMotionPreset(elementConfig.type);
908
566
  mergeWithPreset(preset, elementConfig);
909
- let opacity = isVisible ? 1 : 0;
910
- let translateY = isVisible ? 0 : 20;
911
- let translateX = 0;
912
- let scale = isVisible ? 1 : 0.95;
567
+ const opacity = isVisible ? 1 : 0;
568
+ const translateY = isVisible ? 0 : 20;
569
+ const translateX = 0;
570
+ const scale = isVisible ? 1 : 0.95;
913
571
  return { opacity, translateY, translateX, scale };
914
572
  }, []);
915
573
  useEffect(() => {
@@ -1169,7 +827,7 @@ function usePageMotions(config) {
1169
827
  const mergedConfig = mergeWithPreset(preset, elementConfig);
1170
828
  let opacity = state.finalVisibility ? 1 : 0;
1171
829
  let translateY = state.finalVisibility ? 0 : 20;
1172
- let translateX = 0;
830
+ const translateX = 0;
1173
831
  let scale = state.finalVisibility ? 1 : 0.95;
1174
832
  if (mergedConfig.hover && state.isHovered) {
1175
833
  scale *= 1.1;
@@ -1641,52 +1299,343 @@ function useSmartMotion(options = {}) {
1641
1299
  isClicked: state.isClicked
1642
1300
  };
1643
1301
  }
1644
- function getInitialStyle(type, distance) {
1645
- switch (type) {
1646
- case "slideUp":
1647
- return { opacity: 0, transform: `translateY(${distance}px)` };
1648
- case "slideLeft":
1649
- return { opacity: 0, transform: `translateX(${distance}px)` };
1650
- case "slideRight":
1651
- return { opacity: 0, transform: `translateX(-${distance}px)` };
1652
- case "scaleIn":
1653
- return { opacity: 0, transform: "scale(0)" };
1654
- case "bounceIn":
1655
- return { opacity: 0, transform: "scale(0)" };
1656
- case "fadeIn":
1657
- default:
1658
- 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;
1659
1405
  }
1406
+ return profile;
1660
1407
  }
1661
- function getVisibleStyle() {
1662
- return { opacity: 1, transform: "none" };
1408
+ function mergeProfileOverrides(base, overrides) {
1409
+ return deepMerge(
1410
+ base,
1411
+ overrides
1412
+ );
1663
1413
  }
1664
- function getEasingForType(type, easing2) {
1665
- if (easing2) return easing2;
1666
- if (type === "bounceIn") return "cubic-bezier(0.34, 1.56, 0.64, 1)";
1667
- return "ease-out";
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
+ }
1427
+ }
1428
+ return result;
1668
1429
  }
1669
- function useUnifiedMotion(options) {
1670
- const {
1671
- type,
1672
- duration = 600,
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 : {};
1557
+ const direction = config.direction ?? "up";
1558
+ const distance = config.distance ?? defaultDistance;
1559
+ switch (direction) {
1560
+ case "up":
1561
+ transforms.push(`translateY(${distance}px)`);
1562
+ break;
1563
+ case "down":
1564
+ transforms.push(`translateY(-${distance}px)`);
1565
+ break;
1566
+ case "left":
1567
+ transforms.push(`translateX(${distance}px)`);
1568
+ break;
1569
+ case "right":
1570
+ transforms.push(`translateX(-${distance}px)`);
1571
+ break;
1572
+ }
1573
+ if (!effects.fade) style.opacity = 0;
1574
+ }
1575
+ if (effects.scale) {
1576
+ const config = typeof effects.scale === "object" ? effects.scale : {};
1577
+ const from = config.from ?? 0.95;
1578
+ transforms.push(`scale(${from})`);
1579
+ if (!effects.fade && !effects.slide) style.opacity = 0;
1580
+ }
1581
+ if (effects.bounce) {
1582
+ transforms.push("scale(0)");
1583
+ if (!effects.fade && !effects.slide && !effects.scale) style.opacity = 0;
1584
+ }
1585
+ if (transforms.length > 0) {
1586
+ style.transform = transforms.join(" ");
1587
+ } else {
1588
+ style.transform = "none";
1589
+ }
1590
+ if (effects.fade && transforms.length === 0) {
1591
+ style.transform = "none";
1592
+ }
1593
+ return style;
1594
+ }
1595
+ function getMultiEffectVisibleStyle(effects) {
1596
+ const style = {};
1597
+ if (effects.fade) {
1598
+ const config = typeof effects.fade === "object" ? effects.fade : {};
1599
+ style.opacity = config.targetOpacity ?? 1;
1600
+ } else {
1601
+ style.opacity = 1;
1602
+ }
1603
+ if (effects.scale) {
1604
+ const config = typeof effects.scale === "object" ? effects.scale : {};
1605
+ style.transform = `scale(${config.to ?? 1})`;
1606
+ } else {
1607
+ style.transform = "none";
1608
+ }
1609
+ return style;
1610
+ }
1611
+ function getMultiEffectEasing(effects, easing2) {
1612
+ if (easing2) return easing2;
1613
+ if (effects.bounce) return "cubic-bezier(0.34, 1.56, 0.64, 1)";
1614
+ return "ease-out";
1615
+ }
1616
+ function useUnifiedMotion(options) {
1617
+ const profile = useMotionProfile();
1618
+ const {
1619
+ type,
1620
+ effects,
1621
+ duration = profile.base.duration,
1673
1622
  autoStart = true,
1674
1623
  delay = 0,
1675
1624
  easing: easing2,
1676
- threshold = 0.1,
1677
- triggerOnce = true,
1678
- distance = 50,
1625
+ threshold = profile.base.threshold,
1626
+ triggerOnce = profile.base.triggerOnce,
1627
+ distance = profile.entrance.slide.distance,
1679
1628
  onComplete,
1680
1629
  onStart,
1681
1630
  onStop,
1682
1631
  onReset
1683
1632
  } = options;
1684
- const resolvedEasing = getEasingForType(type, easing2);
1633
+ const resolvedType = type ?? "fadeIn";
1634
+ const resolvedEasing = getEasingForType(resolvedType, easing2);
1685
1635
  const ref = useRef(null);
1686
1636
  const [isVisible, setIsVisible] = useState(false);
1687
1637
  const [isAnimating, setIsAnimating] = useState(false);
1688
1638
  const [progress, setProgress] = useState(0);
1689
- const observerRef = useRef(null);
1690
1639
  const timeoutRef = useRef(null);
1691
1640
  const startRef = useRef(() => {
1692
1641
  });
@@ -1719,29 +1668,32 @@ function useUnifiedMotion(options) {
1719
1668
  }, [stop, onReset]);
1720
1669
  useEffect(() => {
1721
1670
  if (!ref.current || !autoStart) return;
1722
- observerRef.current = new IntersectionObserver(
1723
- (entries) => {
1724
- entries.forEach((entry) => {
1725
- if (entry.isIntersecting) {
1726
- startRef.current();
1727
- if (triggerOnce) {
1728
- observerRef.current?.disconnect();
1729
- }
1730
- }
1731
- });
1671
+ return observeElement(
1672
+ ref.current,
1673
+ (entry) => {
1674
+ if (entry.isIntersecting) startRef.current();
1732
1675
  },
1733
- { threshold }
1676
+ { threshold },
1677
+ triggerOnce
1734
1678
  );
1735
- observerRef.current.observe(ref.current);
1736
- return () => {
1737
- observerRef.current?.disconnect();
1738
- };
1739
1679
  }, [autoStart, threshold, triggerOnce]);
1740
1680
  useEffect(() => {
1741
1681
  return () => stop();
1742
1682
  }, [stop]);
1743
1683
  const style = useMemo(() => {
1744
- const base = isVisible ? getVisibleStyle() : getInitialStyle(type, distance);
1684
+ if (effects) {
1685
+ const base2 = isVisible ? getMultiEffectVisibleStyle(effects) : getMultiEffectInitialStyle(effects, distance);
1686
+ const resolvedEasingMulti = getMultiEffectEasing(effects, easing2);
1687
+ return {
1688
+ ...base2,
1689
+ transition: `all ${duration}ms ${resolvedEasingMulti}`,
1690
+ "--motion-delay": `${delay}ms`,
1691
+ "--motion-duration": `${duration}ms`,
1692
+ "--motion-easing": resolvedEasingMulti,
1693
+ "--motion-progress": `${progress}`
1694
+ };
1695
+ }
1696
+ const base = isVisible ? getVisibleStyle() : getInitialStyle(resolvedType, distance);
1745
1697
  return {
1746
1698
  ...base,
1747
1699
  transition: `all ${duration}ms ${resolvedEasing}`,
@@ -1750,7 +1702,7 @@ function useUnifiedMotion(options) {
1750
1702
  "--motion-easing": resolvedEasing,
1751
1703
  "--motion-progress": `${progress}`
1752
1704
  };
1753
- }, [isVisible, type, distance, duration, resolvedEasing, delay, progress]);
1705
+ }, [isVisible, type, effects, distance, duration, resolvedEasing, easing2, delay, progress, resolvedType]);
1754
1706
  return {
1755
1707
  ref,
1756
1708
  isVisible,
@@ -1763,14 +1715,15 @@ function useUnifiedMotion(options) {
1763
1715
  };
1764
1716
  }
1765
1717
  function useFadeIn(options = {}) {
1718
+ const profile = useMotionProfile();
1766
1719
  const {
1767
1720
  delay = 0,
1768
- duration = 700,
1769
- threshold = 0.1,
1770
- triggerOnce = true,
1771
- 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,
1772
1725
  autoStart = true,
1773
- initialOpacity = 0,
1726
+ initialOpacity = profile.entrance.fade.initialOpacity,
1774
1727
  targetOpacity = 1,
1775
1728
  onComplete,
1776
1729
  onStart,
@@ -1782,7 +1735,6 @@ function useFadeIn(options = {}) {
1782
1735
  const [isAnimating, setIsAnimating] = useState(false);
1783
1736
  const [progress, setProgress] = useState(0);
1784
1737
  const [nodeReady, setNodeReady] = useState(false);
1785
- const observerRef = useRef(null);
1786
1738
  const motionRef = useRef(null);
1787
1739
  const timeoutRef = useRef(null);
1788
1740
  const startRef = useRef(() => {
@@ -1841,31 +1793,15 @@ function useFadeIn(options = {}) {
1841
1793
  }, [stop, onReset]);
1842
1794
  useEffect(() => {
1843
1795
  if (!ref.current || !autoStart) return;
1844
- observerRef.current = new IntersectionObserver(
1845
- (entries) => {
1846
- entries.forEach((entry) => {
1847
- if (entry.isIntersecting) {
1848
- startRef.current();
1849
- if (triggerOnce) {
1850
- observerRef.current?.disconnect();
1851
- }
1852
- }
1853
- });
1796
+ return observeElement(
1797
+ ref.current,
1798
+ (entry) => {
1799
+ if (entry.isIntersecting) startRef.current();
1854
1800
  },
1855
- { threshold }
1801
+ { threshold },
1802
+ triggerOnce
1856
1803
  );
1857
- observerRef.current.observe(ref.current);
1858
- return () => {
1859
- if (observerRef.current) {
1860
- observerRef.current.disconnect();
1861
- }
1862
- };
1863
1804
  }, [autoStart, threshold, triggerOnce, nodeReady]);
1864
- useEffect(() => {
1865
- if (!autoStart) {
1866
- start();
1867
- }
1868
- }, [autoStart, start]);
1869
1805
  useEffect(() => {
1870
1806
  return () => {
1871
1807
  stop();
@@ -1891,15 +1827,16 @@ function useFadeIn(options = {}) {
1891
1827
  };
1892
1828
  }
1893
1829
  function useSlideUp(options = {}) {
1830
+ const profile = useMotionProfile();
1894
1831
  const {
1895
1832
  delay = 0,
1896
- duration = 700,
1897
- threshold = 0.1,
1898
- triggerOnce = true,
1899
- 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,
1900
1837
  autoStart = true,
1901
1838
  direction = "up",
1902
- distance = 50,
1839
+ distance = profile.entrance.slide.distance,
1903
1840
  onComplete,
1904
1841
  onStart,
1905
1842
  onStop,
@@ -1910,7 +1847,6 @@ function useSlideUp(options = {}) {
1910
1847
  const [isAnimating, setIsAnimating] = useState(false);
1911
1848
  const [progress, setProgress] = useState(0);
1912
1849
  const [nodeReady, setNodeReady] = useState(false);
1913
- const observerRef = useRef(null);
1914
1850
  const timeoutRef = useRef(null);
1915
1851
  const startRef = useRef(() => {
1916
1852
  });
@@ -1928,7 +1864,7 @@ function useSlideUp(options = {}) {
1928
1864
  }, 50);
1929
1865
  return () => clearInterval(id);
1930
1866
  }, [nodeReady]);
1931
- const getInitialTransform = useCallback(() => {
1867
+ const getInitialTransform2 = useCallback(() => {
1932
1868
  switch (direction) {
1933
1869
  case "up":
1934
1870
  return `translateY(${distance}px)`;
@@ -1978,37 +1914,21 @@ function useSlideUp(options = {}) {
1978
1914
  }, [stop, onReset]);
1979
1915
  useEffect(() => {
1980
1916
  if (!ref.current || !autoStart) return;
1981
- observerRef.current = new IntersectionObserver(
1982
- (entries) => {
1983
- entries.forEach((entry) => {
1984
- if (entry.isIntersecting) {
1985
- startRef.current();
1986
- if (triggerOnce) {
1987
- observerRef.current?.disconnect();
1988
- }
1989
- }
1990
- });
1917
+ return observeElement(
1918
+ ref.current,
1919
+ (entry) => {
1920
+ if (entry.isIntersecting) startRef.current();
1991
1921
  },
1992
- { threshold }
1922
+ { threshold },
1923
+ triggerOnce
1993
1924
  );
1994
- observerRef.current.observe(ref.current);
1995
- return () => {
1996
- if (observerRef.current) {
1997
- observerRef.current.disconnect();
1998
- }
1999
- };
2000
1925
  }, [autoStart, threshold, triggerOnce, nodeReady]);
2001
- useEffect(() => {
2002
- if (!autoStart) {
2003
- start();
2004
- }
2005
- }, [autoStart, start]);
2006
1926
  useEffect(() => {
2007
1927
  return () => {
2008
1928
  stop();
2009
1929
  };
2010
1930
  }, [stop]);
2011
- const initialTransform = useMemo(() => getInitialTransform(), [getInitialTransform]);
1931
+ const initialTransform = useMemo(() => getInitialTransform2(), [getInitialTransform2]);
2012
1932
  const finalTransform = useMemo(() => {
2013
1933
  return direction === "left" || direction === "right" ? "translateX(0)" : "translateY(0)";
2014
1934
  }, [direction]);
@@ -2045,15 +1965,16 @@ function useSlideRight(options = {}) {
2045
1965
  return useSlideUp({ ...options, direction: "right" });
2046
1966
  }
2047
1967
  function useScaleIn(options = {}) {
1968
+ const profile = useMotionProfile();
2048
1969
  const {
2049
1970
  initialScale = 0,
2050
1971
  targetScale = 1,
2051
- duration = 700,
1972
+ duration = profile.base.duration,
2052
1973
  delay = 0,
2053
1974
  autoStart = true,
2054
- easing: easing2 = "ease-out",
2055
- threshold = 0.1,
2056
- triggerOnce = true,
1975
+ easing: easing2 = profile.base.easing,
1976
+ threshold = profile.base.threshold,
1977
+ triggerOnce = profile.base.triggerOnce,
2057
1978
  onComplete,
2058
1979
  onStart,
2059
1980
  onStop,
@@ -2065,7 +1986,6 @@ function useScaleIn(options = {}) {
2065
1986
  const [isAnimating, setIsAnimating] = useState(false);
2066
1987
  const [isVisible, setIsVisible] = useState(autoStart ? false : true);
2067
1988
  const [progress, setProgress] = useState(autoStart ? 0 : 1);
2068
- const observerRef = useRef(null);
2069
1989
  const timeoutRef = useRef(null);
2070
1990
  const startRef = useRef(() => {
2071
1991
  });
@@ -2113,25 +2033,14 @@ function useScaleIn(options = {}) {
2113
2033
  }, [stop, initialScale, onReset]);
2114
2034
  useEffect(() => {
2115
2035
  if (!ref.current || !autoStart) return;
2116
- observerRef.current = new IntersectionObserver(
2117
- (entries) => {
2118
- entries.forEach((entry) => {
2119
- if (entry.isIntersecting) {
2120
- startRef.current();
2121
- if (triggerOnce) {
2122
- observerRef.current?.disconnect();
2123
- }
2124
- }
2125
- });
2036
+ return observeElement(
2037
+ ref.current,
2038
+ (entry) => {
2039
+ if (entry.isIntersecting) startRef.current();
2126
2040
  },
2127
- { threshold }
2041
+ { threshold },
2042
+ triggerOnce
2128
2043
  );
2129
- observerRef.current.observe(ref.current);
2130
- return () => {
2131
- if (observerRef.current) {
2132
- observerRef.current.disconnect();
2133
- }
2134
- };
2135
2044
  }, [autoStart, threshold, triggerOnce]);
2136
2045
  useEffect(() => {
2137
2046
  return () => {
@@ -2159,15 +2068,15 @@ function useScaleIn(options = {}) {
2159
2068
  };
2160
2069
  }
2161
2070
  function useBounceIn(options = {}) {
2071
+ const profile = useMotionProfile();
2162
2072
  const {
2163
2073
  duration = 600,
2164
2074
  delay = 0,
2165
2075
  autoStart = true,
2166
- intensity = 0.3,
2167
- threshold = 0.1,
2168
- triggerOnce = true,
2169
- easing: easing2 = "cubic-bezier(0.34, 1.56, 0.64, 1)",
2170
- // 바운스 이징
2076
+ intensity = profile.entrance.bounce.intensity,
2077
+ threshold = profile.base.threshold,
2078
+ triggerOnce = profile.base.triggerOnce,
2079
+ easing: easing2 = profile.entrance.bounce.easing,
2171
2080
  onComplete,
2172
2081
  onStart,
2173
2082
  onStop,
@@ -2179,7 +2088,6 @@ function useBounceIn(options = {}) {
2179
2088
  const [isAnimating, setIsAnimating] = useState(false);
2180
2089
  const [isVisible, setIsVisible] = useState(autoStart ? false : true);
2181
2090
  const [progress, setProgress] = useState(autoStart ? 0 : 1);
2182
- const observerRef = useRef(null);
2183
2091
  const timeoutRef = useRef(null);
2184
2092
  const bounceTimeoutRef = useRef(null);
2185
2093
  const startRef = useRef(() => {
@@ -2236,25 +2144,14 @@ function useBounceIn(options = {}) {
2236
2144
  }, [stop, onReset]);
2237
2145
  useEffect(() => {
2238
2146
  if (!ref.current || !autoStart) return;
2239
- observerRef.current = new IntersectionObserver(
2240
- (entries) => {
2241
- entries.forEach((entry) => {
2242
- if (entry.isIntersecting) {
2243
- startRef.current();
2244
- if (triggerOnce) {
2245
- observerRef.current?.disconnect();
2246
- }
2247
- }
2248
- });
2147
+ return observeElement(
2148
+ ref.current,
2149
+ (entry) => {
2150
+ if (entry.isIntersecting) startRef.current();
2249
2151
  },
2250
- { threshold }
2152
+ { threshold },
2153
+ triggerOnce
2251
2154
  );
2252
- observerRef.current.observe(ref.current);
2253
- return () => {
2254
- if (observerRef.current) {
2255
- observerRef.current.disconnect();
2256
- }
2257
- };
2258
2155
  }, [autoStart, threshold, triggerOnce]);
2259
2156
  useEffect(() => {
2260
2157
  return () => {
@@ -2579,18 +2476,33 @@ function usePulse(options = {}) {
2579
2476
  reset
2580
2477
  };
2581
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
2582
2493
  function useSpringMotion(options) {
2494
+ const profile = useMotionProfile();
2583
2495
  const {
2584
2496
  from,
2585
2497
  to,
2586
- mass = 1,
2587
- stiffness = 100,
2588
- damping = 10,
2589
- restDelta = 0.01,
2590
- 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,
2591
2503
  onComplete,
2592
2504
  enabled = true,
2593
- autoStart = false
2505
+ autoStart = true
2594
2506
  } = options;
2595
2507
  const ref = useRef(null);
2596
2508
  const [springState, setSpringState] = useState({
@@ -2602,16 +2514,7 @@ function useSpringMotion(options) {
2602
2514
  const [progress, setProgress] = useState(0);
2603
2515
  const motionRef = useRef(null);
2604
2516
  const lastTimeRef = useRef(0);
2605
- const calculateSpring = useCallback((currentValue, currentVelocity, targetValue, deltaTime) => {
2606
- const displacement = currentValue - targetValue;
2607
- const springForce = -stiffness * displacement;
2608
- const dampingForce = -damping * currentVelocity;
2609
- const totalForce = springForce + dampingForce;
2610
- const acceleration = totalForce / mass;
2611
- const newVelocity = currentVelocity + acceleration * deltaTime;
2612
- const newValue = currentValue + newVelocity * deltaTime;
2613
- return { value: newValue, velocity: newVelocity };
2614
- }, [mass, stiffness, damping]);
2517
+ const springConfig = useMemo(() => ({ stiffness, damping, mass }), [stiffness, damping, mass]);
2615
2518
  const animate = useCallback((currentTime) => {
2616
2519
  if (!enabled || !springState.isAnimating) return;
2617
2520
  const deltaTime = Math.min(currentTime - lastTimeRef.current, 16) / 1e3;
@@ -2620,7 +2523,8 @@ function useSpringMotion(options) {
2620
2523
  springState.value,
2621
2524
  springState.velocity,
2622
2525
  to,
2623
- deltaTime
2526
+ deltaTime,
2527
+ springConfig
2624
2528
  );
2625
2529
  const range = Math.abs(to - from);
2626
2530
  const currentProgress = range > 0 ? Math.min(Math.abs(value - from) / range, 1) : 1;
@@ -2642,7 +2546,7 @@ function useSpringMotion(options) {
2642
2546
  isAnimating: true
2643
2547
  });
2644
2548
  motionRef.current = requestAnimationFrame(animate);
2645
- }, [enabled, springState.isAnimating, to, from, restDelta, restSpeed, onComplete, calculateSpring]);
2549
+ }, [enabled, springState.isAnimating, to, from, restDelta, restSpeed, onComplete, springConfig]);
2646
2550
  const start = useCallback(() => {
2647
2551
  if (springState.isAnimating) return;
2648
2552
  setSpringState((prev) => ({
@@ -2790,11 +2694,12 @@ function useGradient(options = {}) {
2790
2694
  };
2791
2695
  }
2792
2696
  function useHoverMotion(options = {}) {
2697
+ const profile = useMotionProfile();
2793
2698
  const {
2794
- duration = 200,
2795
- easing: easing2 = "ease-out",
2796
- hoverScale = 1.05,
2797
- 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,
2798
2703
  hoverOpacity = 1
2799
2704
  } = options;
2800
2705
  const ref = useRef(null);
@@ -3078,13 +2983,14 @@ function useFocusToggle(options = {}) {
3078
2983
  };
3079
2984
  }
3080
2985
  function useScrollReveal(options = {}) {
2986
+ const profile = useMotionProfile();
3081
2987
  const {
3082
- threshold = 0.1,
2988
+ threshold = profile.base.threshold,
3083
2989
  rootMargin = "0px",
3084
- triggerOnce = true,
2990
+ triggerOnce = profile.base.triggerOnce,
3085
2991
  delay = 0,
3086
- duration = 700,
3087
- easing: easing2 = "ease-out",
2992
+ duration = profile.base.duration,
2993
+ easing: easing2 = profile.base.easing,
3088
2994
  motionType = "fadeIn",
3089
2995
  onComplete,
3090
2996
  onStart,
@@ -3113,15 +3019,14 @@ function useScrollReveal(options = {}) {
3113
3019
  }, [triggerOnce, hasTriggered, delay, onStart, onComplete]);
3114
3020
  useEffect(() => {
3115
3021
  if (!ref.current) return;
3116
- const observer = new IntersectionObserver(observerCallback, {
3117
- threshold,
3118
- rootMargin
3119
- });
3120
- observer.observe(ref.current);
3121
- return () => {
3122
- observer.disconnect();
3123
- };
3022
+ return observeElement(
3023
+ ref.current,
3024
+ (entry) => observerCallback([entry]),
3025
+ { threshold, rootMargin }
3026
+ );
3124
3027
  }, [observerCallback, threshold, rootMargin]);
3028
+ const slideDistance = profile.entrance.slide.distance;
3029
+ const scaleFrom = profile.entrance.scale.from;
3125
3030
  const style = useMemo(() => {
3126
3031
  const baseTransition = `all ${duration}ms ${easing2}`;
3127
3032
  if (!isVisible) {
@@ -3134,25 +3039,25 @@ function useScrollReveal(options = {}) {
3134
3039
  case "slideUp":
3135
3040
  return {
3136
3041
  opacity: 0,
3137
- transform: "translateY(32px)",
3042
+ transform: `translateY(${slideDistance}px)`,
3138
3043
  transition: baseTransition
3139
3044
  };
3140
3045
  case "slideLeft":
3141
3046
  return {
3142
3047
  opacity: 0,
3143
- transform: "translateX(-32px)",
3048
+ transform: `translateX(-${slideDistance}px)`,
3144
3049
  transition: baseTransition
3145
3050
  };
3146
3051
  case "slideRight":
3147
3052
  return {
3148
3053
  opacity: 0,
3149
- transform: "translateX(32px)",
3054
+ transform: `translateX(${slideDistance}px)`,
3150
3055
  transition: baseTransition
3151
3056
  };
3152
3057
  case "scaleIn":
3153
3058
  return {
3154
3059
  opacity: 0,
3155
- transform: "scale(0.95)",
3060
+ transform: `scale(${scaleFrom})`,
3156
3061
  transition: baseTransition
3157
3062
  };
3158
3063
  case "bounceIn":
@@ -3173,7 +3078,7 @@ function useScrollReveal(options = {}) {
3173
3078
  transform: "none",
3174
3079
  transition: baseTransition
3175
3080
  };
3176
- }, [isVisible, motionType, duration, easing2]);
3081
+ }, [isVisible, motionType, duration, easing2, slideDistance, scaleFrom]);
3177
3082
  const start = useCallback(() => {
3178
3083
  setIsAnimating(true);
3179
3084
  onStart?.();
@@ -3206,6 +3111,38 @@ function useScrollReveal(options = {}) {
3206
3111
  stop
3207
3112
  };
3208
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;
3209
3146
  function useScrollProgress(options = {}) {
3210
3147
  const {
3211
3148
  target,
@@ -3214,27 +3151,24 @@ function useScrollProgress(options = {}) {
3214
3151
  } = options;
3215
3152
  const [progress, setProgress] = useState(showOnMount ? 0 : 0);
3216
3153
  const [mounted, setMounted] = useState(false);
3154
+ const progressRef = useRef(progress);
3217
3155
  useEffect(() => {
3218
3156
  setMounted(true);
3219
3157
  }, []);
3220
3158
  useEffect(() => {
3221
3159
  if (!mounted) return;
3222
3160
  const calculateProgress = () => {
3223
- if (typeof window !== "undefined") {
3224
- const scrollTop = window.pageYOffset;
3225
- const scrollHeight = target || document.documentElement.scrollHeight - window.innerHeight;
3226
- const adjustedScrollTop = Math.max(0, scrollTop - offset);
3227
- const progressPercent = Math.min(100, Math.max(0, adjustedScrollTop / scrollHeight * 100));
3228
- 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);
3229
3168
  }
3230
3169
  };
3231
3170
  calculateProgress();
3232
- window.addEventListener("scroll", calculateProgress, { passive: true });
3233
- window.addEventListener("resize", calculateProgress, { passive: true });
3234
- return () => {
3235
- window.removeEventListener("scroll", calculateProgress);
3236
- window.removeEventListener("resize", calculateProgress);
3237
- };
3171
+ return subscribeScroll(calculateProgress);
3238
3172
  }, [target, offset, mounted]);
3239
3173
  return {
3240
3174
  progress,
@@ -3619,14 +3553,11 @@ function useInView(options = {}) {
3619
3553
  useEffect(() => {
3620
3554
  const element = ref.current;
3621
3555
  if (!element) return;
3622
- const observer = new IntersectionObserver(handleIntersect, {
3623
- threshold,
3624
- rootMargin
3625
- });
3626
- observer.observe(element);
3627
- return () => {
3628
- observer.disconnect();
3629
- };
3556
+ return observeElement(
3557
+ element,
3558
+ (entry2) => handleIntersect([entry2]),
3559
+ { threshold, rootMargin }
3560
+ );
3630
3561
  }, [threshold, rootMargin, handleIntersect]);
3631
3562
  return {
3632
3563
  ref,
@@ -4188,7 +4119,1671 @@ function useGestureMotion(options) {
4188
4119
  isActive: gestureState.isActive
4189
4120
  };
4190
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();
5222
+ return {
5223
+ ref,
5224
+ isVisible,
5225
+ isAnimating,
5226
+ style,
5227
+ progress,
5228
+ start,
5229
+ stop,
5230
+ reset,
5231
+ pause,
5232
+ resume,
5233
+ isOpen,
5234
+ activeIndex,
5235
+ itemStyles,
5236
+ openMenu,
5237
+ closeMenu,
5238
+ toggleMenu,
5239
+ setActiveItem,
5240
+ goToNext,
5241
+ goToPrevious
5242
+ };
5243
+ }
5244
+ function useSkeleton(options = {}) {
5245
+ const {
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
5262
+ } = options;
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;
5296
+ }
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]);
5319
+ useEffect(() => {
5320
+ if (autoStart) {
5321
+ start();
5322
+ }
5323
+ }, [autoStart, start]);
5324
+ useEffect(() => {
5325
+ return () => {
5326
+ if (motionRef.current) {
5327
+ cancelAnimationFrame(motionRef.current);
5328
+ }
5329
+ };
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();
5352
+ useEffect(() => {
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
+ }, []);
5371
+ return {
5372
+ ref,
5373
+ isVisible,
5374
+ isAnimating,
5375
+ style,
5376
+ progress,
5377
+ start,
5378
+ stop,
5379
+ reset,
5380
+ pause,
5381
+ resume
5382
+ };
5383
+ }
5384
+ function useTypewriter(options) {
5385
+ const { text, speed = 50, delay = 0, enabled = true, onComplete } = options;
5386
+ const [index, setIndex] = useState(0);
5387
+ const [started, setStarted] = useState(false);
5388
+ const timerRef = useRef(null);
5389
+ const restart = useCallback(() => {
5390
+ setIndex(0);
5391
+ setStarted(false);
5392
+ }, []);
5393
+ useEffect(() => {
5394
+ if (!enabled) return;
5395
+ const id = setTimeout(() => setStarted(true), delay);
5396
+ return () => clearTimeout(id);
5397
+ }, [enabled, delay]);
5398
+ useEffect(() => {
5399
+ if (!started || !enabled) return;
5400
+ if (index >= text.length) {
5401
+ onComplete?.();
5402
+ return;
5403
+ }
5404
+ timerRef.current = setTimeout(() => {
5405
+ setIndex((prev) => prev + 1);
5406
+ }, speed);
5407
+ return () => {
5408
+ if (timerRef.current) clearTimeout(timerRef.current);
5409
+ };
5410
+ }, [started, enabled, index, text.length, speed, onComplete]);
5411
+ useEffect(() => {
5412
+ setIndex(0);
5413
+ setStarted(false);
5414
+ }, [text]);
5415
+ return {
5416
+ displayText: text.slice(0, index),
5417
+ isTyping: started && index < text.length,
5418
+ progress: text.length > 0 ? index / text.length : 0,
5419
+ restart
5420
+ };
5421
+ }
5422
+ function useCustomCursor(options = {}) {
5423
+ const {
5424
+ enabled = true,
5425
+ size = 32,
5426
+ smoothing = 0.15,
5427
+ hoverScale = 1.5,
5428
+ detectLabels = true
5429
+ } = options;
5430
+ const [isVisible, setIsVisible] = useState(false);
5431
+ const [label, setLabel] = useState(null);
5432
+ const [isHovering, setIsHovering] = useState(false);
5433
+ const targetRef = useRef({ x: 0, y: 0 });
5434
+ const currentRef = useRef({ x: 0, y: 0 });
5435
+ const [pos, setPos] = useState({ x: 0, y: 0 });
5436
+ const rafRef = useRef(null);
5437
+ const animate = useCallback(() => {
5438
+ const dx = targetRef.current.x - currentRef.current.x;
5439
+ const dy = targetRef.current.y - currentRef.current.y;
5440
+ currentRef.current.x += dx * smoothing;
5441
+ currentRef.current.y += dy * smoothing;
5442
+ setPos({ x: currentRef.current.x, y: currentRef.current.y });
5443
+ if (Math.abs(dx) > 0.1 || Math.abs(dy) > 0.1) {
5444
+ rafRef.current = requestAnimationFrame(animate);
5445
+ }
5446
+ }, [smoothing]);
5447
+ useEffect(() => {
5448
+ if (!enabled || typeof window === "undefined") return;
5449
+ const handleMouseMove = (e) => {
5450
+ targetRef.current = { x: e.clientX, y: e.clientY };
5451
+ setIsVisible(true);
5452
+ if (!rafRef.current) {
5453
+ rafRef.current = requestAnimationFrame(animate);
5454
+ }
5455
+ if (detectLabels) {
5456
+ const target = e.target;
5457
+ const cursorEl = target.closest("[data-cursor]");
5458
+ if (cursorEl) {
5459
+ setLabel(cursorEl.dataset.cursor || null);
5460
+ setIsHovering(true);
5461
+ } else {
5462
+ setLabel(null);
5463
+ setIsHovering(false);
5464
+ }
5465
+ }
5466
+ };
5467
+ const handleMouseLeave = () => {
5468
+ setIsVisible(false);
5469
+ setLabel(null);
5470
+ setIsHovering(false);
5471
+ };
5472
+ document.addEventListener("mousemove", handleMouseMove);
5473
+ document.addEventListener("mouseleave", handleMouseLeave);
5474
+ return () => {
5475
+ document.removeEventListener("mousemove", handleMouseMove);
5476
+ document.removeEventListener("mouseleave", handleMouseLeave);
5477
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
5478
+ };
5479
+ }, [enabled, detectLabels, animate]);
5480
+ const scale = isHovering ? hoverScale : 1;
5481
+ const style = useMemo(() => ({
5482
+ "--cursor-x": `${pos.x}px`,
5483
+ "--cursor-y": `${pos.y}px`,
5484
+ "--cursor-size": `${size}px`,
5485
+ "--cursor-scale": `${scale}`,
5486
+ position: "fixed",
5487
+ left: pos.x - size * scale / 2,
5488
+ top: pos.y - size * scale / 2,
5489
+ width: size * scale,
5490
+ height: size * scale,
5491
+ pointerEvents: "none",
5492
+ zIndex: 9999,
5493
+ transition: "width 0.2s, height 0.2s, left 0.05s, top 0.05s"
5494
+ }), [pos.x, pos.y, size, scale]);
5495
+ return { x: pos.x, y: pos.y, label, isHovering, style, isVisible };
5496
+ }
5497
+ function useMagneticCursor(options = {}) {
5498
+ const { strength = 0.3, radius = 100, enabled = true } = options;
5499
+ const ref = useRef(null);
5500
+ const transformRef = useRef({ x: 0, y: 0 });
5501
+ const styleRef = useRef({
5502
+ transition: "transform 0.3s cubic-bezier(0.22, 1, 0.36, 1)",
5503
+ transform: "translate(0px, 0px)"
5504
+ });
5505
+ const onMouseMove = useCallback((e) => {
5506
+ if (!enabled || !ref.current) return;
5507
+ const rect = ref.current.getBoundingClientRect();
5508
+ const centerX = rect.left + rect.width / 2;
5509
+ const centerY = rect.top + rect.height / 2;
5510
+ const dx = e.clientX - centerX;
5511
+ const dy = e.clientY - centerY;
5512
+ const dist = Math.sqrt(dx * dx + dy * dy);
5513
+ if (dist < radius) {
5514
+ const pull = (1 - dist / radius) * strength;
5515
+ transformRef.current = { x: dx * pull, y: dy * pull };
5516
+ } else {
5517
+ transformRef.current = { x: 0, y: 0 };
5518
+ }
5519
+ ref.current.style.transform = `translate(${transformRef.current.x}px, ${transformRef.current.y}px)`;
5520
+ }, [enabled, strength, radius]);
5521
+ const onMouseLeave = useCallback(() => {
5522
+ if (!ref.current) return;
5523
+ transformRef.current = { x: 0, y: 0 };
5524
+ ref.current.style.transform = "translate(0px, 0px)";
5525
+ }, []);
5526
+ const handlers = useMemo(() => ({ onMouseMove, onMouseLeave }), [onMouseMove, onMouseLeave]);
5527
+ return { ref, handlers, style: styleRef.current };
5528
+ }
5529
+ function useSmoothScroll(options = {}) {
5530
+ const {
5531
+ enabled = true,
5532
+ lerp = 0.1,
5533
+ wheelMultiplier = 1,
5534
+ touchMultiplier = 2,
5535
+ direction = "vertical",
5536
+ onScroll: onScroll2
5537
+ } = options;
5538
+ const [scroll, setScroll] = useState(0);
5539
+ const [progress, setProgress] = useState(0);
5540
+ const targetRef = useRef(0);
5541
+ const currentRef = useRef(0);
5542
+ const rafRef = useRef(null);
5543
+ const isRunningRef = useRef(false);
5544
+ const touchStartRef = useRef(0);
5545
+ const getMaxScroll = useCallback(() => {
5546
+ if (typeof document === "undefined") return 0;
5547
+ return direction === "vertical" ? document.documentElement.scrollHeight - window.innerHeight : document.documentElement.scrollWidth - window.innerWidth;
5548
+ }, [direction]);
5549
+ const clamp = useCallback((val) => {
5550
+ return Math.max(0, Math.min(val, getMaxScroll()));
5551
+ }, [getMaxScroll]);
5552
+ const animate = useCallback(() => {
5553
+ const dx = targetRef.current - currentRef.current;
5554
+ if (Math.abs(dx) < 0.5) {
5555
+ currentRef.current = targetRef.current;
5556
+ setScroll(currentRef.current);
5557
+ isRunningRef.current = false;
5558
+ return;
5559
+ }
5560
+ currentRef.current += dx * lerp;
5561
+ setScroll(currentRef.current);
5562
+ const max = getMaxScroll();
5563
+ setProgress(max > 0 ? currentRef.current / max : 0);
5564
+ onScroll2?.(currentRef.current);
5565
+ if (direction === "vertical") {
5566
+ window.scrollTo(0, currentRef.current);
5567
+ } else {
5568
+ window.scrollTo(currentRef.current, 0);
5569
+ }
5570
+ rafRef.current = requestAnimationFrame(animate);
5571
+ }, [lerp, direction, getMaxScroll, onScroll2]);
5572
+ const startAnimation = useCallback(() => {
5573
+ if (isRunningRef.current) return;
5574
+ isRunningRef.current = true;
5575
+ rafRef.current = requestAnimationFrame(animate);
5576
+ }, [animate]);
5577
+ useEffect(() => {
5578
+ if (!enabled || typeof window === "undefined") return;
5579
+ document.documentElement.style.scrollBehavior = "auto";
5580
+ currentRef.current = direction === "vertical" ? window.scrollY : window.scrollX;
5581
+ targetRef.current = currentRef.current;
5582
+ const handleWheel = (e) => {
5583
+ e.preventDefault();
5584
+ const delta = direction === "vertical" ? e.deltaY : e.deltaX;
5585
+ targetRef.current = clamp(targetRef.current + delta * wheelMultiplier);
5586
+ startAnimation();
5587
+ };
5588
+ const handleTouchStart = (e) => {
5589
+ touchStartRef.current = direction === "vertical" ? e.touches[0].clientY : e.touches[0].clientX;
5590
+ };
5591
+ const handleTouchMove = (e) => {
5592
+ const current = direction === "vertical" ? e.touches[0].clientY : e.touches[0].clientX;
5593
+ const delta = (touchStartRef.current - current) * touchMultiplier;
5594
+ touchStartRef.current = current;
5595
+ targetRef.current = clamp(targetRef.current + delta);
5596
+ startAnimation();
5597
+ };
5598
+ window.addEventListener("wheel", handleWheel, { passive: false });
5599
+ window.addEventListener("touchstart", handleTouchStart, { passive: true });
5600
+ window.addEventListener("touchmove", handleTouchMove, { passive: true });
5601
+ return () => {
5602
+ window.removeEventListener("wheel", handleWheel);
5603
+ window.removeEventListener("touchstart", handleTouchStart);
5604
+ window.removeEventListener("touchmove", handleTouchMove);
5605
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
5606
+ document.documentElement.style.scrollBehavior = "";
5607
+ };
5608
+ }, [enabled, direction, wheelMultiplier, touchMultiplier, clamp, startAnimation]);
5609
+ const scrollTo = useCallback((target, opts) => {
5610
+ const offset = opts?.offset ?? 0;
5611
+ if (typeof target === "number") {
5612
+ targetRef.current = clamp(target + offset);
5613
+ } else {
5614
+ const rect = target.getBoundingClientRect();
5615
+ const pos = direction === "vertical" ? rect.top + currentRef.current + offset : rect.left + currentRef.current + offset;
5616
+ targetRef.current = clamp(pos);
5617
+ }
5618
+ startAnimation();
5619
+ }, [clamp, direction, startAnimation]);
5620
+ const stop = useCallback(() => {
5621
+ if (rafRef.current) {
5622
+ cancelAnimationFrame(rafRef.current);
5623
+ rafRef.current = null;
5624
+ }
5625
+ isRunningRef.current = false;
5626
+ targetRef.current = currentRef.current;
5627
+ }, []);
5628
+ return {
5629
+ scroll,
5630
+ targetScroll: targetRef.current,
5631
+ progress,
5632
+ scrollTo,
5633
+ stop
5634
+ };
5635
+ }
5636
+ function useElementProgress(options = {}) {
5637
+ const { start = 0, end = 1, clamp = true } = options;
5638
+ const ref = useRef(null);
5639
+ const [progress, setProgress] = useState(0);
5640
+ const [isInView, setIsInView] = useState(false);
5641
+ const progressRef = useRef(0);
5642
+ const isInViewRef = useRef(false);
5643
+ useEffect(() => {
5644
+ const el = ref.current;
5645
+ if (!el || typeof window === "undefined") return;
5646
+ const calculate = () => {
5647
+ const rect = el.getBoundingClientRect();
5648
+ const vh = window.innerHeight;
5649
+ const elementTop = rect.top;
5650
+ const elementBottom = rect.bottom;
5651
+ const trackStart = vh * (1 - start);
5652
+ const trackEnd = vh * end * -1 + vh;
5653
+ const range = trackStart - trackEnd;
5654
+ const raw = range > 0 ? (trackStart - elementTop) / range : 0;
5655
+ const clamped = clamp ? Math.max(0, Math.min(1, raw)) : raw;
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
+ }
5665
+ };
5666
+ calculate();
5667
+ return subscribeScroll(calculate);
5668
+ }, [start, end, clamp]);
5669
+ return { ref, progress, isInView };
5670
+ }
5671
+ function Motion({
5672
+ as: Component = "div",
5673
+ type,
5674
+ effects,
5675
+ scroll,
5676
+ delay,
5677
+ duration,
5678
+ children,
5679
+ className,
5680
+ style: userStyle,
5681
+ ...rest
5682
+ }) {
5683
+ const scrollOptions = useMemo(() => {
5684
+ if (!scroll) return null;
5685
+ const base = typeof scroll === "object" ? scroll : {};
5686
+ return {
5687
+ ...base,
5688
+ ...delay != null && { delay },
5689
+ ...duration != null && { duration },
5690
+ ...type != null && { motionType: type }
5691
+ };
5692
+ }, [scroll, delay, duration, type]);
5693
+ const scrollMotion = useScrollReveal(scrollOptions ?? { delay: 0 });
5694
+ const unifiedMotion = useUnifiedMotion({
5695
+ type: type ?? "fadeIn",
5696
+ effects,
5697
+ delay,
5698
+ duration,
5699
+ autoStart: true
5700
+ });
5701
+ const isScroll = scroll != null && scroll !== false;
5702
+ const motion = isScroll ? scrollMotion : unifiedMotion;
5703
+ const mergedStyle = useMemo(() => {
5704
+ if (!userStyle) return motion.style;
5705
+ return { ...motion.style, ...userStyle };
5706
+ }, [motion.style, userStyle]);
5707
+ return /* @__PURE__ */ jsx(
5708
+ Component,
5709
+ {
5710
+ ref: motion.ref,
5711
+ className,
5712
+ style: mergedStyle,
5713
+ ...rest,
5714
+ children
5715
+ }
5716
+ );
5717
+ }
5718
+ function getInitialTransform(motionType, slideDistance, scaleFrom) {
5719
+ switch (motionType) {
5720
+ case "slideUp":
5721
+ return `translateY(${slideDistance}px)`;
5722
+ case "slideLeft":
5723
+ return `translateX(-${slideDistance}px)`;
5724
+ case "slideRight":
5725
+ return `translateX(${slideDistance}px)`;
5726
+ case "scaleIn":
5727
+ return `scale(${scaleFrom})`;
5728
+ case "bounceIn":
5729
+ return "scale(0.75)";
5730
+ case "fadeIn":
5731
+ default:
5732
+ return "none";
5733
+ }
5734
+ }
5735
+ function useStagger(options) {
5736
+ const profile = useMotionProfile();
5737
+ const {
5738
+ count,
5739
+ staggerDelay = profile.stagger.perItem,
5740
+ baseDelay = profile.stagger.baseDelay,
5741
+ duration = profile.base.duration,
5742
+ motionType = "fadeIn",
5743
+ threshold = profile.base.threshold,
5744
+ easing: easing2 = profile.base.easing
5745
+ } = options;
5746
+ const containerRef = useRef(null);
5747
+ const [isVisible, setIsVisible] = useState(false);
5748
+ useEffect(() => {
5749
+ if (!containerRef.current) return;
5750
+ return observeElement(
5751
+ containerRef.current,
5752
+ (entry) => {
5753
+ if (entry.isIntersecting) setIsVisible(true);
5754
+ },
5755
+ { threshold },
5756
+ true
5757
+ // once — 첫 intersection 후 자동 정리
5758
+ );
5759
+ }, [threshold]);
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]);
5763
+ const styles = useMemo(() => {
5764
+ return Array.from({ length: count }, (_, i) => {
5765
+ const itemDelay = baseDelay + i * staggerDelay;
5766
+ if (!isVisible) {
5767
+ return {
5768
+ opacity: 0,
5769
+ transform: initialTransform,
5770
+ transition: `opacity ${duration}ms ${easing2} ${itemDelay}ms, transform ${duration}ms ${easing2} ${itemDelay}ms`
5771
+ };
5772
+ }
5773
+ return {
5774
+ opacity: 1,
5775
+ transform: "none",
5776
+ transition: `opacity ${duration}ms ${easing2} ${itemDelay}ms, transform ${duration}ms ${easing2} ${itemDelay}ms`
5777
+ };
5778
+ });
5779
+ }, [count, isVisible, staggerDelay, baseDelay, duration, motionType, easing2, initialTransform]);
5780
+ return {
5781
+ containerRef,
5782
+ styles,
5783
+ isVisible
5784
+ };
5785
+ }
4191
5786
 
4192
- export { MOTION_PRESETS, 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, useFadeIn, useFocusToggle, useGesture, useGestureMotion, useGradient, useHoverMotion, useInView, useMotionState, useMouse, usePageMotions, usePulse, useReducedMotion, useRepeat, useScaleIn, useScrollProgress, useScrollReveal, useSimplePageMotion, useSlideDown, useSlideLeft, useSlideRight, useSlideUp, useSmartMotion, useSpringMotion, useToggleMotion, 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 };
4193
5788
  //# sourceMappingURL=index.mjs.map
4194
5789
  //# sourceMappingURL=index.mjs.map