@tamagui/web 2.3.3 → 2.4.0-1783110557100

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 (205) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/cjs/Tamagui.native.js.map +1 -1
  3. package/dist/cjs/_withStableStyle.native.js.map +1 -1
  4. package/dist/cjs/animationDriverTypes.test-d.native.js.map +1 -1
  5. package/dist/cjs/config.native.js.map +1 -1
  6. package/dist/cjs/constants/constants.native.js.map +1 -1
  7. package/dist/cjs/constants/isDevTools.native.js.map +1 -1
  8. package/dist/cjs/contexts/ComponentContext.native.js.map +1 -1
  9. package/dist/cjs/contexts/GroupContext.native.js.map +1 -1
  10. package/dist/cjs/createComponent.cjs +4 -3
  11. package/dist/cjs/createComponent.native.js +7 -6
  12. package/dist/cjs/createComponent.native.js.map +1 -1
  13. package/dist/cjs/createFont.native.js.map +1 -1
  14. package/dist/cjs/createShorthands.native.js.map +1 -1
  15. package/dist/cjs/createTamagui.native.js.map +1 -1
  16. package/dist/cjs/createTokens.native.js.map +1 -1
  17. package/dist/cjs/createVariable.native.js.map +1 -1
  18. package/dist/cjs/createVariables.native.js.map +1 -1
  19. package/dist/cjs/defaultComponentState.native.js.map +1 -1
  20. package/dist/cjs/eventHandling.cjs +1 -1
  21. package/dist/cjs/eventHandling.native.js +12 -36
  22. package/dist/cjs/eventHandling.native.js.map +1 -1
  23. package/dist/cjs/helpers/createDesignSystem.native.js.map +1 -1
  24. package/dist/cjs/helpers/createMediaStyle.native.js.map +1 -1
  25. package/dist/cjs/helpers/createStyledContext.native.js.map +1 -1
  26. package/dist/cjs/helpers/defaultAnimationDriver.native.js.map +1 -1
  27. package/dist/cjs/helpers/defaultOffset.native.js.map +1 -1
  28. package/dist/cjs/helpers/expandStyle.native.js.map +1 -1
  29. package/dist/cjs/helpers/expandStyles.native.js.map +1 -1
  30. package/dist/cjs/helpers/getDefaultProps.native.js.map +1 -1
  31. package/dist/cjs/helpers/getDynamicVal.native.js.map +1 -1
  32. package/dist/cjs/helpers/getExpandedShorthands.native.js.map +1 -1
  33. package/dist/cjs/helpers/getFontLanguage.native.js.map +1 -1
  34. package/dist/cjs/helpers/getGroupPropParts.native.js.map +1 -1
  35. package/dist/cjs/helpers/getShorthandValue.native.js.map +1 -1
  36. package/dist/cjs/helpers/getSplitStyles.cjs +5 -0
  37. package/dist/cjs/helpers/getSplitStyles.native.js +9 -0
  38. package/dist/cjs/helpers/getSplitStyles.native.js.map +1 -1
  39. package/dist/cjs/helpers/getTokenForKey.native.js.map +1 -1
  40. package/dist/cjs/helpers/getVariantExtras.native.js.map +1 -1
  41. package/dist/cjs/helpers/insertStyleRule.native.js.map +1 -1
  42. package/dist/cjs/helpers/isActivePlatform.native.js.map +1 -1
  43. package/dist/cjs/helpers/isActiveTheme.native.js.map +1 -1
  44. package/dist/cjs/helpers/isObj.native.js.map +1 -1
  45. package/dist/cjs/helpers/isTamaguiComponent.native.js.map +1 -1
  46. package/dist/cjs/helpers/isTamaguiElement.native.js.map +1 -1
  47. package/dist/cjs/helpers/log.native.js.map +1 -1
  48. package/dist/cjs/helpers/mainThreadPressEvents.native.js +20 -3
  49. package/dist/cjs/helpers/mainThreadPressEvents.native.js.map +1 -1
  50. package/dist/cjs/helpers/mediaObjectToString.native.js.map +1 -1
  51. package/dist/cjs/helpers/mediaState.native.js.map +1 -1
  52. package/dist/cjs/helpers/mergeProps.native.js.map +1 -1
  53. package/dist/cjs/helpers/mergeRenderElementProps.native.js.map +1 -1
  54. package/dist/cjs/helpers/mergeSlotStyleProps.native.js.map +1 -1
  55. package/dist/cjs/helpers/mergeVariants.native.js.map +1 -1
  56. package/dist/cjs/helpers/nativeOnlyProps.native.js.map +1 -1
  57. package/dist/cjs/helpers/normalizeStyle.native.js.map +1 -1
  58. package/dist/cjs/helpers/normalizeValueWithProperty.native.js.map +1 -1
  59. package/dist/cjs/helpers/objectIdentityKey.native.js.map +1 -1
  60. package/dist/cjs/helpers/parseNativeStyle.native.js.map +1 -1
  61. package/dist/cjs/helpers/pointerEvents.native.js +26 -20
  62. package/dist/cjs/helpers/pointerEvents.native.js.map +1 -1
  63. package/dist/cjs/helpers/propMapper.native.js +40 -15
  64. package/dist/cjs/helpers/propMapper.native.js.map +1 -1
  65. package/dist/cjs/helpers/proxyThemeToParents.native.js.map +1 -1
  66. package/dist/cjs/helpers/proxyThemeVariables.native.js.map +1 -1
  67. package/dist/cjs/helpers/pseudoDescriptors.native.js.map +1 -1
  68. package/dist/cjs/helpers/pseudoTransitions.native.js.map +1 -1
  69. package/dist/cjs/helpers/registerCSSVariable.native.js.map +1 -1
  70. package/dist/cjs/helpers/resolveAnimationDriver.native.js.map +1 -1
  71. package/dist/cjs/helpers/resolveCompoundTokens.native.js.map +1 -1
  72. package/dist/cjs/helpers/skipProps.native.js.map +1 -1
  73. package/dist/cjs/helpers/sortString.native.js.map +1 -1
  74. package/dist/cjs/helpers/styleOriginalValues.native.js.map +1 -1
  75. package/dist/cjs/helpers/subscribeToContextGroup.native.js.map +1 -1
  76. package/dist/cjs/helpers/themeable.native.js.map +1 -1
  77. package/dist/cjs/helpers/themes.native.js.map +1 -1
  78. package/dist/cjs/helpers/timer.native.js.map +1 -1
  79. package/dist/cjs/helpers/transformsToString.native.js.map +1 -1
  80. package/dist/cjs/helpers/useRenderElement.native.js.map +1 -1
  81. package/dist/cjs/helpers/wrapStyleTags.native.js.map +1 -1
  82. package/dist/cjs/hooks/getThemeProxied.native.js.map +1 -1
  83. package/dist/cjs/hooks/useComponentState.cjs +25 -1
  84. package/dist/cjs/hooks/useComponentState.native.js +25 -1
  85. package/dist/cjs/hooks/useComponentState.native.js.map +1 -1
  86. package/dist/cjs/hooks/useDisableSSR.native.js.map +1 -1
  87. package/dist/cjs/hooks/useIsTouchDevice.native.js.map +1 -1
  88. package/dist/cjs/hooks/useMedia.cjs +107 -42
  89. package/dist/cjs/hooks/useMedia.native.js +149 -60
  90. package/dist/cjs/hooks/useMedia.native.js.map +1 -1
  91. package/dist/cjs/hooks/useProps.native.js.map +1 -1
  92. package/dist/cjs/hooks/useTheme.cjs +17 -4
  93. package/dist/cjs/hooks/useTheme.native.js +18 -4
  94. package/dist/cjs/hooks/useTheme.native.js.map +1 -1
  95. package/dist/cjs/hooks/useThemeName.native.js.map +1 -1
  96. package/dist/cjs/hooks/useThemeState.cjs +132 -67
  97. package/dist/cjs/hooks/useThemeState.native.js +144 -70
  98. package/dist/cjs/hooks/useThemeState.native.js.map +1 -1
  99. package/dist/cjs/index.cjs +2 -0
  100. package/dist/cjs/index.native.js +2 -0
  101. package/dist/cjs/index.native.js.map +1 -1
  102. package/dist/cjs/inject-styles.native.js.map +1 -1
  103. package/dist/cjs/insertFont.native.js.map +1 -1
  104. package/dist/cjs/interfaces/CSSColorNames.native.js.map +1 -1
  105. package/dist/cjs/interfaces/GetRef.native.js.map +1 -1
  106. package/dist/cjs/interfaces/RNExclusiveTypes.native.js.map +1 -1
  107. package/dist/cjs/interfaces/TamaguiComponentEvents.native.js.map +1 -1
  108. package/dist/cjs/interfaces/TamaguiComponentPropsBaseBase.native.js.map +1 -1
  109. package/dist/cjs/opacityModifier.test-d.native.js.map +1 -1
  110. package/dist/cjs/setupHooks.native.js.map +1 -1
  111. package/dist/cjs/setupReactNative.native.js.map +1 -1
  112. package/dist/cjs/styled.native.js.map +1 -1
  113. package/dist/cjs/styledHtml.test-d.native.js.map +1 -1
  114. package/dist/cjs/styledNonStyleProps.test-d.native.js.map +1 -1
  115. package/dist/cjs/type-utils.native.js.map +1 -1
  116. package/dist/cjs/types.native.js.map +1 -1
  117. package/dist/cjs/views/Configuration.native.js.map +1 -1
  118. package/dist/cjs/views/Slot.native.js.map +1 -1
  119. package/dist/cjs/views/TamaguiProvider.native.js.map +1 -1
  120. package/dist/cjs/views/Text.native.js.map +1 -1
  121. package/dist/cjs/views/Theme.cjs +1 -1
  122. package/dist/cjs/views/Theme.native.js +1 -1
  123. package/dist/cjs/views/Theme.native.js.map +1 -1
  124. package/dist/cjs/views/ThemeProvider.native.js.map +1 -1
  125. package/dist/cjs/views/View.native.js.map +1 -1
  126. package/dist/esm/createComponent.mjs +4 -3
  127. package/dist/esm/createComponent.mjs.map +1 -1
  128. package/dist/esm/createComponent.native.js +10 -8
  129. package/dist/esm/createComponent.native.js.map +1 -1
  130. package/dist/esm/eventHandling.mjs +1 -1
  131. package/dist/esm/eventHandling.mjs.map +1 -1
  132. package/dist/esm/eventHandling.native.js +12 -36
  133. package/dist/esm/eventHandling.native.js.map +1 -1
  134. package/dist/esm/helpers/getSplitStyles.mjs +5 -0
  135. package/dist/esm/helpers/getSplitStyles.mjs.map +1 -1
  136. package/dist/esm/helpers/getSplitStyles.native.js +9 -0
  137. package/dist/esm/helpers/getSplitStyles.native.js.map +1 -1
  138. package/dist/esm/helpers/mainThreadPressEvents.native.js +20 -3
  139. package/dist/esm/helpers/mainThreadPressEvents.native.js.map +1 -1
  140. package/dist/esm/helpers/pointerEvents.native.js +26 -20
  141. package/dist/esm/helpers/pointerEvents.native.js.map +1 -1
  142. package/dist/esm/helpers/propMapper.native.js +40 -15
  143. package/dist/esm/helpers/propMapper.native.js.map +1 -1
  144. package/dist/esm/hooks/useComponentState.mjs +26 -2
  145. package/dist/esm/hooks/useComponentState.mjs.map +1 -1
  146. package/dist/esm/hooks/useComponentState.native.js +26 -2
  147. package/dist/esm/hooks/useComponentState.native.js.map +1 -1
  148. package/dist/esm/hooks/useMedia.mjs +108 -43
  149. package/dist/esm/hooks/useMedia.mjs.map +1 -1
  150. package/dist/esm/hooks/useMedia.native.js +150 -61
  151. package/dist/esm/hooks/useMedia.native.js.map +1 -1
  152. package/dist/esm/hooks/useTheme.mjs +17 -4
  153. package/dist/esm/hooks/useTheme.mjs.map +1 -1
  154. package/dist/esm/hooks/useTheme.native.js +18 -4
  155. package/dist/esm/hooks/useTheme.native.js.map +1 -1
  156. package/dist/esm/hooks/useThemeState.mjs +133 -68
  157. package/dist/esm/hooks/useThemeState.mjs.map +1 -1
  158. package/dist/esm/hooks/useThemeState.native.js +145 -71
  159. package/dist/esm/hooks/useThemeState.native.js.map +1 -1
  160. package/dist/esm/index.js +3 -3
  161. package/dist/esm/index.js.map +1 -1
  162. package/dist/esm/index.mjs +3 -3
  163. package/dist/esm/index.mjs.map +1 -1
  164. package/dist/esm/index.native.js +3 -3
  165. package/dist/esm/index.native.js.map +1 -1
  166. package/dist/esm/views/Theme.mjs +1 -1
  167. package/dist/esm/views/Theme.mjs.map +1 -1
  168. package/dist/esm/views/Theme.native.js +1 -1
  169. package/dist/esm/views/Theme.native.js.map +1 -1
  170. package/package.json +13 -13
  171. package/src/createComponent.tsx +18 -7
  172. package/src/eventHandling.native.ts +28 -40
  173. package/src/eventHandling.ts +2 -1
  174. package/src/helpers/getSplitStyles.tsx +33 -0
  175. package/src/helpers/mainThreadPressEvents.native.ts +25 -3
  176. package/src/helpers/pointerEvents.native.ts +26 -19
  177. package/src/helpers/propMapper.ts +23 -0
  178. package/src/hooks/useComponentState.ts +39 -2
  179. package/src/hooks/useMedia.tsx +147 -42
  180. package/src/hooks/useTheme.tsx +27 -5
  181. package/src/hooks/useThemeState.ts +263 -137
  182. package/src/index.ts +7 -1
  183. package/src/types.tsx +53 -5
  184. package/src/views/Theme.tsx +4 -1
  185. package/types/createComponent.d.ts.map +1 -1
  186. package/types/eventHandling.d.ts +1 -1
  187. package/types/eventHandling.d.ts.map +1 -1
  188. package/types/eventHandling.native.d.ts +2 -2
  189. package/types/eventHandling.native.d.ts.map +1 -1
  190. package/types/helpers/getSplitStyles.d.ts.map +1 -1
  191. package/types/helpers/mainThreadPressEvents.native.d.ts.map +1 -1
  192. package/types/helpers/pointerEvents.native.d.ts.map +1 -1
  193. package/types/helpers/propMapper.d.ts.map +1 -1
  194. package/types/hooks/useComponentState.d.ts +1 -1
  195. package/types/hooks/useComponentState.d.ts.map +1 -1
  196. package/types/hooks/useMedia.d.ts.map +1 -1
  197. package/types/hooks/useTheme.d.ts +1 -1
  198. package/types/hooks/useTheme.d.ts.map +1 -1
  199. package/types/hooks/useThemeState.d.ts +1 -1
  200. package/types/hooks/useThemeState.d.ts.map +1 -1
  201. package/types/index.d.ts +2 -2
  202. package/types/index.d.ts.map +1 -1
  203. package/types/types.d.ts +3 -1
  204. package/types/types.d.ts.map +1 -1
  205. package/types/views/Theme.d.ts.map +1 -1
@@ -27,7 +27,8 @@ export function wrapWithGestureDetector(
27
27
  _gesture: any,
28
28
  _stateRef: { current: any },
29
29
  _isHOC?: boolean,
30
- _isCompositeComponent?: boolean
30
+ _isCompositeComponent?: boolean,
31
+ _hasRealPressEvents?: boolean
31
32
  ) {
32
33
  return content
33
34
  }
@@ -153,6 +153,15 @@ function isValidStyleKey(
153
153
  return key in validStyles ? true : accept && key in accept
154
154
  }
155
155
 
156
+ function shouldSkipNativeHoverProp(key: string, isMedia: false | boolean | string) {
157
+ if (process.env.TAMAGUI_TARGET !== 'native') return false
158
+ if (key === 'hoverStyle') return true
159
+ if (isMedia === 'group') {
160
+ return getGroupPropParts(key.slice(1)).pseudo === 'hover'
161
+ }
162
+ return false
163
+ }
164
+
156
165
  export const getSplitStyles: StyleSplitter = (
157
166
  props,
158
167
  staticConfig,
@@ -305,6 +314,18 @@ export const getSplitStyles: StyleSplitter = (
305
314
  continue
306
315
  }
307
316
 
317
+ // native: data-* attributes never become native props (they're stripped
318
+ // further down anyway), and the compiler-emitted data-disable-theme/-media
319
+ // flags are already consumed in createComponent. skip them before any per-prop
320
+ // work so they don't pay the isValidStyleKey + handling cost on the hot path.
321
+ if (
322
+ process.env.TAMAGUI_TARGET === 'native' &&
323
+ keyInit[0] === 'd' &&
324
+ keyInit.startsWith('data-')
325
+ ) {
326
+ continue
327
+ }
328
+
308
329
  if (
309
330
  process.env.NODE_ENV === 'development' &&
310
331
  (debug === 'profile' || (globalThis as any).time)
@@ -481,6 +502,10 @@ export const getSplitStyles: StyleSplitter = (
481
502
 
482
503
  const isStyleProp = isValidStyleKeyInit || isMediaOrPseudo || (isVariant && !noExpand)
483
504
 
505
+ if (shouldSkipNativeHoverProp(keyInit, isMedia)) {
506
+ continue
507
+ }
508
+
484
509
  if (isStyleProp && (asChild === 'except-style' || asChild === 'except-style-web')) {
485
510
  continue
486
511
  }
@@ -625,6 +650,10 @@ export const getSplitStyles: StyleSplitter = (
625
650
  key = normalizeGroupKey(key, groupContext)
626
651
  }
627
652
 
653
+ if (shouldSkipNativeHoverProp(key, isMedia)) {
654
+ return
655
+ }
656
+
628
657
  if (
629
658
  inlineProps?.has(key) ||
630
659
  (process.env.IS_STATIC === 'is_static' && inlineWhenUnflattened?.has(key))
@@ -948,6 +977,10 @@ export const getSplitStyles: StyleSplitter = (
948
977
  const groupPseudoKey = groupInfo.pseudo
949
978
  const groupMediaKey = groupInfo.media
950
979
 
980
+ if (process.env.TAMAGUI_TARGET === 'native' && groupPseudoKey === 'hover') {
981
+ return
982
+ }
983
+
951
984
  if (!groupState) {
952
985
  if (process.env.NODE_ENV === 'development' && debug) {
953
986
  log(`No parent with group prop, skipping styles: ${groupName}`)
@@ -6,6 +6,7 @@
6
6
  * long press, cancellation, and min press duration.
7
7
  */
8
8
 
9
+ import { unstable_hasExternalPressOwnership } from '@tamagui/native'
9
10
  import { useRef } from 'react'
10
11
 
11
12
  type PressState =
@@ -20,6 +21,7 @@ interface PressRef {
20
21
  pressOutTimer: ReturnType<typeof setTimeout> | null
21
22
  longPressTimer: ReturnType<typeof setTimeout> | null
22
23
  activateTime: number
24
+ blockedByExternalOwnership: boolean
23
25
  }
24
26
 
25
27
  const DEFAULT_LONG_PRESS_DELAY = 500
@@ -39,6 +41,7 @@ export function useMainThreadPressEvents(
39
41
  pressOutTimer: null,
40
42
  longPressTimer: null,
41
43
  activateTime: 0,
44
+ blockedByExternalOwnership: false,
42
45
  }
43
46
  }
44
47
 
@@ -92,12 +95,22 @@ export function useMainThreadPressEvents(
92
95
  const userTerminationRequest = viewProps.onResponderTerminationRequest
93
96
  const userMove = viewProps.onResponderMove
94
97
 
95
- viewProps.onStartShouldSetResponder = (e: any) =>
96
- Boolean(userStartShouldSet?.(e)) || !events.disabled
98
+ viewProps.onStartShouldSetResponder = (e: any) => {
99
+ if (userStartShouldSet?.(e)) return true
100
+ return !events.disabled && !unstable_hasExternalPressOwnership()
101
+ }
97
102
 
98
103
  viewProps.onResponderGrant = (e: any) => {
99
- userGrant?.(e)
100
104
  cleanup()
105
+
106
+ if (unstable_hasExternalPressOwnership()) {
107
+ ref.current.state = 'idle'
108
+ ref.current.blockedByExternalOwnership = true
109
+ return
110
+ }
111
+
112
+ userGrant?.(e)
113
+ ref.current.blockedByExternalOwnership = false
101
114
  ref.current.state = 'pressing'
102
115
 
103
116
  if (delayPressIn > 0) {
@@ -117,6 +130,13 @@ export function useMainThreadPressEvents(
117
130
  }
118
131
 
119
132
  viewProps.onResponderRelease = (e: any) => {
133
+ if (ref.current.blockedByExternalOwnership || unstable_hasExternalPressOwnership()) {
134
+ cleanup()
135
+ ref.current.blockedByExternalOwnership = false
136
+ ref.current.state = 'idle'
137
+ return
138
+ }
139
+
120
140
  userRelease?.(e)
121
141
  const wasLongPressed = ref.current.state === 'longPressed'
122
142
  cleanup()
@@ -132,6 +152,7 @@ export function useMainThreadPressEvents(
132
152
 
133
153
  deactivate(e)
134
154
  ref.current.state = 'idle'
155
+ ref.current.blockedByExternalOwnership = false
135
156
  }
136
157
 
137
158
  viewProps.onResponderTerminate = (e: any) => {
@@ -141,6 +162,7 @@ export function useMainThreadPressEvents(
141
162
  deactivate(e)
142
163
  }
143
164
  ref.current.state = 'idle'
165
+ ref.current.blockedByExternalOwnership = false
144
166
  }
145
167
 
146
168
  viewProps.onResponderTerminationRequest = (e: any) => {
@@ -25,13 +25,20 @@ export function usePointerEvents(props: any, viewProps: any) {
25
25
  onPointerEnter ||
26
26
  onPointerLeave
27
27
 
28
- // track if pointer is currently inside bounds (for enter/leave)
29
- const isInsideRef = useRef(false)
30
- const layoutRef = useRef({ width: 0, height: 0 })
31
- // track if pointer is captured (for move events outside bounds)
32
- const isCapturedRef = useRef(false)
28
+ // single ref-bag (was three separate useRef slots) one hook slot per
29
+ // component instead of three. isInside = pointer within bounds (enter/leave),
30
+ // layout = measured bounds, isCaptured = setPointerCapture state for moves.
31
+ const ref = useRef<{
32
+ isInside: boolean
33
+ layout: { width: number; height: number }
34
+ isCaptured: boolean
35
+ } | null>(null)
36
+ if (!ref.current) {
37
+ ref.current = { isInside: false, layout: { width: 0, height: 0 }, isCaptured: false }
38
+ }
33
39
 
34
40
  if (!hasPointerEvents) return
41
+ const bag = ref.current
35
42
 
36
43
  // create normalized event with setPointerCapture support
37
44
  const createNormalizedEvent = (e: any) => {
@@ -49,10 +56,10 @@ export function usePointerEvents(props: any, viewProps: any) {
49
56
  nativeEvent: touch,
50
57
  target: {
51
58
  setPointerCapture: (_pointerId: number) => {
52
- isCapturedRef.current = true
59
+ bag.isCaptured = true
53
60
  },
54
61
  releasePointerCapture: (_pointerId: number) => {
55
- isCapturedRef.current = false
62
+ bag.isCaptured = false
56
63
  },
57
64
  },
58
65
  }
@@ -69,7 +76,7 @@ export function usePointerEvents(props: any, viewProps: any) {
69
76
  // pointer up
70
77
  if (onPointerUp) {
71
78
  viewProps.onTouchEnd = composeEventHandlers(viewProps.onTouchEnd, (e: any) => {
72
- isCapturedRef.current = false // auto-release on up
79
+ bag.isCaptured = false // auto-release on up
73
80
  onPointerUp(createNormalizedEvent(e))
74
81
  })
75
82
  }
@@ -78,12 +85,12 @@ export function usePointerEvents(props: any, viewProps: any) {
78
85
  if (onPointerMove) {
79
86
  viewProps.onTouchMove = composeEventHandlers(viewProps.onTouchMove, (e: any) => {
80
87
  const { locationX, locationY } = e.nativeEvent
81
- const { width, height } = layoutRef.current
88
+ const { width, height } = bag.layout
82
89
  const isInBounds =
83
90
  locationX >= 0 && locationX <= width && locationY >= 0 && locationY <= height
84
91
 
85
92
  // fire if captured OR in bounds (matches web behavior)
86
- if (isCapturedRef.current || isInBounds) {
93
+ if (bag.isCaptured || isInBounds) {
87
94
  onPointerMove(createNormalizedEvent(e))
88
95
  }
89
96
  })
@@ -92,7 +99,7 @@ export function usePointerEvents(props: any, viewProps: any) {
92
99
  // pointer cancel
93
100
  if (onPointerCancel) {
94
101
  viewProps.onTouchCancel = composeEventHandlers(viewProps.onTouchCancel, (e: any) => {
95
- isCapturedRef.current = false
102
+ bag.isCaptured = false
96
103
  onPointerCancel(createNormalizedEvent(e))
97
104
  })
98
105
  }
@@ -101,7 +108,7 @@ export function usePointerEvents(props: any, viewProps: any) {
101
108
  if (onPointerEnter || onPointerLeave || onPointerMove) {
102
109
  // track layout for bounds checking
103
110
  viewProps.onLayout = composeEventHandlers(viewProps.onLayout, (e: any) => {
104
- layoutRef.current = {
111
+ bag.layout = {
105
112
  width: e.nativeEvent.layout.width,
106
113
  height: e.nativeEvent.layout.height,
107
114
  }
@@ -112,9 +119,9 @@ export function usePointerEvents(props: any, viewProps: any) {
112
119
  if (onPointerEnter) {
113
120
  viewProps.onTouchStart = composeEventHandlers(viewProps.onTouchStart, (e: any) => {
114
121
  const { locationX, locationY } = e.nativeEvent
115
- const { width, height } = layoutRef.current
122
+ const { width, height } = bag.layout
116
123
  if (locationX >= 0 && locationX <= width && locationY >= 0 && locationY <= height) {
117
- isInsideRef.current = true
124
+ bag.isInside = true
118
125
  onPointerEnter(createNormalizedEvent(e))
119
126
  }
120
127
  })
@@ -124,18 +131,18 @@ export function usePointerEvents(props: any, viewProps: any) {
124
131
  if (onPointerLeave) {
125
132
  viewProps.onTouchMove = composeEventHandlers(viewProps.onTouchMove, (e: any) => {
126
133
  const { locationX, locationY } = e.nativeEvent
127
- const { width, height } = layoutRef.current
134
+ const { width, height } = bag.layout
128
135
  const isInside =
129
136
  locationX >= 0 && locationX <= width && locationY >= 0 && locationY <= height
130
- if (isInsideRef.current && !isInside) {
131
- isInsideRef.current = false
137
+ if (bag.isInside && !isInside) {
138
+ bag.isInside = false
132
139
  onPointerLeave(createNormalizedEvent(e))
133
140
  }
134
141
  })
135
142
 
136
143
  viewProps.onTouchEnd = composeEventHandlers(viewProps.onTouchEnd, (e: any) => {
137
- if (isInsideRef.current) {
138
- isInsideRef.current = false
144
+ if (bag.isInside) {
145
+ bag.isInside = false
139
146
  onPointerLeave(createNormalizedEvent(e))
140
147
  }
141
148
  })
@@ -43,6 +43,29 @@ export const propMapper: PropMapper = (key, value, styleState, disabled, map) =>
43
43
  const { conf, styleProps, staticConfig } = styleState
44
44
  const { variants } = staticConfig
45
45
 
46
+ // "unset" is a CSS-wide keyword: valid CSS on web, but React Native
47
+ // style props reject it (e.g. aspectRatio throws "must be a number, a
48
+ // ratio string or `auto`"). On native, clear anything an earlier prop or
49
+ // styled default already merged for this key — matching web, where unset
50
+ // resets toward initial — then drop the value so RN never sees it.
51
+ if (process.env.TAMAGUI_TARGET === 'native' && value === 'unset') {
52
+ const expandedKey =
53
+ (!styleProps.disableExpandShorthands && conf.shorthands[key]) || key
54
+ const expanded = styleProps.noExpand
55
+ ? null
56
+ : expandStyle(expandedKey, value, conf.settings.styleCompat || 'web')
57
+ if (styleState.style) {
58
+ if (expanded) {
59
+ for (const [nkey] of expanded) {
60
+ delete styleState.style[nkey]
61
+ }
62
+ } else {
63
+ delete styleState.style[expandedKey]
64
+ }
65
+ }
66
+ return
67
+ }
68
+
46
69
  if (!styleProps.noExpand) {
47
70
  if (variants && key in variants) {
48
71
  const variantValue = resolveVariants(key, value, styleProps, styleState, '')
@@ -1,5 +1,5 @@
1
1
  import { getPlatformDriver, isServer, isWeb } from '@tamagui/constants'
2
- import { useCreateShallowSetState } from '@tamagui/is-equal-shallow'
2
+ import { mergeIfNotShallowEqual } from '@tamagui/is-equal-shallow'
3
3
  import { useDidFinishSSR, useIsClientOnly } from '@tamagui/use-did-finish-ssr'
4
4
  import { useRef, useState } from 'react'
5
5
  import { getSetting } from '../config'
@@ -176,7 +176,44 @@ export const useComponentState = (
176
176
 
177
177
  const groupName = props.group as any as string | undefined
178
178
 
179
- const setStateShallow = useCreateShallowSetState(setState, props.debug)
179
+ // hoisted shallow-set closure: created once per component instance and
180
+ // reused every render. drops the useCallback hook that useCreateShallowSetState
181
+ // would otherwise add. setState from useState is stable per instance so we
182
+ // can safely capture it. debug is read off stateRef at call time.
183
+ //
184
+ // important: this lives on `baseSetStateShallow`, not `setStateShallow`.
185
+ // createComponent's avoidReRenders path overwrites `stateRef.current.setStateShallow`
186
+ // with an emitter wrapper and captures this base as its real-re-render escape hatch.
187
+ // if the base shared the `setStateShallow` field, on the 2nd+ render this hook would
188
+ // read back the wrapper, the wrapper's escape hatch would point at itself, and a real
189
+ // re-render (e.g. unmounted 'should-enter' -> false) would never reach React, leaving
190
+ // enter animations stuck at opacity 0.
191
+ if (!stateRef.current.baseSetStateShallow) {
192
+ const r = stateRef.current
193
+ r.baseSetStateShallow = (stateOrGetState: any) => {
194
+ setState((prev: any) => {
195
+ const next =
196
+ typeof stateOrGetState === 'function' ? stateOrGetState(prev) : stateOrGetState
197
+ const update = mergeIfNotShallowEqual(prev, next)
198
+ if (process.env.NODE_ENV === 'development') {
199
+ const dbg = (r as any).__debug
200
+ if (dbg && update !== prev) {
201
+ console.groupCollapsed(`setStateShallow CHANGE`, '=>', update)
202
+ console.info(`previously`, prev)
203
+ console.trace()
204
+ console.groupEnd()
205
+ }
206
+ }
207
+ return update
208
+ })
209
+ }
210
+ }
211
+ if (process.env.NODE_ENV === 'development') {
212
+ ;(stateRef.current as any).__debug = props.debug
213
+ }
214
+ const setStateShallow = stateRef.current.baseSetStateShallow!
215
+ if (process.env.NODE_ENV === 'development' && globalThis.time)
216
+ globalThis.time`state-useCreateShallowSetState`
180
217
 
181
218
  // set enter/exit variants onto our new props object
182
219
  if (presenceState && isAnimated && isHydrated && staticConfig.variants) {
@@ -1,5 +1,5 @@
1
1
  import { isServer, isWeb } from '@tamagui/constants'
2
- import { useRef, useSyncExternalStore } from 'react'
2
+ import { useEffect, useReducer, useRef } from 'react'
3
3
  import { getSetting } from '../config'
4
4
  import { resetMediaStyleCache } from '../helpers/createMediaStyle'
5
5
  import { matchMedia } from '../helpers/matchMedia'
@@ -66,6 +66,8 @@ export const configureMedia = (config: TamaguiInternalConfig) => {
66
66
  mediaVersion++
67
67
  // reset cached media style prefixes/selectors so they get recalculated with new key order
68
68
  resetMediaStyleCache()
69
+ // touch-tracker getter object depends on the current media key set
70
+ resetMediaTouchTracker()
69
71
  for (const key in media) {
70
72
  getMedia()[key] = mediaQueryDefaultActive?.[key] || false
71
73
  mediaKeys.add(`$${key}`)
@@ -136,6 +138,47 @@ type MediaState = {
136
138
 
137
139
  const States = new WeakMap<any, MediaState>()
138
140
 
141
+ // shared "touch tracker" prototype: one object whose enumerable getter
142
+ // properties are pre-defined for every configured media key. Hermes inlines
143
+ // getter calls; the old `new Proxy(state, { get })` path forced an interpreted
144
+ // trap on every access — the dominant per-component cost in benchmarks. Each
145
+ // component owns just an Object.create(proto) with a Symbol-keyed slot
146
+ // pointing at its tracking set + current snapshot.
147
+ type MediaRefSlot = {
148
+ proxyTarget: MediaQueryState
149
+ keys: Set<string>
150
+ }
151
+ let touchTrackerProto: object | null = null
152
+ const refSlot = Symbol('mediaRefSlot')
153
+
154
+ function buildTouchTrackerProto(): object {
155
+ const proto: PropertyDescriptorMap = {}
156
+ for (const fullKey of mediaKeys) {
157
+ const key = fullKey[0] === '$' ? fullKey.slice(1) : fullKey
158
+ proto[key] = {
159
+ enumerable: true,
160
+ configurable: true,
161
+ get(this: { [refSlot]: MediaRefSlot }) {
162
+ const slot = this[refSlot]
163
+ if (!disableMediaTouch) {
164
+ slot.keys.add(key)
165
+ }
166
+ return slot.proxyTarget[key]
167
+ },
168
+ }
169
+ }
170
+ return Object.create(null, proto)
171
+ }
172
+
173
+ function getTouchTrackerProto(): object {
174
+ if (!touchTrackerProto) touchTrackerProto = buildTouchTrackerProto()
175
+ return touchTrackerProto
176
+ }
177
+
178
+ function resetMediaTouchTracker() {
179
+ touchTrackerProto = null
180
+ }
181
+
139
182
  export function setMediaShouldUpdate(
140
183
  ref: any,
141
184
  enabled?: boolean,
@@ -165,38 +208,46 @@ export function useMedia(
165
208
  ): UseMediaState {
166
209
  'use no memo'
167
210
 
168
- const componentState = componentContext ? States.get(componentContext) : null
169
-
170
- const internalRef = useRef<{
211
+ type MediaRef = {
171
212
  keys: Set<string>
172
213
  lastState: MediaQueryState
173
214
  pendingState?: MediaQueryState
174
- }>(null)
175
- if (!internalRef.current) {
176
- internalRef.current = {
177
- keys: new Set(),
178
- lastState: getMedia(),
179
- }
180
- }
181
-
182
- // reset on next render
183
- if (internalRef.current.pendingState) {
184
- internalRef.current.lastState = internalRef.current.pendingState
185
- internalRef.current.pendingState = undefined
186
- }
187
-
188
- const { keys } = internalRef.current
189
-
190
- // clear each render to track only rendered touched keys
191
- if (keys.size) {
192
- keys.clear()
215
+ renderVersion: number
216
+ unsubscribe?: () => void
217
+ // stable per-component closures + reusable Proxy. allocating new ones each
218
+ // render (via useSyncExternalStore + `new Proxy(state, ...)`) was a real
219
+ // per-component-per-render cost; we hold one Proxy whose target is swapped
220
+ // by mutating `proxyTarget` and re-reading it in the get trap.
221
+ proxyTarget: MediaQueryState
222
+ proxy: UseMediaState
223
+ getSnapshot: () => MediaQueryState
224
+ componentContext?: ComponentContextI
225
+ debug?: DebugProp
193
226
  }
194
227
 
195
- const state = useSyncExternalStore(
196
- subscribe,
197
- () => {
198
- const curKeys = componentState?.keys || keys
199
- const { lastState, pendingState } = internalRef.current!
228
+ const internalRef = useRef<MediaRef | null>(null)
229
+ if (!internalRef.current) {
230
+ const initial = getMedia()
231
+ const r: MediaRef = {
232
+ keys: new Set<string>(),
233
+ lastState: initial,
234
+ renderVersion: 0,
235
+ proxyTarget: initial,
236
+ proxy: undefined as unknown as UseMediaState,
237
+ getSnapshot: undefined as unknown as () => MediaQueryState,
238
+ componentContext,
239
+ debug,
240
+ }
241
+ // proxy → Object.create(getterProto) with a Symbol slot. Per-key get is a
242
+ // monomorphic getter call (Hermes-fast) instead of a Proxy trap.
243
+ const tracker = Object.create(getTouchTrackerProto())
244
+ tracker[refSlot] = { proxyTarget: initial, keys: r.keys } as MediaRefSlot
245
+ r.proxy = tracker as UseMediaState
246
+ r.getSnapshot = () => {
247
+ const curKeys = r.componentContext
248
+ ? States.get(r.componentContext)?.keys || r.keys
249
+ : r.keys
250
+ const { lastState, pendingState } = r
200
251
 
201
252
  if (!curKeys.size) {
202
253
  return lastState
@@ -205,38 +256,92 @@ export function useMedia(
205
256
  const ms = getMedia()
206
257
  for (const key of curKeys) {
207
258
  if (ms[key] !== (pendingState || lastState)[key]) {
208
- if (process.env.NODE_ENV === 'development' && debug) {
259
+ if (process.env.NODE_ENV === 'development' && r.debug) {
209
260
  console.warn(`useMedia() ✍️`, key, lastState[key], '=>', ms[key])
210
261
  }
211
262
 
212
263
  // in emitter mode (no-rerender) avoid changing state, instead emit
213
- if (componentContext?.mediaEmit) {
214
- componentContext.mediaEmit(ms)
215
- internalRef.current!.pendingState = ms
264
+ if (r.componentContext?.mediaEmit) {
265
+ r.componentContext.mediaEmit(ms)
266
+ r.pendingState = ms
216
267
  return lastState
217
268
  }
218
269
 
219
- internalRef.current!.lastState = ms
270
+ r.lastState = ms
220
271
 
221
272
  return ms
222
273
  }
223
274
  }
224
275
 
225
276
  return lastState
226
- },
227
- getServerSnapshot
228
- )
277
+ }
278
+ internalRef.current = r
279
+ } else {
280
+ // refresh per-render inputs the closures read through the ref
281
+ internalRef.current.componentContext = componentContext
282
+ internalRef.current.debug = debug
283
+ }
229
284
 
230
- return new Proxy(state, {
231
- get(_, key) {
232
- if (!disableMediaTouch && typeof key === 'string') {
233
- keys.add(key)
285
+ const ref = internalRef.current
286
+ ref.renderVersion++
287
+
288
+ // reset on next render
289
+ if (ref.pendingState) {
290
+ ref.lastState = ref.pendingState
291
+ ref.pendingState = undefined
292
+ }
293
+
294
+ // clear each render to track only rendered touched keys
295
+ if (ref.keys.size) {
296
+ ref.keys.clear()
297
+ }
298
+
299
+ // manual subscription (same shape as useThemeStateSubscribed): same
300
+ // granular bailout via getSnapshot returning the same MediaQueryState ref
301
+ // when none of the component's touched keys changed, but fewer
302
+ // React-internal hook slots on Hermes than useSyncExternalStore.
303
+ const [, forceUpdate] = useReducer(incReducer, 0)
304
+ const state = isServer ? initState : ref.getSnapshot()
305
+ ref.proxyTarget = state
306
+ ;(ref.proxy as any)[refSlot].proxyTarget = state
307
+
308
+ useEffect(() => {
309
+ const renderVersion = ref.renderVersion
310
+ const shouldSubscribe =
311
+ !ref.componentContext || !!States.get(ref.componentContext)?.enabled
312
+
313
+ if (shouldSubscribe) {
314
+ if (!ref.unsubscribe) {
315
+ ref.unsubscribe = subscribe(() => {
316
+ const next = ref.getSnapshot()
317
+ if (next !== ref.proxyTarget) {
318
+ ref.proxyTarget = next
319
+ ;(ref.proxy as any)[refSlot].proxyTarget = next
320
+ forceUpdate()
321
+ }
322
+ })
234
323
  }
235
- return Reflect.get(state, key)
236
- },
324
+ } else if (ref.unsubscribe) {
325
+ ref.unsubscribe()
326
+ ref.unsubscribe = undefined
327
+ }
328
+
329
+ return () => {
330
+ // react runs passive cleanup before the next effect as well as on unmount.
331
+ // a newer render bumps renderVersion before that cleanup, so equality here
332
+ // means this is the final unmount cleanup.
333
+ if (ref.renderVersion === renderVersion) {
334
+ ref.unsubscribe?.()
335
+ ref.unsubscribe = undefined
336
+ }
337
+ }
237
338
  })
339
+
340
+ return ref.proxy
238
341
  }
239
342
 
343
+ const incReducer = (c: number): number => c + 1
344
+
240
345
  const getServerSnapshot = () => initState
241
346
 
242
347
  let disableMediaTouch = false
@@ -1,10 +1,15 @@
1
- import { useRef } from 'react'
1
+ import { useRef, type MutableRefObject } from 'react'
2
2
  import type { ThemeParsed, ThemeState, UseThemeWithStateProps } from '../types'
3
3
  import { getThemeProxied, type ThemeProxied } from './getThemeProxied'
4
4
  import { useThemeState } from './useThemeState'
5
5
 
6
6
  const EMPTY = {}
7
7
 
8
+ type KeysBag = {
9
+ keys: MutableRefObject<Set<string> | null>
10
+ schemeKeys: MutableRefObject<Set<string> | null>
11
+ }
12
+
8
13
  export const useTheme = () => {
9
14
  'use no memo'
10
15
 
@@ -20,13 +25,30 @@ export type ThemeWithState = [ThemeParsed, ThemeState]
20
25
  */
21
26
  export const useThemeWithState = (
22
27
  props: UseThemeWithStateProps,
23
- isRoot = false
28
+ isRoot = false,
29
+ // <Theme> components push their themeState.id into ThemeStateContext,
30
+ // so their descendants subscribe under this id; <Theme> needs to schedule
31
+ // descendant updates when its propsKey changes. Leaf styled components
32
+ // (createComponent, useProps, useTheme, animation hooks) do NOT push a
33
+ // context — listenersByParent[componentId] is always empty — so the
34
+ // schedule-update effect is a no-op for them and can be skipped to save
35
+ // one hook slot per styled component on mount. Theme.tsx passes true.
36
+ forThemeView = false
24
37
  ): ThemeWithState => {
25
38
  'use no memo'
26
39
 
27
- const keys = useRef<Set<string> | null>(null)
28
- const schemeKeys = useRef<Set<string> | null>(null)
29
- const themeState = useThemeState(props, isRoot, keys, schemeKeys)
40
+ // single useRef holding both keys + schemeKeys; saves one hook slot per
41
+ // component vs separate useRef calls. these still look like MutableRefObject
42
+ // to getThemeProxied which only reads/writes `.current`.
43
+ const bag = useRef<KeysBag | null>(null)
44
+ if (!bag.current) {
45
+ bag.current = {
46
+ keys: { current: null },
47
+ schemeKeys: { current: null },
48
+ }
49
+ }
50
+ const { keys, schemeKeys } = bag.current
51
+ const themeState = useThemeState(props, isRoot, keys, schemeKeys, forThemeView)
30
52
 
31
53
  if (process.env.NODE_ENV === 'development') {
32
54
  if (!props.passThrough && !themeState?.theme) {