@humanspeak/svelte-motion 0.6.0 → 0.6.1

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.
@@ -14,6 +14,8 @@
14
14
  import type {
15
15
  MotionProps,
16
16
  MotionTransition,
17
+ AnimationControlsDefinition,
18
+ AnimationControlsSubscriber,
17
19
  DragAxis,
18
20
  DragConstraints,
19
21
  DragControls,
@@ -33,6 +35,7 @@
33
35
  import { onDestroy, untrack, type Snippet } from 'svelte'
34
36
  import { VOID_TAGS } from '../utils/constants'
35
37
  import { mergeTransitions, animateWithLifecycle } from '../utils/animation'
38
+ import { isAnimationControls } from '../utils/animationControls.svelte'
36
39
  import { attachWhileTap } from '../utils/interaction'
37
40
  import { attachWhileHover, computeHoverBaseline, splitHoverDefinition } from '../utils/hover'
38
41
  import { attachWhileFocus } from '../utils/focus'
@@ -60,6 +63,7 @@
60
63
  import { ProjectionNode } from '../utils/projection'
61
64
  import { getProjectionParent, setProjectionParent } from '../components/projection.context'
62
65
  import { MotionDomProjectionAdapter } from '../utils/motionDomProjection'
66
+ import { SvelteSet } from 'svelte/reactivity'
63
67
  import {
64
68
  getMotionDomProjectionParent,
65
69
  setMotionDomProjectionParent
@@ -69,6 +73,7 @@
69
73
  resolveAnimate,
70
74
  resolveExit,
71
75
  resolveWhile,
76
+ resolveVariantList,
72
77
  resolveRestingValues
73
78
  } from '../utils/variants'
74
79
  import {
@@ -282,6 +287,11 @@
282
287
  width: rect.width,
283
288
  height: rect.height
284
289
  })
290
+ const hasRectChanged = (previous: RectLike, next: RectLike): boolean =>
291
+ Math.abs(previous.left - next.left) > 0.5 ||
292
+ Math.abs(previous.top - next.top) > 0.5 ||
293
+ Math.abs(previous.width - next.width) > 0.5 ||
294
+ Math.abs(previous.height - next.height) > 0.5
285
295
  const isViewportOffscreen = (rect: DOMRect): boolean =>
286
296
  rect.bottom <= 0 ||
287
297
  rect.right <= 0 ||
@@ -463,6 +473,8 @@
463
473
 
464
474
  // Variant inheritance and resolution
465
475
  const parentVariantStore = getVariantContext()
476
+ const animateControls = $derived(isAnimationControls(animateProp) ? animateProp : undefined)
477
+ const declarativeAnimateProp = $derived(animateControls ? undefined : animateProp)
466
478
 
467
479
  // Get initial inherited variant synchronously
468
480
  let initialInheritedVariant: string | undefined = undefined
@@ -472,8 +484,8 @@
472
484
 
473
485
  // Create store with initial value so children can inherit immediately
474
486
  const initialVariantValue =
475
- typeof animateProp === 'string'
476
- ? animateProp
487
+ typeof declarativeAnimateProp === 'string'
488
+ ? declarativeAnimateProp
477
489
  : (variantsProp && initialInheritedVariant) || undefined
478
490
  const localVariantStore = writable<string | undefined>(initialVariantValue)
479
491
 
@@ -490,7 +502,8 @@
490
502
 
491
503
  // Use the initial value first, then switch to reactive once mounted
492
504
  const effectiveAnimate = $derived(
493
- animateProp ?? (variantsProp ? (inheritedVariant ?? initialInheritedVariant) : undefined)
505
+ declarativeAnimateProp ??
506
+ (variantsProp ? (inheritedVariant ?? initialInheritedVariant) : undefined)
494
507
  )
495
508
 
496
509
  // Propagate initial={false} to children BEFORE setting variant context
@@ -547,7 +560,8 @@
547
560
 
548
561
  $effect(() => {
549
562
  if (!variantsProp) return localVariantStore.set(undefined)
550
- if (typeof animateProp === 'string') return localVariantStore.set(animateProp)
563
+ if (typeof declarativeAnimateProp === 'string')
564
+ return localVariantStore.set(declarativeAnimateProp)
551
565
  if (typeof effectiveAnimate === 'string') return localVariantStore.set(effectiveAnimate)
552
566
  localVariantStore.set(undefined)
553
567
  })
@@ -692,6 +706,168 @@
692
706
  : null
693
707
  }
694
708
 
709
+ const getAnimationPromise = (control: unknown): Promise<unknown> => {
710
+ const finished = getFinishedPromise(control)
711
+ if (finished) return finished
712
+ if (control && typeof (control as Promise<unknown>).then === 'function') {
713
+ return control as Promise<unknown>
714
+ }
715
+ return Promise.resolve()
716
+ }
717
+
718
+ type StoppableAnimationControl = {
719
+ stop?: () => void
720
+ cancel?: () => void
721
+ }
722
+
723
+ const activeAnimationControls = new SvelteSet<StoppableAnimationControl>()
724
+ let animationControlsGeneration = 0
725
+ let animationControlsHasReceivedCommand = false
726
+
727
+ const isStoppableAnimationControl = (control: unknown): control is StoppableAnimationControl =>
728
+ !!control &&
729
+ typeof control === 'object' &&
730
+ (typeof (control as StoppableAnimationControl).stop === 'function' ||
731
+ typeof (control as StoppableAnimationControl).cancel === 'function')
732
+
733
+ const trackAnimationControlsControl = (control: unknown): Promise<unknown> => {
734
+ const promise = getAnimationPromise(control)
735
+ if (isStoppableAnimationControl(control)) {
736
+ activeAnimationControls.add(control)
737
+ promise.then(
738
+ () => activeAnimationControls.delete(control),
739
+ () => activeAnimationControls.delete(control)
740
+ )
741
+ }
742
+ return promise
743
+ }
744
+
745
+ const resolveAnimationControlsDefinition = (
746
+ definition: AnimationControlsDefinition
747
+ ): DOMKeyframesDefinition | undefined => {
748
+ const resolvedDefinition =
749
+ typeof definition === 'function' ? definition(effectiveCustom) : definition
750
+ if (typeof resolvedDefinition === 'string' || Array.isArray(resolvedDefinition)) {
751
+ return resolveVariantList(variantsProp, resolvedDefinition, effectiveCustom)
752
+ }
753
+ return resolvedDefinition
754
+ }
755
+
756
+ const applyAnimationControlsTarget = (definition: AnimationControlsDefinition) => {
757
+ if (!element) return
758
+ const resolved = resolveAnimationControlsDefinition(definition)
759
+ if (!resolved) return
760
+
761
+ animationControlsHasReceivedCommand = true
762
+ const target = { ...(resolved as Record<string, unknown>) } as Record<string, unknown> & {
763
+ transition?: AnimationOptions
764
+ transitionEnd?: Record<string, unknown>
765
+ }
766
+ const transitionEnd = target.transitionEnd
767
+ delete target.transition
768
+ delete target.transitionEnd
769
+ const finalTarget = resolveRestingValues({
770
+ ...target,
771
+ ...(transitionEnd ?? {})
772
+ } as DOMKeyframesDefinition) as Record<string, unknown> | undefined
773
+ if (!finalTarget) return
774
+
775
+ const transformedTarget = transformSVGPathProperties(element, finalTarget) as Record<
776
+ string,
777
+ unknown
778
+ >
779
+ animate(element, transformedTarget as DOMKeyframesDefinition, { duration: 0 })
780
+ element.setAttribute(
781
+ 'style',
782
+ mergeInlineStyles(element.getAttribute('style') ?? '', undefined, transformedTarget)
783
+ )
784
+ enterAnimationSettled = true
785
+ }
786
+
787
+ const stopAnimationControlsAnimations = () => {
788
+ animationControlsHasReceivedCommand = true
789
+ animationControlsGeneration += 1
790
+
791
+ for (const control of activeAnimationControls) {
792
+ if (typeof control.stop === 'function') {
793
+ control.stop()
794
+ } else {
795
+ control.cancel?.()
796
+ }
797
+ }
798
+ activeAnimationControls.clear()
799
+
800
+ if (!element) return
801
+ if (typeof element.getAnimations !== 'function') return
802
+ for (const animation of element.getAnimations()) {
803
+ try {
804
+ animation.commitStyles?.()
805
+ } catch {
806
+ // Ignore unsupported commitStyles cases.
807
+ }
808
+ animation.cancel()
809
+ }
810
+ }
811
+
812
+ const startAnimationControlsDefinition = async (
813
+ definition: AnimationControlsDefinition,
814
+ transitionOverride?: AnimationOptions
815
+ ): Promise<unknown> => {
816
+ if (!element) return
817
+ const resolved = resolveAnimationControlsDefinition(definition)
818
+ if (!resolved) return
819
+
820
+ animationControlsHasReceivedCommand = true
821
+ const filtered = filterReducedMotionKeyframes(
822
+ resolved as Record<string, unknown>,
823
+ reducedMotion
824
+ ) as Record<string, unknown> & {
825
+ transition?: AnimationOptions
826
+ transitionEnd?: Record<string, unknown>
827
+ }
828
+ const transition = filtered.transition
829
+ const target = { ...filtered }
830
+ delete target.transition
831
+ delete target.transitionEnd
832
+ const transitionAnimate: MotionTransition =
833
+ transitionOverride ?? mergeTransitions(mergedTransition ?? {}, transition ?? {})
834
+ const svgPathFinished =
835
+ isSVGPathElement(element) && hasSVGPathProperties(target)
836
+ ? animateSVGPathAttributes(element, target, transitionAnimate, true)
837
+ : []
838
+ const payload = transformSVGPathProperties(
839
+ element,
840
+ svgPathFinished.length > 0 ? stripSVGPathKeyframes(target) : target
841
+ ) as Record<string, unknown>
842
+
843
+ const controlsGeneration = ++animationControlsGeneration
844
+ enterAnimationSettled = false
845
+ onAnimationStartProp?.(definition as unknown as DOMKeyframesDefinition)
846
+
847
+ const promises: Promise<unknown>[] = [...svgPathFinished]
848
+ if (isNotEmpty(payload)) {
849
+ promises.push(
850
+ trackAnimationControlsControl(
851
+ animate(
852
+ element,
853
+ payload as DOMKeyframesDefinition,
854
+ transitionAnimate as AnimationOptions
855
+ )
856
+ )
857
+ )
858
+ }
859
+
860
+ try {
861
+ await Promise.all(promises)
862
+ } catch (error) {
863
+ if (controlsGeneration !== animationControlsGeneration) return
864
+ throw error
865
+ }
866
+ if (controlsGeneration !== animationControlsGeneration) return
867
+ applyAnimationControlsTarget(definition)
868
+ onAnimationCompleteProp?.(definition as unknown as DOMKeyframesDefinition)
869
+ }
870
+
695
871
  /**
696
872
  * Animates SVG path drawing props via motion-dom's `svgEffect`, matching
697
873
  * upstream's attribute-based pathLength/pathSpacing/pathOffset behavior.
@@ -704,7 +880,8 @@
704
880
  const animateSVGPathAttributes = (
705
881
  path: SVGPathElement,
706
882
  keyframes: Record<string, unknown>,
707
- transition: MotionTransition
883
+ transition: MotionTransition,
884
+ trackControl = false
708
885
  ): Promise<unknown>[] => {
709
886
  if (!hasSVGPathProperties(keyframes)) return []
710
887
 
@@ -733,7 +910,9 @@
733
910
  : keyframes[key]) as never,
734
911
  transition as unknown as AnimationOptions
735
912
  )
736
- return getFinishedPromise(control)
913
+ return trackControl
914
+ ? trackAnimationControlsControl(control)
915
+ : getFinishedPromise(control)
737
916
  })
738
917
  .filter((promise): promise is Promise<unknown> => promise !== null)
739
918
  }
@@ -831,9 +1010,13 @@
831
1010
  ? (resolveRestingValues(
832
1011
  animateKeyframes as DOMKeyframesDefinition | undefined
833
1012
  ) as unknown as Record<string, unknown>)
834
- : isNotEmpty(initialKeyframes)
835
- ? undefined
836
- : (animateKeyframes as unknown as Record<string, unknown>)
1013
+ : animateControls &&
1014
+ !animationControlsHasReceivedCommand &&
1015
+ isNotEmpty(initialKeyframes)
1016
+ ? (initialKeyframes as unknown as Record<string, unknown>)
1017
+ : isNotEmpty(initialKeyframes)
1018
+ ? undefined
1019
+ : (animateKeyframes as unknown as Record<string, unknown>)
837
1020
  ),
838
1021
  class: classProp
839
1022
  })
@@ -1320,9 +1503,12 @@
1320
1503
  // which shows up as a "pop" after the deferred animation completes.
1321
1504
  pwLog('[motion] wait-unblocked: marking enter handled')
1322
1505
  initialAnimationTriggered = true
1323
- if (animateProp && typeof animateProp !== 'string') {
1506
+ if (
1507
+ declarativeAnimateProp &&
1508
+ typeof declarativeAnimateProp !== 'string'
1509
+ ) {
1324
1510
  objectAnimateRanOnMount = true
1325
- lastAnimatePropJson = JSON.stringify(animateProp)
1511
+ lastAnimatePropJson = JSON.stringify(declarativeAnimateProp)
1326
1512
  }
1327
1513
  isLoaded = 'ready'
1328
1514
  })
@@ -1360,8 +1546,8 @@
1360
1546
  let lastAnimatePropJson = $state<string | undefined>(undefined)
1361
1547
  let motionDomProjectionUpdatePending = false
1362
1548
  const currentAnimateKey = $derived(
1363
- typeof animateProp === 'string'
1364
- ? animateProp
1549
+ typeof declarativeAnimateProp === 'string'
1550
+ ? declarativeAnimateProp
1365
1551
  : typeof effectiveAnimate === 'string'
1366
1552
  ? effectiveAnimate
1367
1553
  : undefined
@@ -1372,6 +1558,7 @@
1372
1558
  motionDomProjection.updateOptions({
1373
1559
  layout: layoutProp,
1374
1560
  layoutId: scopedLayoutId,
1561
+ layoutScroll: layoutScrollProp,
1375
1562
  transition: mergedTransition as never,
1376
1563
  style: styleProp
1377
1564
  })
@@ -1380,6 +1567,13 @@
1380
1567
  $effect(() => {
1381
1568
  if (!motionDomProjection) return
1382
1569
  if (!element) return
1570
+ motionDomProjection.updateOptions({
1571
+ layout: layoutProp,
1572
+ layoutId: scopedLayoutId,
1573
+ layoutScroll: layoutScrollProp,
1574
+ transition: mergedTransition as never,
1575
+ style: styleProp
1576
+ })
1383
1577
  motionDomProjection.mount(element)
1384
1578
  return () => {
1385
1579
  motionDomProjection.unmount()
@@ -1423,10 +1617,18 @@
1423
1617
  if (previous) {
1424
1618
  const next = measureLayoutRect()
1425
1619
  if (next) {
1426
- const flipLayoutMode = layoutProp === 'position' ? 'position' : true
1427
- const transforms = computeFlipTransforms(previous, next, flipLayoutMode)
1428
- runFlipAnimation(element!, transforms, (mergedTransition ?? {}) as AnimationOptions)
1429
- lastRect = next
1620
+ if (motionDomProjection) {
1621
+ lastRect = next
1622
+ } else {
1623
+ const flipLayoutMode = layoutProp === 'position' ? 'position' : true
1624
+ const transforms = computeFlipTransforms(previous, next, flipLayoutMode)
1625
+ runFlipAnimation(
1626
+ element!,
1627
+ transforms,
1628
+ (mergedTransition ?? {}) as AnimationOptions
1629
+ )
1630
+ lastRect = next
1631
+ }
1430
1632
  }
1431
1633
  }
1432
1634
  projection.didUpdate()
@@ -1469,14 +1671,20 @@
1469
1671
 
1470
1672
  let rafId: number | null = null
1471
1673
  let wasViewportOffscreenSinceLastLayout = false
1674
+ let wasViewportScrolledSinceLastLayout = false
1472
1675
  const flipLayoutMode = layoutProp === 'position' ? 'position' : true
1473
1676
  motionDomProjection?.seedLayout()
1474
1677
  lastRect = measureLayoutRect()
1475
1678
  setCompositorHints(element!, true)
1476
1679
 
1477
1680
  const rememberOffscreenScroll = () => {
1681
+ wasViewportScrolledSinceLastLayout = true
1682
+ if (motionDomProjection?.isAnimating()) {
1683
+ motionDomProjection.finishAnimation()
1684
+ }
1478
1685
  if (isViewportOffscreen(element!.getBoundingClientRect())) {
1479
1686
  wasViewportOffscreenSinceLastLayout = true
1687
+ motionDomProjection?.finishAnimation()
1480
1688
  }
1481
1689
  }
1482
1690
 
@@ -1503,18 +1711,42 @@
1503
1711
  return
1504
1712
  }
1505
1713
 
1714
+ if (
1715
+ wasViewportScrolledSinceLastLayout ||
1716
+ wasViewportOffscreenSinceLastLayout ||
1717
+ isViewportOffscreen(element!.getBoundingClientRect())
1718
+ ) {
1719
+ lastRect = measureLayoutRect()
1720
+ motionDomProjection?.finishAnimation()
1721
+ wasViewportScrolledSinceLastLayout = false
1722
+ wasViewportOffscreenSinceLastLayout = false
1723
+ return
1724
+ }
1725
+
1506
1726
  const nextBox = projection.commitLayoutChange()
1727
+ let shouldCommitMotionDomLayout = false
1507
1728
  if (nextBox && lastRect) {
1508
1729
  const next = boxToRectLike(nextBox)
1509
- const transforms = computeFlipTransforms(lastRect, next, flipLayoutMode)
1510
- runFlipAnimation(element!, transforms, (mergedTransition ?? {}) as AnimationOptions)
1730
+ shouldCommitMotionDomLayout = hasRectChanged(lastRect, next)
1731
+ if (!motionDomProjection) {
1732
+ const transforms = computeFlipTransforms(lastRect, next, flipLayoutMode)
1733
+ runFlipAnimation(
1734
+ element!,
1735
+ transforms,
1736
+ (mergedTransition ?? {}) as AnimationOptions
1737
+ )
1738
+ }
1511
1739
  lastRect = next
1740
+ wasViewportScrolledSinceLastLayout = false
1512
1741
  wasViewportOffscreenSinceLastLayout = false
1513
1742
  } else if (nextBox) {
1514
1743
  lastRect = boxToRectLike(nextBox)
1744
+ wasViewportScrolledSinceLastLayout = false
1515
1745
  wasViewportOffscreenSinceLastLayout = false
1516
1746
  }
1517
- motionDomProjection?.commitObservedLayoutChange()
1747
+ if (shouldCommitMotionDomLayout) {
1748
+ motionDomProjection?.commitObservedLayoutChange()
1749
+ }
1518
1750
  }
1519
1751
 
1520
1752
  const commitPresenceLayoutRelease = (event: Event) => {
@@ -1532,14 +1764,20 @@
1532
1764
  lastRect = next
1533
1765
  const shouldSkipLayoutAnimation =
1534
1766
  detail?.viewportScrolledDuringHold ||
1767
+ wasViewportScrolledSinceLastLayout ||
1535
1768
  wasViewportOffscreenSinceLastLayout ||
1536
1769
  isViewportOffscreen(viewportRect)
1537
- if (!shouldSkipLayoutAnimation) {
1770
+ if (!shouldSkipLayoutAnimation && !motionDomProjection) {
1538
1771
  const transforms = computeFlipTransforms(previous, next, flipLayoutMode)
1539
1772
  runFlipAnimation(element!, transforms, (mergedTransition ?? {}) as AnimationOptions)
1540
1773
  }
1774
+ wasViewportScrolledSinceLastLayout = false
1541
1775
  wasViewportOffscreenSinceLastLayout = false
1542
- motionDomProjection?.commitObservedLayoutChange()
1776
+ if (!shouldSkipLayoutAnimation && hasRectChanged(previous, next)) {
1777
+ motionDomProjection?.commitObservedLayoutChange()
1778
+ } else if (shouldSkipLayoutAnimation) {
1779
+ motionDomProjection?.finishAnimation()
1780
+ }
1543
1781
  }
1544
1782
 
1545
1783
  const scheduleProjectionCommit = () => {
@@ -1581,6 +1819,7 @@
1581
1819
 
1582
1820
  const prev = layoutIdRegistry.consume(scopedLayoutId)
1583
1821
  if (!prev) return // First appearance, no animation needed
1822
+ if (motionDomProjection) return
1584
1823
 
1585
1824
  const next = measureRect(element, resolveLayoutScrollAncestors())
1586
1825
  const transforms = computeFlipTransforms(prev.rect, next, true)
@@ -1690,6 +1929,22 @@
1690
1929
  )
1691
1930
  })
1692
1931
 
1932
+ // Legacy animation controls (`animate={controls}`) mirror upstream's
1933
+ // VisualElement subscription model with a small Svelte adapter. The
1934
+ // controls own when animations start; this component only resolves
1935
+ // variants/custom data and runs the resulting target on its element.
1936
+ $effect(() => {
1937
+ if (!(element && animateControls)) return
1938
+
1939
+ const subscriber: AnimationControlsSubscriber = {
1940
+ start: startAnimationControlsDefinition,
1941
+ set: applyAnimationControlsTarget,
1942
+ stop: stopAnimationControlsAnimations
1943
+ }
1944
+
1945
+ return animateControls.subscribe(subscriber)
1946
+ })
1947
+
1693
1948
  // Handle key prop changes inside AnimatePresence (simulates React's key-based remounting)
1694
1949
  // When key changes, run exit → initial → animate sequence on the same element
1695
1950
  $effect(() => {
@@ -1798,10 +2053,14 @@
1798
2053
  // Re-run animate when animateProp changes while ready
1799
2054
  $effect(() => {
1800
2055
  if (!(element && isLoaded === 'ready')) return
2056
+ if (animateControls) return
1801
2057
  // Skip first run if we mounted with initial={false} AND the variant hasn't changed
1802
2058
  if (mountedWithInitialFalse) {
1803
2059
  // Only skip if the variant is the same as what we mounted with
1804
- if (typeof animateProp === 'string' && lastRanVariantKey === animateProp) {
2060
+ if (
2061
+ typeof declarativeAnimateProp === 'string' &&
2062
+ lastRanVariantKey === declarativeAnimateProp
2063
+ ) {
1805
2064
  mountedWithInitialFalse = false
1806
2065
  return
1807
2066
  }
@@ -1813,25 +2072,28 @@
1813
2072
  pwLog('[motion] effect: skipping, initial animation already triggered')
1814
2073
  initialAnimationTriggered = false
1815
2074
  // Also mark object animate as ran to prevent duplicate runs from effect re-triggers
1816
- if (animateProp && typeof animateProp !== 'string') {
2075
+ if (declarativeAnimateProp && typeof declarativeAnimateProp !== 'string') {
1817
2076
  objectAnimateRanOnMount = true
1818
2077
  }
1819
2078
  return
1820
2079
  }
1821
- if (typeof animateProp === 'string') {
2080
+ if (typeof declarativeAnimateProp === 'string') {
1822
2081
  // Compare BOTH the variant key and the resolved keyframes JSON.
1823
2082
  // For static variants the JSON is constant per key; for
1824
2083
  // function-form variants the JSON changes when `custom`
1825
2084
  // changes, which we must treat as a new animation target.
1826
2085
  const resolvedJson = resolvedAnimate ? JSON.stringify(resolvedAnimate) : undefined
1827
- if (lastRanVariantKey !== animateProp || lastRanResolvedJson !== resolvedJson) {
1828
- lastRanVariantKey = animateProp
2086
+ if (
2087
+ lastRanVariantKey !== declarativeAnimateProp ||
2088
+ lastRanResolvedJson !== resolvedJson
2089
+ ) {
2090
+ lastRanVariantKey = declarativeAnimateProp
1829
2091
  lastRanResolvedJson = resolvedJson
1830
2092
  runAnimation()
1831
2093
  }
1832
- } else if (animateProp) {
2094
+ } else if (declarativeAnimateProp) {
1833
2095
  // Object animate props - detect if the prop actually changed
1834
- const currentJson = JSON.stringify(animateProp)
2096
+ const currentJson = JSON.stringify(declarativeAnimateProp)
1835
2097
  const propChanged = lastAnimatePropJson !== currentJson
1836
2098
 
1837
2099
  // Reset flag if animate prop changed
@@ -1853,7 +2115,7 @@
1853
2115
  // Also run when inherited/effective variant changes
1854
2116
  $effect(() => {
1855
2117
  void resolvedAnimate
1856
- if (!(element && isLoaded === 'ready' && !animateProp && resolvedAnimate)) return
2118
+ if (!(element && isLoaded === 'ready' && !declarativeAnimateProp && resolvedAnimate)) return
1857
2119
  // Skip first run if we mounted with initial={false} AND the variant hasn't changed
1858
2120
  if (mountedWithInitialFalse) {
1859
2121
  // Only skip if the variant is the same as what we mounted with
@@ -1914,9 +2176,9 @@
1914
2176
  dataPath = 6
1915
2177
  isLoaded = 'initial'
1916
2178
  initialAnimationTriggered = true
1917
- if (animateProp && typeof animateProp !== 'string') {
2179
+ if (declarativeAnimateProp && typeof declarativeAnimateProp !== 'string') {
1918
2180
  objectAnimateRanOnMount = true
1919
- lastAnimatePropJson = JSON.stringify(animateProp)
2181
+ lastAnimatePropJson = JSON.stringify(declarativeAnimateProp)
1920
2182
  }
1921
2183
  finishOptimizedAppearAnimation(optimizedAppearId)
1922
2184
  .then(() => {
@@ -1980,8 +2242,8 @@
1980
2242
 
1981
2243
  // Mark that we're triggering the initial animation to prevent duplicate runs
1982
2244
  initialAnimationTriggered = true
1983
- if (animateProp && typeof animateProp !== 'string') {
1984
- lastAnimatePropJson = JSON.stringify(animateProp)
2245
+ if (declarativeAnimateProp && typeof declarativeAnimateProp !== 'string') {
2246
+ lastAnimatePropJson = JSON.stringify(declarativeAnimateProp)
1985
2247
  }
1986
2248
 
1987
2249
  // IMPORTANT: Start the animation BEFORE changing isLoaded.
package/dist/index.d.ts CHANGED
@@ -12,9 +12,10 @@ export { motion } from './motion';
12
12
  export { animate, delay, hover, inView, press, resize, scroll, stagger, transform } from 'motion';
13
13
  export { anticipate, backIn, backInOut, backOut, circIn, circInOut, circOut, cubicBezier, easeIn, easeInOut, easeOut } from 'motion';
14
14
  export { clamp, distance, distance2D, interpolate, mix, pipe, progress, wrap } from 'motion';
15
- export type { DragAxis, DragConstraints, DragControls, DragInfo, DragTransition, MotionAnimate, MotionInitial, MotionOnDirectionLock, MotionOnDragTransitionEnd, MotionOnInViewEnd, MotionOnInViewStart, MotionOnProjectionUpdate, MotionTransition, MotionWhileDrag, MotionWhileFocus, MotionWhileHover, MotionWhileInView, MotionWhileTap, ProjectionUpdatePayload, ReducedMotionConfig, Variants } from './types';
15
+ export type { AnimationControls, AnimationControlsDefinition, AnimationControlsSubscriber, DragAxis, DragConstraints, DragControls, DragInfo, DragTransition, MotionAnimate, MotionInitial, MotionOnDirectionLock, MotionOnDragTransitionEnd, MotionOnInViewEnd, MotionOnInViewStart, MotionOnProjectionUpdate, MotionTransition, MotionWhileDrag, MotionWhileFocus, MotionWhileHover, MotionWhileInView, MotionWhileTap, ProjectionUpdatePayload, ReducedMotionConfig, Variants } from './types';
16
16
  export { useAnimate } from './utils/animate.svelte';
17
17
  export type { AnimationScope } from './utils/animate.svelte';
18
+ export { animationControls, isAnimationControls, useAnimation, useAnimationControls } from './utils/animationControls.svelte';
18
19
  export { useAnimationFrame } from './utils/animationFrame';
19
20
  export type { AugmentedMotionValue } from './utils/augmentMotionValue.svelte';
20
21
  export { useCycle } from './utils/cycle.svelte';
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ export { anticipate, backIn, backInOut, backOut, circIn, circInOut, circOut, cub
15
15
  // Re-export utility functions
16
16
  export { clamp, distance, distance2D, interpolate, mix, pipe, progress, wrap } from 'motion';
17
17
  export { useAnimate } from './utils/animate.svelte';
18
+ export { animationControls, isAnimationControls, useAnimation, useAnimationControls } from './utils/animationControls.svelte';
18
19
  export { useAnimationFrame } from './utils/animationFrame';
19
20
  export { useCycle } from './utils/cycle.svelte';
20
21
  export { createDragControls } from './utils/dragControls';
package/dist/types.d.ts CHANGED
@@ -74,7 +74,91 @@ export type MotionInitial = DOMKeyframesDefinition | string | string[] | false |
74
74
  * <motion.div variants={myVariants} animate="visible" />
75
75
  * ```
76
76
  */
77
- export type MotionAnimate = DOMKeyframesDefinition | string | string[] | undefined;
77
+ /**
78
+ * Definition accepted by legacy animation controls.
79
+ *
80
+ * Mirrors Motion's `AnimationDefinition`: a keyframes object, a variant
81
+ * label, an ordered list of variant labels, or a resolver function that
82
+ * receives `custom` data.
83
+ *
84
+ * @example
85
+ * ```ts
86
+ * controls.start('visible')
87
+ * controls.start(['visible', 'active'])
88
+ * controls.start({ opacity: 1, x: 0 })
89
+ * controls.start((custom) => ({ x: custom * 100 }))
90
+ * ```
91
+ */
92
+ export type AnimationControlsDefinition = DOMKeyframesDefinition | string | string[] | ((custom: unknown) => DOMKeyframesDefinition | string);
93
+ /**
94
+ * Internal subscriber shape used by {@link AnimationControls}.
95
+ *
96
+ * Motion's upstream controls subscribe VisualElements. Svelte Motion
97
+ * subscribes a lightweight adapter from each `motion.*` component.
98
+ */
99
+ export type AnimationControlsSubscriber = {
100
+ /** Start an animation on the subscribed component. */
101
+ start: (definition: AnimationControlsDefinition, transitionOverride?: AnimationOptions) => Promise<unknown>;
102
+ /** Synchronously set final values on the subscribed component. */
103
+ set: (definition: AnimationControlsDefinition) => void;
104
+ /** Stop currently running animations on the subscribed component. */
105
+ stop: () => void;
106
+ };
107
+ /**
108
+ * Legacy imperative controls returned by {@link useAnimationControls}.
109
+ *
110
+ * Pass the object to `animate={controls}` on one or more `motion.*`
111
+ * components, then call `controls.start(...)`, `controls.set(...)`, or
112
+ * `controls.stop()` from events or effects.
113
+ *
114
+ * @example
115
+ * ```svelte
116
+ * <script lang="ts">
117
+ * import { motion, useAnimationControls } from '@humanspeak/svelte-motion'
118
+ *
119
+ * const controls = useAnimationControls()
120
+ * </script>
121
+ *
122
+ * <button onclick={() => controls.start('open')}>Open</button>
123
+ * <motion.div animate={controls} variants={{ open: { opacity: 1 } }} />
124
+ * ```
125
+ */
126
+ export type AnimationControls = {
127
+ /**
128
+ * Subscribe a motion component adapter to these controls.
129
+ *
130
+ * @param subscriber Component adapter to animate.
131
+ * @returns Unsubscribe callback.
132
+ */
133
+ subscribe: (subscriber: AnimationControlsSubscriber) => () => void;
134
+ /**
135
+ * Start an animation on every subscribed component.
136
+ *
137
+ * @param definition Target keyframes, variant label(s), or resolver.
138
+ * @param transitionOverride Optional transition that overrides the
139
+ * component/default transition for this run.
140
+ * @returns Promise resolving when all subscribed animations complete.
141
+ */
142
+ start: (definition: AnimationControlsDefinition, transitionOverride?: AnimationOptions) => Promise<unknown[]>;
143
+ /**
144
+ * Synchronously set every subscribed component to the target's final
145
+ * values.
146
+ *
147
+ * @param definition Target keyframes, variant label(s), or resolver.
148
+ */
149
+ set: (definition: AnimationControlsDefinition) => void;
150
+ /** Stop animations on every subscribed component. */
151
+ stop: () => void;
152
+ /**
153
+ * Mark controls as mounted and return cleanup.
154
+ *
155
+ * Called automatically by `useAnimationControls()`.
156
+ *
157
+ * @returns Cleanup that marks controls unmounted and stops subscribers.
158
+ */
159
+ mount: () => () => void;
160
+ };
161
+ export type MotionAnimate = DOMKeyframesDefinition | string | string[] | AnimationControls | undefined;
78
162
  /**
79
163
  * Exit animation properties for a motion component when unmounted.
80
164
  *
@@ -0,0 +1,63 @@
1
+ import type { AnimationControls } from '../types.js';
2
+ /**
3
+ * Returns true when a value looks like Motion's legacy animation controls.
4
+ *
5
+ * Upstream `motion-dom` treats any non-null object with a `start`
6
+ * function as animation controls. Matching that narrow check keeps
7
+ * `animate={controls}` detection compatible with Motion's public shape.
8
+ *
9
+ * @param value Value passed to `animate`.
10
+ * @returns Whether `value` is an animation controls object.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * const controls = useAnimationControls()
15
+ * isAnimationControls(controls) // true
16
+ * isAnimationControls({ opacity: 1 }) // false
17
+ * ```
18
+ */
19
+ export declare const isAnimationControls: (value: unknown) => value is AnimationControls;
20
+ /**
21
+ * Create legacy animation controls.
22
+ *
23
+ * This mirrors upstream Motion's `animationControls()`: controls collect
24
+ * subscribed motion components, guard `start`/`set` until mounted, fan out
25
+ * starts to every subscriber, and stop all subscribers on unmount.
26
+ *
27
+ * @returns Animation controls with `subscribe`, `start`, `set`, `stop`,
28
+ * and `mount`.
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * const controls = animationControls()
33
+ * const cleanup = controls.mount()
34
+ * await controls.start({ opacity: 1 })
35
+ * cleanup()
36
+ * ```
37
+ */
38
+ export declare const animationControls: () => AnimationControls;
39
+ /**
40
+ * Create imperative controls for one or more `motion.*` components.
41
+ *
42
+ * Pass the returned object to `animate={controls}`. Once mounted, call
43
+ * `controls.start(definition)`, `controls.set(definition)`, or
44
+ * `controls.stop()` to coordinate every subscribed component.
45
+ *
46
+ * @returns Mounted animation controls.
47
+ * @see https://motion.dev/docs/react-use-animation-controls
48
+ *
49
+ * @example
50
+ * ```svelte
51
+ * <script lang="ts">
52
+ * import { motion, useAnimationControls } from '@humanspeak/svelte-motion'
53
+ *
54
+ * const controls = useAnimationControls()
55
+ * </script>
56
+ *
57
+ * <button onclick={() => controls.start({ scale: 1.2 })}>Pop</button>
58
+ * <motion.div animate={controls} />
59
+ * ```
60
+ */
61
+ export declare const useAnimationControls: () => AnimationControls;
62
+ /** Alias matching Motion's legacy `useAnimation` export. */
63
+ export declare const useAnimation: () => AnimationControls;
@@ -0,0 +1,111 @@
1
+ import { SvelteSet } from 'svelte/reactivity';
2
+ const mountedError = 'controls.start() should only be called after a component has mounted. Consider calling within a $effect.';
3
+ const setMountedError = 'controls.set() should only be called after a component has mounted. Consider calling within a $effect.';
4
+ /**
5
+ * Returns true when a value looks like Motion's legacy animation controls.
6
+ *
7
+ * Upstream `motion-dom` treats any non-null object with a `start`
8
+ * function as animation controls. Matching that narrow check keeps
9
+ * `animate={controls}` detection compatible with Motion's public shape.
10
+ *
11
+ * @param value Value passed to `animate`.
12
+ * @returns Whether `value` is an animation controls object.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * const controls = useAnimationControls()
17
+ * isAnimationControls(controls) // true
18
+ * isAnimationControls({ opacity: 1 }) // false
19
+ * ```
20
+ */
21
+ export const isAnimationControls = (value) => {
22
+ return (value !== null &&
23
+ typeof value === 'object' &&
24
+ typeof value.start === 'function');
25
+ };
26
+ /**
27
+ * Create legacy animation controls.
28
+ *
29
+ * This mirrors upstream Motion's `animationControls()`: controls collect
30
+ * subscribed motion components, guard `start`/`set` until mounted, fan out
31
+ * starts to every subscriber, and stop all subscribers on unmount.
32
+ *
33
+ * @returns Animation controls with `subscribe`, `start`, `set`, `stop`,
34
+ * and `mount`.
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * const controls = animationControls()
39
+ * const cleanup = controls.mount()
40
+ * await controls.start({ opacity: 1 })
41
+ * cleanup()
42
+ * ```
43
+ */
44
+ export const animationControls = () => {
45
+ let hasMounted = false;
46
+ const subscribers = new SvelteSet();
47
+ const controls = {
48
+ subscribe(subscriber) {
49
+ subscribers.add(subscriber);
50
+ return () => {
51
+ subscribers.delete(subscriber);
52
+ };
53
+ },
54
+ start(definition, transitionOverride) {
55
+ if (!hasMounted) {
56
+ throw new Error(mountedError);
57
+ }
58
+ const animations = [];
59
+ subscribers.forEach((subscriber) => {
60
+ animations.push(subscriber.start(definition, transitionOverride));
61
+ });
62
+ return Promise.all(animations);
63
+ },
64
+ set(definition) {
65
+ if (!hasMounted) {
66
+ throw new Error(setMountedError);
67
+ }
68
+ subscribers.forEach((subscriber) => subscriber.set(definition));
69
+ },
70
+ stop() {
71
+ subscribers.forEach((subscriber) => subscriber.stop());
72
+ },
73
+ mount() {
74
+ hasMounted = true;
75
+ return () => {
76
+ hasMounted = false;
77
+ controls.stop();
78
+ };
79
+ }
80
+ };
81
+ return controls;
82
+ };
83
+ /**
84
+ * Create imperative controls for one or more `motion.*` components.
85
+ *
86
+ * Pass the returned object to `animate={controls}`. Once mounted, call
87
+ * `controls.start(definition)`, `controls.set(definition)`, or
88
+ * `controls.stop()` to coordinate every subscribed component.
89
+ *
90
+ * @returns Mounted animation controls.
91
+ * @see https://motion.dev/docs/react-use-animation-controls
92
+ *
93
+ * @example
94
+ * ```svelte
95
+ * <script lang="ts">
96
+ * import { motion, useAnimationControls } from '@humanspeak/svelte-motion'
97
+ *
98
+ * const controls = useAnimationControls()
99
+ * </script>
100
+ *
101
+ * <button onclick={() => controls.start({ scale: 1.2 })}>Pop</button>
102
+ * <motion.div animate={controls} />
103
+ * ```
104
+ */
105
+ export const useAnimationControls = () => {
106
+ const controls = animationControls();
107
+ $effect(() => controls.mount());
108
+ return controls;
109
+ };
110
+ /** Alias matching Motion's legacy `useAnimation` export. */
111
+ export const useAnimation = useAnimationControls;
@@ -1,6 +1,7 @@
1
1
  import { animate } from 'motion';
2
2
  const layoutSizeAnimationAttribute = 'data-layout-size-animation';
3
- const px = (value) => `${Math.max(0, value)}px`;
3
+ const roundedPx = (value) => `${Math.max(0, Math.round(value))}px`;
4
+ const mix = (from, to, progress) => from + (to - from) * progress;
4
5
  const isViewportOffscreen = (el) => {
5
6
  if (typeof window === 'undefined')
6
7
  return false;
@@ -26,23 +27,29 @@ const runBoxSizeAnimation = (el, transforms, transition) => {
26
27
  if (child.style.willChange === 'transform')
27
28
  child.style.willChange = '';
28
29
  }
29
- el.style.width = px(prevWidth);
30
- el.style.height = px(prevHeight);
30
+ el.style.width = roundedPx(prevWidth);
31
+ el.style.height = roundedPx(prevHeight);
31
32
  const sizedRect = el.getBoundingClientRect();
32
33
  const residualDx = nextRect.left + dx - sizedRect.left;
33
34
  const residualDy = nextRect.top + dy - sizedRect.top;
34
35
  const shouldTranslate = Math.abs(residualDx) > 0.5 || Math.abs(residualDy) > 0.5;
35
- const keyframes = {
36
- width: [px(prevWidth), px(nextRect.width)],
37
- height: [px(prevHeight), px(nextRect.height)]
38
- };
39
36
  if (shouldTranslate) {
40
- keyframes.x = [residualDx, 0];
41
- keyframes.y = [residualDy, 0];
42
37
  el.style.transformOrigin = '0 0';
43
- el.style.transform = `translate(${residualDx}px, ${residualDy}px)`;
38
+ el.style.transform = `translate(${Math.round(residualDx)}px, ${Math.round(residualDy)}px)`;
44
39
  }
45
- const animation = animate(el, keyframes, transition);
40
+ const writeBox = (progress) => {
41
+ el.style.width = roundedPx(mix(prevWidth, nextRect.width, progress));
42
+ el.style.height = roundedPx(mix(prevHeight, nextRect.height, progress));
43
+ if (shouldTranslate) {
44
+ const x = Math.round(mix(residualDx, 0, progress));
45
+ const y = Math.round(mix(residualDy, 0, progress));
46
+ el.style.transform = x === 0 && y === 0 ? '' : `translate(${x}px, ${y}px)`;
47
+ }
48
+ };
49
+ const animation = animate(0, 1, {
50
+ ...transition,
51
+ onUpdate: writeBox
52
+ });
46
53
  let removeScrollListener;
47
54
  let offscreenRaf = null;
48
55
  let cleanupRan = false;
@@ -278,7 +285,7 @@ export const observeLayoutChanges = (el, onChange) => {
278
285
  const attributeObserver = new MutationObserver(() => schedule());
279
286
  attributeObserver.observe(el, {
280
287
  attributes: true,
281
- attributeFilter: ['class', 'style', 'data-presence-layout-hold']
288
+ attributeFilter: ['class', 'data-presence-layout-hold']
282
289
  });
283
290
  const childListObserver = new MutationObserver(() => schedule());
284
291
  childListObserver.observe(el, {
@@ -16,6 +16,8 @@ export interface MotionDomProjectionUpdateOptions {
16
16
  layout?: LayoutOption;
17
17
  /** Shared layout id used by upstream projection matching. */
18
18
  layoutId?: string;
19
+ /** Tracks scroll on this element for descendant layout projection. */
20
+ layoutScroll?: boolean;
19
21
  /** Transition passed to the upstream layout animation builder. */
20
22
  transition?: Transition;
21
23
  /** Inline style props passed through to the visual element. */
@@ -29,6 +31,7 @@ export interface MotionDomProjectionUpdateOptions {
29
31
  * and `HTMLVisualElement` internals Framer Motion uses.
30
32
  */
31
33
  export declare class MotionDomProjectionAdapter {
34
+ private static adapters;
32
35
  readonly visualElement: ProjectionVisualElement;
33
36
  readonly projection: IProjectionNode<HTMLElement>;
34
37
  private element;
@@ -121,6 +124,32 @@ export declare class MotionDomProjectionAdapter {
121
124
  * ```
122
125
  */
123
126
  commitObservedLayoutChange(): void;
127
+ /**
128
+ * Finish any active upstream layout animation in this subtree.
129
+ *
130
+ * @returns Nothing.
131
+ *
132
+ * @example
133
+ * ```ts
134
+ * adapter.finishAnimation()
135
+ * ```
136
+ */
137
+ finishAnimation(): void;
138
+ /**
139
+ * Check whether this projection subtree has an active layout animation.
140
+ *
141
+ * @returns `true` when this projection subtree is currently animating.
142
+ *
143
+ * @example
144
+ * ```ts
145
+ * if (adapter.isAnimating()) adapter.finishAnimation()
146
+ * ```
147
+ */
148
+ isAnimating(): boolean;
149
+ private seedCachedSnapshotsForSubtree;
150
+ private finishAnimationForSubtree;
151
+ private isAnimatingSubtree;
152
+ private updatePathScroll;
124
153
  private refreshCachedLayout;
125
154
  }
126
155
  export {};
@@ -42,6 +42,7 @@ const animationTypeForLayout = (layout) => typeof layout === 'string' && animati
42
42
  * and `HTMLVisualElement` internals Framer Motion uses.
43
43
  */
44
44
  export class MotionDomProjectionAdapter {
45
+ static adapters = new WeakMap();
45
46
  visualElement;
46
47
  projection;
47
48
  element = null;
@@ -59,6 +60,7 @@ export class MotionDomProjectionAdapter {
59
60
  }, { allowProjection: true });
60
61
  this.projection = new HTMLProjectionNode(this.visualElement.latestValues, parent?.projection);
61
62
  this.visualElement.projection = this.projection;
63
+ MotionDomProjectionAdapter.adapters.set(this.projection, this);
62
64
  }
63
65
  /**
64
66
  * Update projection options from current Svelte props.
@@ -82,6 +84,7 @@ export class MotionDomProjectionAdapter {
82
84
  this.projection.setOptions({
83
85
  layout: options.layout,
84
86
  layoutId: options.layoutId,
87
+ layoutScroll: options.layoutScroll,
85
88
  animationType: animationTypeForLayout(options.layout),
86
89
  transition: options.transition,
87
90
  visualElement: this.visualElement
@@ -104,6 +107,7 @@ export class MotionDomProjectionAdapter {
104
107
  if (this.element)
105
108
  this.unmount();
106
109
  this.element = element;
110
+ MotionDomProjectionAdapter.adapters.set(this.projection, this);
107
111
  this.visualElement.mount(element);
108
112
  this.seedLayout();
109
113
  }
@@ -124,6 +128,7 @@ export class MotionDomProjectionAdapter {
124
128
  this.projection.scheduleCheckAfterUnmount();
125
129
  this.visualElement.unmount();
126
130
  visualElementStore.delete(element);
131
+ MotionDomProjectionAdapter.adapters.delete(this.projection);
127
132
  this.element = null;
128
133
  this.lastLayout = undefined;
129
134
  }
@@ -138,7 +143,7 @@ export class MotionDomProjectionAdapter {
138
143
  * ```
139
144
  */
140
145
  willUpdate() {
141
- if (!this.element || !this.layoutId)
146
+ if (!this.element || !this.layout)
142
147
  return;
143
148
  this.projection.willUpdate();
144
149
  }
@@ -153,7 +158,7 @@ export class MotionDomProjectionAdapter {
153
158
  * ```
154
159
  */
155
160
  didUpdate() {
156
- if (!this.element || !this.layoutId)
161
+ if (!this.element || !this.layout)
157
162
  return;
158
163
  this.projection.root?.didUpdate();
159
164
  this.refreshCachedLayout();
@@ -171,6 +176,7 @@ export class MotionDomProjectionAdapter {
171
176
  seedLayout() {
172
177
  if (!this.element)
173
178
  return;
179
+ this.updatePathScroll();
174
180
  this.projection.isLayoutDirty = true;
175
181
  this.projection.updateLayout();
176
182
  this.lastLayout = cloneMeasurements(this.projection.layout);
@@ -191,19 +197,80 @@ export class MotionDomProjectionAdapter {
191
197
  * ```
192
198
  */
193
199
  commitObservedLayoutChange() {
194
- if (!this.element || !this.layoutId)
200
+ if (!this.element || !this.layout)
195
201
  return;
196
- const snapshot = cloneMeasurements(this.lastLayout);
197
- if (!snapshot) {
202
+ if (!this.lastLayout) {
198
203
  this.seedLayout();
199
204
  return;
200
205
  }
201
206
  this.projection.root?.startUpdate();
202
- this.projection.snapshot = snapshot;
203
- this.projection.isLayoutDirty = true;
207
+ this.seedCachedSnapshotsForSubtree(this.projection);
204
208
  this.projection.root?.didUpdate();
205
209
  this.refreshCachedLayout();
206
210
  }
211
+ /**
212
+ * Finish any active upstream layout animation in this subtree.
213
+ *
214
+ * @returns Nothing.
215
+ *
216
+ * @example
217
+ * ```ts
218
+ * adapter.finishAnimation()
219
+ * ```
220
+ */
221
+ finishAnimation() {
222
+ if (!this.element || !this.layout)
223
+ return;
224
+ this.finishAnimationForSubtree(this.projection);
225
+ this.seedLayout();
226
+ }
227
+ /**
228
+ * Check whether this projection subtree has an active layout animation.
229
+ *
230
+ * @returns `true` when this projection subtree is currently animating.
231
+ *
232
+ * @example
233
+ * ```ts
234
+ * if (adapter.isAnimating()) adapter.finishAnimation()
235
+ * ```
236
+ */
237
+ isAnimating() {
238
+ return this.isAnimatingSubtree(this.projection);
239
+ }
240
+ seedCachedSnapshotsForSubtree(projection) {
241
+ const adapter = MotionDomProjectionAdapter.adapters.get(projection);
242
+ const snapshot = cloneMeasurements(adapter?.lastLayout);
243
+ if (snapshot && projection.options.layout) {
244
+ projection.snapshot = snapshot;
245
+ projection.isLayoutDirty = true;
246
+ }
247
+ for (const child of projection.children) {
248
+ this.seedCachedSnapshotsForSubtree(child);
249
+ }
250
+ }
251
+ finishAnimationForSubtree(projection) {
252
+ projection.finishAnimation();
253
+ projection.targetDelta = projection.relativeTarget = projection.target = undefined;
254
+ projection.isProjectionDirty = true;
255
+ projection.scheduleRender();
256
+ for (const child of projection.children) {
257
+ this.finishAnimationForSubtree(child);
258
+ }
259
+ }
260
+ isAnimatingSubtree(projection) {
261
+ if (projection.currentAnimation)
262
+ return true;
263
+ for (const child of projection.children) {
264
+ if (this.isAnimatingSubtree(child))
265
+ return true;
266
+ }
267
+ return false;
268
+ }
269
+ updatePathScroll() {
270
+ for (const node of this.projection.path) {
271
+ node.updateScroll();
272
+ }
273
+ }
207
274
  refreshCachedLayout() {
208
275
  requestAnimationFrame(() => {
209
276
  this.lastLayout = cloneMeasurements(this.projection.layout);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanspeak/svelte-motion",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Framer Motion for Svelte 5. Declarative motion.<tag> components with AnimatePresence exit animations, gestures (hover, tap, drag, focus, in-view), variants, FLIP layout animations, shared-layout transitions, spring physics, and scroll-linked motion values. The drop-in Framer Motion alternative for Svelte and SvelteKit.",
5
5
  "keywords": [
6
6
  "svelte",