@tamagui/animation-helpers 2.7.7 → 3.0.0-beta.1097.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.
Files changed (79) hide show
  1. package/dist/cjs/index.cjs +32 -21
  2. package/dist/cjs/index.native.cjs +46 -0
  3. package/dist/cjs/index.native.js +33 -21
  4. package/dist/cjs/index.native.js.map +1 -1
  5. package/dist/cjs/presets.cjs +77 -0
  6. package/dist/cjs/presets.native.cjs +79 -0
  7. package/dist/cjs/presets.native.js +81 -0
  8. package/dist/cjs/presets.native.js.map +1 -0
  9. package/dist/cjs/propertyNames.cjs +91 -0
  10. package/dist/cjs/propertyNames.native.cjs +95 -0
  11. package/dist/cjs/propertyNames.native.js +97 -0
  12. package/dist/cjs/propertyNames.native.js.map +1 -0
  13. package/dist/cjs/resolveTransition.cjs +345 -0
  14. package/dist/cjs/resolveTransition.native.cjs +424 -0
  15. package/dist/cjs/resolveTransition.native.js +426 -0
  16. package/dist/cjs/resolveTransition.native.js.map +1 -0
  17. package/dist/cjs/transitionResolver.cjs +34 -0
  18. package/dist/cjs/transitionResolver.native.cjs +37 -0
  19. package/dist/cjs/transitionResolver.native.js +39 -0
  20. package/dist/cjs/transitionResolver.native.js.map +1 -0
  21. package/dist/cjs/types.cjs +9 -11
  22. package/dist/cjs/types.native.cjs +18 -0
  23. package/dist/cjs/types.native.js +9 -10
  24. package/dist/cjs/types.native.js.map +1 -1
  25. package/dist/esm/index.js +6 -3
  26. package/dist/esm/index.mjs +6 -3
  27. package/dist/esm/index.native.js +6 -3
  28. package/dist/esm/presets.mjs +57 -0
  29. package/dist/esm/presets.mjs.map +1 -0
  30. package/dist/esm/presets.native.js +57 -0
  31. package/dist/esm/presets.native.js.map +1 -0
  32. package/dist/esm/propertyNames.mjs +67 -0
  33. package/dist/esm/propertyNames.mjs.map +1 -0
  34. package/dist/esm/propertyNames.native.js +69 -0
  35. package/dist/esm/propertyNames.native.js.map +1 -0
  36. package/dist/esm/resolveTransition.mjs +315 -0
  37. package/dist/esm/resolveTransition.mjs.map +1 -0
  38. package/dist/esm/resolveTransition.native.js +392 -0
  39. package/dist/esm/resolveTransition.native.js.map +1 -0
  40. package/dist/esm/transitionResolver.mjs +11 -0
  41. package/dist/esm/transitionResolver.mjs.map +1 -0
  42. package/dist/esm/transitionResolver.native.js +12 -0
  43. package/dist/esm/transitionResolver.native.js.map +1 -0
  44. package/dist/esm/types.mjs +0 -2
  45. package/dist/esm/types.native.js +0 -2
  46. package/package.json +23 -2
  47. package/src/index.ts +39 -9
  48. package/src/presets.ts +40 -0
  49. package/src/propertyNames.ts +78 -0
  50. package/src/resolveTransition.ts +503 -0
  51. package/src/transitionResolver.ts +35 -0
  52. package/src/types.ts +42 -46
  53. package/types/index.d.ts +6 -2
  54. package/types/index.d.ts.map +2 -2
  55. package/types/presets.d.ts +79 -0
  56. package/types/presets.d.ts.map +11 -0
  57. package/types/propertyNames.d.ts +13 -0
  58. package/types/propertyNames.d.ts.map +11 -0
  59. package/types/resolveTransition.d.ts +108 -0
  60. package/types/resolveTransition.d.ts.map +11 -0
  61. package/types/transitionResolver.d.ts +30 -0
  62. package/types/transitionResolver.d.ts.map +11 -0
  63. package/types/types.d.ts +35 -42
  64. package/types/types.d.ts.map +2 -2
  65. package/dist/cjs/normalizeTransition.cjs +0 -164
  66. package/dist/cjs/normalizeTransition.native.js +0 -231
  67. package/dist/cjs/normalizeTransition.native.js.map +0 -1
  68. package/dist/esm/index.js.map +0 -1
  69. package/dist/esm/index.mjs.map +0 -1
  70. package/dist/esm/index.native.js.map +0 -1
  71. package/dist/esm/normalizeTransition.mjs +0 -134
  72. package/dist/esm/normalizeTransition.mjs.map +0 -1
  73. package/dist/esm/normalizeTransition.native.js +0 -198
  74. package/dist/esm/normalizeTransition.native.js.map +0 -1
  75. package/dist/esm/types.mjs.map +0 -1
  76. package/dist/esm/types.native.js.map +0 -1
  77. package/src/normalizeTransition.ts +0 -255
  78. package/types/normalizeTransition.d.ts +0 -54
  79. package/types/normalizeTransition.d.ts.map +0 -11
@@ -0,0 +1,78 @@
1
+ // which css property a tamagui style key animates under.
2
+ //
3
+ // this is a module of its own, with no imports, because `@tamagui/web` needs
4
+ // it on every styled component while the parser and the spring solver in
5
+ // `resolveTransition` are only needed when a driver is in the bundle.
6
+
7
+ /**
8
+ * transform parts map onto the css property that actually animates them.
9
+ * `translate`, `scale`, and `rotate` are real individual transform properties;
10
+ * everything else can only be reached through `transform` as a whole.
11
+ *
12
+ * collapsing here is deliberate. css cannot animate `rotateX` separately from
13
+ * `skewY` (both live in `transform`), so letting a js driver do it would be a
14
+ * capability that silently disappears the moment you build for the web.
15
+ */
16
+ const transformProperties: Record<string, string> = {
17
+ x: 'translate',
18
+ y: 'translate',
19
+ z: 'translate',
20
+ translate: 'translate',
21
+ translateX: 'translate',
22
+ translateY: 'translate',
23
+ translateZ: 'translate',
24
+ scale: 'scale',
25
+ scaleX: 'scale',
26
+ scaleY: 'scale',
27
+ scaleZ: 'scale',
28
+ rotate: 'rotate',
29
+ rotateX: 'transform',
30
+ rotateY: 'transform',
31
+ rotateZ: 'transform',
32
+ skewX: 'transform',
33
+ skewY: 'transform',
34
+ perspective: 'transform',
35
+ matrix: 'transform',
36
+ transform: 'transform',
37
+ }
38
+
39
+ /**
40
+ * the css property name an entry is filed under, so `backgroundColor` and
41
+ * `background-color` are one key and cannot both silently apply.
42
+ */
43
+ export function canonicalTransitionProperty(key: string): string {
44
+ if (key === 'all' || key === 'none') return key
45
+ const transform = transformProperties[key]
46
+ if (transform) return transform
47
+ return key.includes('-') ? key : key.replace(/[A-Z]/g, '-$&').toLowerCase()
48
+ }
49
+
50
+ export function isTransformProperty(key: string): boolean {
51
+ return key in transformProperties
52
+ }
53
+
54
+ const styleKeysByProperty: Record<string, readonly string[]> = {
55
+ translate: ['translate', 'translateX', 'translateY', 'translateZ', 'x', 'y', 'z'],
56
+ scale: ['scale', 'scaleX', 'scaleY', 'scaleZ'],
57
+ rotate: ['rotate'],
58
+ transform: [
59
+ 'transform',
60
+ 'rotateX',
61
+ 'rotateY',
62
+ 'rotateZ',
63
+ 'skewX',
64
+ 'skewY',
65
+ 'perspective',
66
+ 'matrix',
67
+ ],
68
+ }
69
+
70
+ /**
71
+ * the style keys a css property covers, for drivers that key their per-property
72
+ * options by style key rather than css property (motion, react-native).
73
+ */
74
+ export function styleKeysForProperty(property: string): readonly string[] {
75
+ const transform = styleKeysByProperty[property]
76
+ if (transform) return transform
77
+ return [property.replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase())]
78
+ }
@@ -0,0 +1,503 @@
1
+ // the one place a `transition` prop becomes driver-ready values.
2
+ //
3
+ // every driver and `getSplitStyles` call `resolveTransition`, so there is a
4
+ // single grammar, a single preset lookup, and a single spring solver behind
5
+ // css, reanimated, motion, and react-native. a transition that means one thing
6
+ // on one driver cannot mean something else on another, because none of them
7
+ // decide anything on their own any more.
8
+ //
9
+ // the shape is CSS's: an ordered entry list where the LAST entry naming a
10
+ // property wins for that property, and entries do not inherit from each other.
11
+ // `transition="all 200ms, opacity 150ms ease-out"` reads exactly as it does in
12
+ // a stylesheet.
13
+
14
+ import {
15
+ parseTransition,
16
+ parseTransitionObject,
17
+ springFromDurationBounce,
18
+ springSettleTime,
19
+ springToDurationBounce,
20
+ springToLinearEasing,
21
+ type TransitionBehavior,
22
+ type TransitionDiagnostic,
23
+ type TransitionEntry,
24
+ type TransitionObjectValue,
25
+ type TransitionTiming,
26
+ } from '@tamagui/style-grammar/transitions'
27
+
28
+ import { canonicalTransitionProperty } from './propertyNames'
29
+ import { setTransitionResolver } from './transitionResolver'
30
+
31
+ export type DriverTiming =
32
+ | {
33
+ kind: 'timing'
34
+ durationMs: number
35
+ easing: string
36
+ }
37
+ | {
38
+ kind: 'spring'
39
+ /** the undamped period, the portable "how fast does this feel" number */
40
+ durationMs: number
41
+ bounce: number
42
+ stiffness: number
43
+ damping: number
44
+ mass: number
45
+ /** authored low-level overrides, for drivers that take them raw */
46
+ extra?: Record<string, unknown>
47
+ }
48
+
49
+ export interface ResolvedEntry {
50
+ /** canonical property name, kebab-case, or `all` / `none` */
51
+ property: string
52
+ timing: DriverTiming
53
+ delayMs: number
54
+ behavior: TransitionBehavior
55
+ }
56
+
57
+ export interface ResolvedTransition {
58
+ /** in authored order; later entries win under the css last-wins rule */
59
+ entries: readonly ResolvedEntry[]
60
+ /** the winning `all` entry, or null when only specific properties were named */
61
+ all: ResolvedEntry | null
62
+ /** winning entry per canonical property name, excluding `all` */
63
+ byProperty: Readonly<Record<string, ResolvedEntry>>
64
+ /** true when the author disabled transitions entirely (`transition="none"`) */
65
+ none: boolean
66
+ /**
67
+ * true when any entry was authored as a preset name or a `spring()`, the two
68
+ * things that have no css spelling of their own. a resolution without one is
69
+ * already plain css and needs no driver at all.
70
+ */
71
+ fused: boolean
72
+ /** replaces this whole resolution while mounting, when the author set one */
73
+ enter: ResolvedTransition | null
74
+ /** replaces this whole resolution while unmounting */
75
+ exit: ResolvedTransition | null
76
+ diagnostics: readonly TransitionDiagnostic[]
77
+ }
78
+
79
+ const emptyResolved: ResolvedTransition = {
80
+ entries: [],
81
+ all: null,
82
+ byProperty: {},
83
+ none: false,
84
+ fused: false,
85
+ enter: null,
86
+ exit: null,
87
+ diagnostics: [],
88
+ }
89
+
90
+ /** the resolution that applies in a given animation state */
91
+ export function forAnimationState(
92
+ resolved: ResolvedTransition,
93
+ state: 'enter' | 'exit' | 'default'
94
+ ): ResolvedTransition {
95
+ if (state === 'enter' && resolved.enter) return resolved.enter
96
+ if (state === 'exit' && resolved.exit) return resolved.exit
97
+ return resolved
98
+ }
99
+
100
+ function timeToMs(value: string): number {
101
+ // the grammar guarantees a `<number>ms` or `<number>s` token here
102
+ return value.endsWith('ms') ? Number.parseFloat(value) : Number.parseFloat(value) * 1000
103
+ }
104
+
105
+ function springTiming(
106
+ durationMs: number,
107
+ bounce: number,
108
+ extra?: Record<string, unknown>
109
+ ): DriverTiming {
110
+ const mass = typeof extra?.mass === 'number' ? extra.mass : 1
111
+ const physics = springFromDurationBounce({ duration: durationMs, bounce }, mass)
112
+ return {
113
+ kind: 'spring',
114
+ durationMs,
115
+ bounce,
116
+ // authored physics win over the derived pair, so the escape hatch is exact
117
+ stiffness: typeof extra?.stiffness === 'number' ? extra.stiffness : physics.stiffness,
118
+ damping: typeof extra?.damping === 'number' ? extra.damping : physics.damping,
119
+ mass,
120
+ extra,
121
+ }
122
+ }
123
+
124
+ /**
125
+ * turns one entry in a driver's `animations` config into a timing.
126
+ *
127
+ * accepts every shape `PresetConfig` allows: a css string (`'350ms ease-out'`),
128
+ * spring physics (`{ type: 'spring', damping, stiffness, mass }`), a timing
129
+ * (`{ type: 'timing', duration, easing }`), and the canonical pair
130
+ * (`{ duration, bounce }`). configs written for any driver therefore resolve
131
+ * to the same motion on all four.
132
+ */
133
+ export function presetToTiming(preset: unknown): DriverTiming | null {
134
+ if (typeof preset === 'string') {
135
+ const parsed = parseTransition(preset)
136
+ if (!parsed.ok || parsed.value.kind !== 'transition') return null
137
+ const entry = parsed.value.entries[0]
138
+ if (!entry) return null
139
+ return timingToDriver(entry.timing, null)
140
+ }
141
+
142
+ if (!preset || typeof preset !== 'object') return null
143
+ const config = preset as Record<string, unknown>
144
+ const duration = typeof config.duration === 'number' ? config.duration : undefined
145
+ const hasPhysics =
146
+ typeof config.stiffness === 'number' || typeof config.damping === 'number'
147
+
148
+ const stiffness = typeof config.stiffness === 'number' ? config.stiffness : undefined
149
+ const damping = typeof config.damping === 'number' ? config.damping : undefined
150
+
151
+ const isSpring =
152
+ config.type === 'spring' || typeof config.bounce === 'number' || hasPhysics
153
+
154
+ if (isSpring) {
155
+ const mass = typeof config.mass === 'number' ? config.mass : 1
156
+ if (stiffness !== undefined) {
157
+ const canonical = springToDurationBounce({
158
+ stiffness,
159
+ damping: damping ?? 2 * Math.sqrt(stiffness * mass),
160
+ mass,
161
+ })
162
+ return {
163
+ kind: 'spring',
164
+ durationMs: duration ?? canonical.duration,
165
+ bounce: typeof config.bounce === 'number' ? config.bounce : canonical.bounce,
166
+ stiffness,
167
+ damping: damping ?? 2 * Math.sqrt(stiffness * mass),
168
+ mass,
169
+ extra: config,
170
+ }
171
+ }
172
+ if (duration !== undefined) {
173
+ return springTiming(
174
+ duration,
175
+ typeof config.bounce === 'number' ? config.bounce : 0,
176
+ config
177
+ )
178
+ }
179
+ return null
180
+ }
181
+
182
+ if (duration === undefined) return null
183
+ const easing = typeof config.easing === 'string' ? config.easing : 'ease'
184
+ return { kind: 'timing', durationMs: duration, easing }
185
+ }
186
+
187
+ /**
188
+ * lowers a grammar timing atom into driver values, resolving a preset name
189
+ * against the driver's configured animations.
190
+ */
191
+ function timingToDriver(
192
+ timing: TransitionTiming,
193
+ animations: Record<string, unknown> | null
194
+ ): DriverTiming | null {
195
+ if (timing.type === 'css') {
196
+ return {
197
+ kind: 'timing',
198
+ durationMs: timeToMs(timing.duration),
199
+ easing: timing.timingFunction,
200
+ }
201
+ }
202
+
203
+ if (timing.type === 'spring') {
204
+ return springTiming(
205
+ timeToMs(timing.duration),
206
+ timing.bounce,
207
+ timing.config as Record<string, unknown> | undefined
208
+ )
209
+ }
210
+
211
+ const preset = animations?.[timing.name]
212
+ if (preset === undefined) return null
213
+ const base = presetToTiming(preset)
214
+ if (!base || !timing.config) return base
215
+
216
+ // authored overrides land on top of the preset, in the preset's own terms
217
+ const overrides = timing.config as Record<string, unknown>
218
+ if (base.kind === 'spring' || typeof overrides.bounce === 'number') {
219
+ const overrideDuration =
220
+ typeof overrides.duration === 'number'
221
+ ? overrides.duration
222
+ : typeof overrides.duration === 'string'
223
+ ? timeToMs(overrides.duration)
224
+ : undefined
225
+ const overrideBounce =
226
+ typeof overrides.bounce === 'number' ? overrides.bounce : undefined
227
+ const durationMs = overrideDuration ?? base.durationMs
228
+ const bounce = overrideBounce ?? (base.kind === 'spring' ? base.bounce : 0)
229
+ let carried = base.kind === 'spring' ? base.extra : undefined
230
+ // a duration or bounce override re-solves the spring, so a preset written as
231
+ // stiffness/damping cannot pass those two through: they ARE what the
232
+ // override replaces. mass belongs to the object rather than the curve, so it
233
+ // carries. physics written in the override still win, exactly as elsewhere.
234
+ if (carried && (overrideDuration !== undefined || overrideBounce !== undefined)) {
235
+ const { stiffness, damping, ...rest } = carried
236
+ carried = rest
237
+ }
238
+ return springTiming(durationMs, bounce, { ...carried, ...overrides })
239
+ }
240
+
241
+ return {
242
+ kind: 'timing',
243
+ durationMs:
244
+ typeof overrides.duration === 'number'
245
+ ? overrides.duration
246
+ : typeof overrides.duration === 'string'
247
+ ? timeToMs(overrides.duration)
248
+ : base.durationMs,
249
+ easing: typeof overrides.easing === 'string' ? overrides.easing : base.easing,
250
+ }
251
+ }
252
+
253
+ const namedEasings: Record<string, readonly [number, number, number, number]> = {
254
+ linear: [0, 0, 1, 1],
255
+ ease: [0.25, 0.1, 0.25, 1],
256
+ 'ease-in': [0.42, 0, 1, 1],
257
+ 'ease-out': [0, 0, 0.58, 1],
258
+ 'ease-in-out': [0.42, 0, 0.58, 1],
259
+ }
260
+
261
+ /**
262
+ * a css easing as cubic-bezier control points, which is the only easing shape
263
+ * reanimated, motion, and react-native all accept.
264
+ *
265
+ * returns null for `steps()` and `linear()`, which have no bezier equivalent.
266
+ * a driver that gets null should fall back to its own default rather than
267
+ * pretending it applied something.
268
+ */
269
+ export function easingToBezier(
270
+ easing: string
271
+ ): readonly [number, number, number, number] | null {
272
+ const named = namedEasings[easing]
273
+ if (named) return named
274
+ if (!easing.startsWith('cubic-bezier(')) return null
275
+ const parts = easing.slice('cubic-bezier('.length, -1).split(',')
276
+ if (parts.length !== 4) return null
277
+ const points = parts.map((part) => Number.parseFloat(part))
278
+ if (points.some((point) => !Number.isFinite(point))) return null
279
+ return points as unknown as [number, number, number, number]
280
+ }
281
+
282
+ export interface ResolveTransitionOptions {
283
+ /** the driver's animations config; its keys are the valid preset names */
284
+ animations?: Record<string, unknown> | null
285
+ /** config shorthands, so `transition="bg 200ms"` names backgroundColor */
286
+ shorthands?: Record<string, string> | null
287
+ }
288
+
289
+ const cacheByAnimations = new WeakMap<object, Map<string, ResolvedTransition>>()
290
+ const objectCache = new WeakMap<object, ResolvedTransition>()
291
+ const noAnimations: Record<string, unknown> = {}
292
+
293
+ /**
294
+ * parses and resolves a `transition` prop into driver-ready entries.
295
+ *
296
+ * memoized per (animations config, authored value), because this runs on every
297
+ * render of every component carrying a transition.
298
+ */
299
+ export function resolveTransition(
300
+ transition: TransitionObjectValue | null | undefined,
301
+ options: ResolveTransitionOptions = {}
302
+ ): ResolvedTransition {
303
+ if (transition == null || transition === '') return emptyResolved
304
+
305
+ const animations = options.animations ?? null
306
+ const cacheKey = (animations ?? noAnimations) as object
307
+
308
+ if (typeof transition === 'string') {
309
+ let byValue = cacheByAnimations.get(cacheKey)
310
+ if (!byValue) {
311
+ byValue = new Map()
312
+ cacheByAnimations.set(cacheKey, byValue)
313
+ }
314
+ const hit = byValue.get(transition)
315
+ if (hit) return hit
316
+ const built = buildResolved(transition, animations, options.shorthands ?? null)
317
+ // an authored value set is bounded in practice, but a codegen loop is not
318
+ if (byValue.size > 2048) byValue.clear()
319
+ byValue.set(transition, built)
320
+ return built
321
+ }
322
+
323
+ if (typeof transition !== 'object') return emptyResolved
324
+ const hit = objectCache.get(transition)
325
+ if (hit) return hit
326
+ const built = buildResolved(transition, animations, options.shorthands ?? null)
327
+ objectCache.set(transition, built)
328
+ return built
329
+ }
330
+
331
+ function buildResolved(
332
+ transition: TransitionObjectValue,
333
+ animations: Record<string, unknown> | null,
334
+ shorthands: Record<string, string> | null
335
+ ): ResolvedTransition {
336
+ const presetNames = animations ? new Set(Object.keys(animations)) : undefined
337
+ const parsed = parseTransitionObject(transition, presetNames)
338
+
339
+ if (!parsed.ok) {
340
+ return { ...emptyResolved, diagnostics: parsed.diagnostics }
341
+ }
342
+ if (parsed.value.kind !== 'transition') return emptyResolved
343
+
344
+ return fromEntries(
345
+ parsed.value.entries,
346
+ animations,
347
+ shorthands,
348
+ parsed.value.enter
349
+ ? fromEntries(parsed.value.enter, animations, shorthands, null, null)
350
+ : null,
351
+ parsed.value.exit
352
+ ? fromEntries(parsed.value.exit, animations, shorthands, null, null)
353
+ : null
354
+ )
355
+ }
356
+
357
+ const transformFamily = ['transform', 'translate', 'scale', 'rotate'] as const
358
+
359
+ function fromEntries(
360
+ source: readonly TransitionEntry[],
361
+ animations: Record<string, unknown> | null,
362
+ shorthands: Record<string, string> | null,
363
+ enter: ResolvedTransition | null,
364
+ exit: ResolvedTransition | null
365
+ ): ResolvedTransition {
366
+ const entries: ResolvedEntry[] = []
367
+ const byProperty: Record<string, ResolvedEntry> = {}
368
+ const diagnostics: TransitionDiagnostic[] = []
369
+ let all: ResolvedEntry | null = null
370
+ let none = false
371
+ let fused = false
372
+
373
+ for (const entry of source) {
374
+ if (entry.property === 'none') {
375
+ none = true
376
+ continue
377
+ }
378
+
379
+ if (entry.timing.type !== 'css') fused = true
380
+
381
+ const timing = timingToDriver(entry.timing, animations)
382
+ if (!timing) {
383
+ diagnostics.push({
384
+ code: 'transition-invalid-token',
385
+ token: entry.timing.type === 'preset' ? entry.timing.name : undefined,
386
+ message:
387
+ entry.timing.type === 'preset'
388
+ ? `"${entry.timing.name}" is not a configured animation`
389
+ : 'transition timing could not be resolved',
390
+ })
391
+ continue
392
+ }
393
+
394
+ const expanded = shorthands?.[entry.property] ?? entry.property
395
+ const property = canonicalTransitionProperty(expanded)
396
+ const delayMs = timeToMs(entry.delay)
397
+ // tamagui writes x/y into the css `translate` longhand, scale into
398
+ // `scale` and rotate into `rotate`, so an authored `transform` covers the
399
+ // whole family or it transitions nothing the author can see.
400
+ for (const name of expanded === 'transform' ? transformFamily : [property]) {
401
+ const resolved: ResolvedEntry = {
402
+ property: name,
403
+ timing,
404
+ delayMs,
405
+ behavior: entry.behavior,
406
+ }
407
+ entries.push(resolved)
408
+ // last wins, exactly as a stylesheet resolves a repeated property
409
+ if (name === 'all') all = resolved
410
+ else byProperty[name] = resolved
411
+ }
412
+ }
413
+
414
+ return { entries, all, byProperty, none, fused, enter, exit, diagnostics }
415
+ }
416
+
417
+ /**
418
+ * the transition that applies to one style key, under css last-wins.
419
+ *
420
+ * an authored `transform` already expanded into the whole family in
421
+ * `fromEntries`, so `x`, `scale` and `rotate` find a direct entry here.
422
+ */
423
+ export function getTransitionForKey(
424
+ resolved: ResolvedTransition,
425
+ key: string
426
+ ): ResolvedEntry | null {
427
+ if (resolved.none) return null
428
+ const direct = resolved.byProperty[canonicalTransitionProperty(key)]
429
+ if (direct) return direct
430
+ return resolved.all
431
+ }
432
+
433
+ /** true when anything at all will animate */
434
+ export function hasTransition(resolved: ResolvedTransition): boolean {
435
+ return !resolved.none && resolved.entries.length > 0
436
+ }
437
+
438
+ /**
439
+ * how long a timing actually runs, which for a spring is its settle time and
440
+ * not its nominal duration. this is the number a completion deadline needs.
441
+ */
442
+ export function getSettleMs(timing: DriverTiming): number {
443
+ if (timing.kind === 'timing') return timing.durationMs
444
+ return springSettleTime({ duration: timing.durationMs, bounce: timing.bounce })
445
+ }
446
+
447
+ /** the longest anything will take, for the driver's completion bookkeeping */
448
+ export function getMaxDurationMs(resolved: ResolvedTransition): number {
449
+ let max = 0
450
+ for (const entry of resolved.entries) {
451
+ const total = getSettleMs(entry.timing) + entry.delayMs
452
+ if (total > max) max = total
453
+ }
454
+ return max
455
+ }
456
+
457
+ const linearEasingCache = new Map<string, { easing: string; durationMs: number }>()
458
+
459
+ /**
460
+ * one entry as css. a spring becomes a `linear()` easing sampled across its
461
+ * settle time, which is how a real spring curve, overshoot included, survives
462
+ * with no javascript running.
463
+ */
464
+ export function entryToCSS(entry: ResolvedEntry): string {
465
+ const delay = entry.delayMs ? ` ${entry.delayMs}ms` : ''
466
+ const behavior = entry.behavior === 'allow-discrete' ? ' allow-discrete' : ''
467
+
468
+ if (entry.timing.kind === 'timing') {
469
+ return `${entry.property} ${entry.timing.durationMs}ms ${entry.timing.easing}${delay}${behavior}`
470
+ }
471
+
472
+ const key = `${entry.timing.durationMs}/${entry.timing.bounce}`
473
+ let spring = linearEasingCache.get(key)
474
+ if (!spring) {
475
+ spring = springToLinearEasing({
476
+ duration: entry.timing.durationMs,
477
+ bounce: entry.timing.bounce,
478
+ })
479
+ if (linearEasingCache.size > 256) linearEasingCache.clear()
480
+ linearEasingCache.set(key, spring)
481
+ }
482
+ return `${entry.property} ${Math.round(spring.durationMs)}ms ${spring.easing}${delay}${behavior}`
483
+ }
484
+
485
+ /**
486
+ * the whole resolution as a css `transition` value, or `undefined` when
487
+ * nothing animates. entry order is preserved, so css last-wins does the
488
+ * per-property resolution for us in the browser.
489
+ */
490
+ export function toCSSTransition(resolved: ResolvedTransition): string | undefined {
491
+ if (resolved.none) return 'none'
492
+ if (!resolved.entries.length) return undefined
493
+
494
+ let out = ''
495
+ for (const entry of resolved.entries) {
496
+ out += `${out ? ', ' : ''}${entryToCSS(entry)}`
497
+ }
498
+ return out || undefined
499
+ }
500
+
501
+ // see `transitionResolver.ts`: this is what keeps the grammar out of a bundle
502
+ // that has no animation driver in it.
503
+ setTransitionResolver({ resolve: resolveTransition, toCSS: toCSSTransition })
@@ -0,0 +1,35 @@
1
+ import type { ResolvedTransition } from './resolveTransition'
2
+
3
+ /**
4
+ * `getSplitStyles` needs to know whether a `transition` string is plain css or
5
+ * needs a driver, and that answer has to come from the same grammar the
6
+ * compiler uses. Importing the grammar into `@tamagui/web` would put the
7
+ * parser and the spring solver in every bundle, including one with no
8
+ * animations in it at all, which is about 3.6kb gzip of runtime nobody asked
9
+ * for.
10
+ *
11
+ * So `resolveTransition` registers itself here on import. Loading any driver
12
+ * loads it, and a bundle with no driver in it has no presets to resolve, so
13
+ * the absent resolver is the correct answer rather than a missing one.
14
+ *
15
+ * The slot lives on `globalThis` because a bundler routinely gives one process
16
+ * two copies of this module: Vite's SSR graph and its optimized-deps graph each
17
+ * carry their own, and a module-local `let` then leaves the reader looking at
18
+ * the copy nobody registered into. The server rendered a transition as a static
19
+ * class while the client resolved it to a driver animation, which React reports
20
+ * as a hydration mismatch.
21
+ */
22
+ export type TransitionResolver = {
23
+ resolve: (value: any, options: { animations?: any }) => ResolvedTransition
24
+ toCSS: (resolved: ResolvedTransition) => string | undefined
25
+ }
26
+
27
+ const slot = Symbol.for('tamagui.transitionResolver')
28
+
29
+ export function setTransitionResolver(next: TransitionResolver): void {
30
+ ;(globalThis as any)[slot] = next
31
+ }
32
+
33
+ export function getTransitionResolver(): TransitionResolver | null {
34
+ return (globalThis as any)[slot] ?? null
35
+ }