@oxyhq/bloom 0.1.7 → 0.1.9

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 (45) hide show
  1. package/lib/commonjs/button/Button.js +53 -19
  2. package/lib/commonjs/button/Button.js.map +1 -1
  3. package/lib/commonjs/collapsible/Collapsible.js +42 -3
  4. package/lib/commonjs/collapsible/Collapsible.js.map +1 -1
  5. package/lib/commonjs/loading/Loading.js +2 -1
  6. package/lib/commonjs/loading/Loading.js.map +1 -1
  7. package/lib/commonjs/skeleton/index.js +31 -8
  8. package/lib/commonjs/skeleton/index.js.map +1 -1
  9. package/lib/commonjs/styles/tokens.js +31 -1
  10. package/lib/commonjs/styles/tokens.js.map +1 -1
  11. package/lib/commonjs/switch/Switch.js +10 -7
  12. package/lib/commonjs/switch/Switch.js.map +1 -1
  13. package/lib/module/button/Button.js +55 -21
  14. package/lib/module/button/Button.js.map +1 -1
  15. package/lib/module/collapsible/Collapsible.js +44 -5
  16. package/lib/module/collapsible/Collapsible.js.map +1 -1
  17. package/lib/module/loading/Loading.js +2 -1
  18. package/lib/module/loading/Loading.js.map +1 -1
  19. package/lib/module/skeleton/index.js +31 -8
  20. package/lib/module/skeleton/index.js.map +1 -1
  21. package/lib/module/styles/tokens.js +30 -0
  22. package/lib/module/styles/tokens.js.map +1 -1
  23. package/lib/module/switch/Switch.js +10 -7
  24. package/lib/module/switch/Switch.js.map +1 -1
  25. package/lib/typescript/commonjs/button/Button.d.ts.map +1 -1
  26. package/lib/typescript/commonjs/collapsible/Collapsible.d.ts.map +1 -1
  27. package/lib/typescript/commonjs/loading/Loading.d.ts.map +1 -1
  28. package/lib/typescript/commonjs/skeleton/index.d.ts.map +1 -1
  29. package/lib/typescript/commonjs/styles/tokens.d.ts +29 -0
  30. package/lib/typescript/commonjs/styles/tokens.d.ts.map +1 -1
  31. package/lib/typescript/commonjs/switch/Switch.d.ts.map +1 -1
  32. package/lib/typescript/module/button/Button.d.ts.map +1 -1
  33. package/lib/typescript/module/collapsible/Collapsible.d.ts.map +1 -1
  34. package/lib/typescript/module/loading/Loading.d.ts.map +1 -1
  35. package/lib/typescript/module/skeleton/index.d.ts.map +1 -1
  36. package/lib/typescript/module/styles/tokens.d.ts +29 -0
  37. package/lib/typescript/module/styles/tokens.d.ts.map +1 -1
  38. package/lib/typescript/module/switch/Switch.d.ts.map +1 -1
  39. package/package.json +2 -2
  40. package/src/button/Button.tsx +53 -20
  41. package/src/collapsible/Collapsible.tsx +40 -6
  42. package/src/loading/Loading.tsx +2 -1
  43. package/src/skeleton/index.tsx +42 -12
  44. package/src/styles/tokens.ts +21 -0
  45. package/src/switch/Switch.tsx +5 -7
@@ -1 +1 @@
1
- {"version":3,"file":"Collapsible.d.ts","sourceRoot":"","sources":["../../../../src/collapsible/Collapsible.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAyB,MAAM,OAAO,CAAC;AAI9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAwDhD,eAAO,MAAM,WAAW,8CAA6B,CAAC"}
1
+ {"version":3,"file":"Collapsible.d.ts","sourceRoot":"","sources":["../../../../src/collapsible/Collapsible.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAyD,MAAM,OAAO,CAAC;AAK9E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAyFhD,eAAO,MAAM,WAAW,8CAA6B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"Loading.d.ts","sourceRoot":"","sources":["../../../../src/loading/Loading.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmC,MAAM,OAAO,CAAC;AAKxD,OAAO,KAAK,EACV,YAAY,EAKb,MAAM,SAAS,CAAC;AAmNjB,eAAO,MAAM,OAAO,0CAAyB,CAAC"}
1
+ {"version":3,"file":"Loading.d.ts","sourceRoot":"","sources":["../../../../src/loading/Loading.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmC,MAAM,OAAO,CAAC;AAMxD,OAAO,KAAK,EACV,YAAY,EAKb,MAAM,SAAS,CAAC;AAmNjB,eAAO,MAAM,OAAO,0CAAyB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/skeleton/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAQ,KAAK,SAAS,EAAE,KAAK,SAAS,EAAc,MAAM,cAAc,CAAC;AAQhF,wBAAgB,IAAI,CAAC,EACnB,KAAK,EACL,KAAK,GACN,EAAE;IACD,KAAK,CAAC,EAAE,SAAS,GAAG,SAAS,EAAE,CAAC;IAChC,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,2CAyBA;AAED,wBAAgB,MAAM,CAAC,EACrB,QAAQ,EACR,IAAI,EACJ,KAAK,EACL,KAAK,GACN,EAAE;IACD,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,SAAS,GAAG,SAAS,EAAE,CAAC;CACjC,2CAiBA;AAED,wBAAgB,IAAI,CAAC,EACnB,IAAI,EACJ,KAAK,EACL,KAAK,GACN,EAAE;IACD,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,SAAS,GAAG,SAAS,EAAE,CAAC;CACjC,2CAgBA;AAED,wBAAgB,GAAG,CAAC,EAClB,QAAQ,EACR,KAAK,GACN,EAAE;IACD,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,KAAK,CAAC,EAAE,SAAS,GAAG,SAAS,EAAE,CAAC;CACjC,2CAEA;AAED,wBAAgB,GAAG,CAAC,EAClB,QAAQ,EACR,KAAK,GACN,EAAE;IACD,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,KAAK,CAAC,EAAE,SAAS,GAAG,SAAS,EAAE,CAAC;CACjC,2CAEA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/skeleton/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4B,MAAM,OAAO,CAAC;AACjD,OAAO,EAAkB,KAAK,SAAS,EAAE,KAAK,SAAS,EAAc,MAAM,cAAc,CAAC;AAiC1F,wBAAgB,IAAI,CAAC,EACnB,KAAK,EACL,KAAK,GACN,EAAE;IACD,KAAK,CAAC,EAAE,SAAS,GAAG,SAAS,EAAE,CAAC;IAChC,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,2CA0BA;AAED,wBAAgB,MAAM,CAAC,EACrB,QAAQ,EACR,IAAI,EACJ,KAAK,EACL,KAAK,GACN,EAAE;IACD,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,SAAS,GAAG,SAAS,EAAE,CAAC;CACjC,2CAmBA;AAED,wBAAgB,IAAI,CAAC,EACnB,IAAI,EACJ,KAAK,EACL,KAAK,GACN,EAAE;IACD,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,SAAS,GAAG,SAAS,EAAE,CAAC;CACjC,2CAkBA;AAED,wBAAgB,GAAG,CAAC,EAClB,QAAQ,EACR,KAAK,GACN,EAAE;IACD,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,KAAK,CAAC,EAAE,SAAS,GAAG,SAAS,EAAE,CAAC;CACjC,2CAEA;AAED,wBAAgB,GAAG,CAAC,EAClB,QAAQ,EACR,KAAK,GACN,EAAE;IACD,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,KAAK,CAAC,EAAE,SAAS,GAAG,SAAS,EAAE,CAAC;CACjC,2CAEA"}
@@ -51,6 +51,35 @@ export declare const borderRadius: {
51
51
  readonly _2xl: 24;
52
52
  readonly full: 999;
53
53
  };
54
+ /**
55
+ * Gradient definitions for icon system.
56
+ */
57
+ /**
58
+ * Animation timing and spring tokens.
59
+ * Centralized values ensure consistent motion across all components.
60
+ */
61
+ export declare const animation: {
62
+ readonly duration: {
63
+ readonly instant: 100;
64
+ readonly fast: 150;
65
+ readonly normal: 200;
66
+ readonly slow: 300;
67
+ };
68
+ readonly spring: {
69
+ readonly snappy: {
70
+ readonly friction: 8;
71
+ readonly tension: 100;
72
+ };
73
+ readonly gentle: {
74
+ readonly friction: 8;
75
+ readonly tension: 60;
76
+ };
77
+ readonly bouncy: {
78
+ readonly friction: 6;
79
+ readonly tension: 120;
80
+ };
81
+ };
82
+ };
54
83
  /**
55
84
  * Gradient definitions for icon system.
56
85
  */
@@ -1 +1 @@
1
- {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../../../src/styles/tokens.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,eAAO,MAAM,KAAK;;;;;;;;;;;CAWR,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,QAAQ;;;;;;;;;;;CAWX,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,UAAU;;;;;CAKb,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,YAAY;;;;;;;;;CASf,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,SAAS;;yBAOb,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;;;;yBAOvB,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;;;CAGtB,CAAC"}
1
+ {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../../../src/styles/tokens.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,eAAO,MAAM,KAAK;;;;;;;;;;;CAWR,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,QAAQ;;;;;;;;;;;CAWX,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,UAAU;;;;;CAKb,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,YAAY;;;;;;;;;CASf,CAAC;AAEX;;GAEG;AACH;;;GAGG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;CAYZ,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,SAAS;;yBAOb,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;;;;yBAOvB,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;;;CAGtB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"Switch.d.ts","sourceRoot":"","sources":["../../../../src/switch/Switch.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAkC,MAAM,OAAO,CAAC;AAIvD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAgH3C,eAAO,MAAM,MAAM,4FAAwB,CAAC"}
1
+ {"version":3,"file":"Switch.d.ts","sourceRoot":"","sources":["../../../../src/switch/Switch.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAkC,MAAM,OAAO,CAAC;AAKvD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AA6G3C,eAAO,MAAM,MAAM,4FAAwB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/bloom",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Bloom UI — Oxy ecosystem component library for React Native + Expo + Web",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -46,7 +46,7 @@
46
46
  }
47
47
  },
48
48
  "./portal": {
49
- "react-native": "./src/portal/index.ts",
49
+ "react-native": "./src/portal/index.tsx",
50
50
  "import": {
51
51
  "types": "./lib/typescript/module/portal/index.d.ts",
52
52
  "default": "./lib/module/portal/index.js"
@@ -1,7 +1,8 @@
1
- import React, { useMemo, memo } from 'react';
2
- import { TouchableOpacity, Text, Platform, type ViewStyle, type TextStyle } from 'react-native';
1
+ import React, { useCallback, useMemo, useRef, memo } from 'react';
2
+ import { Pressable, Text, Platform, Animated, type ViewStyle, type TextStyle } from 'react-native';
3
3
 
4
4
  import { useTheme } from '../theme/use-theme';
5
+ import { animation } from '../styles/tokens';
5
6
  import type { ButtonProps } from './types';
6
7
 
7
8
  export type { ButtonProps, ButtonVariant, ButtonSize } from './types';
@@ -29,6 +30,9 @@ const SIZE_CONFIG = {
29
30
 
30
31
  const ICON_HIT_SLOP = { top: 10, bottom: 10, left: 10, right: 10 } as const;
31
32
 
33
+ const PRESS_SCALE = 0.97;
34
+ const SCALE_VARIANTS = new Set<string>(['primary', 'secondary']);
35
+
32
36
  const ButtonComponent: React.FC<ButtonProps> = ({
33
37
  onPress,
34
38
  children,
@@ -46,6 +50,26 @@ const ButtonComponent: React.FC<ButtonProps> = ({
46
50
  testID,
47
51
  }) => {
48
52
  const theme = useTheme();
53
+ const scaleAnim = useRef(new Animated.Value(1)).current;
54
+ const hasScaleFeedback = SCALE_VARIANTS.has(variant);
55
+
56
+ const onPressIn = useCallback(() => {
57
+ if (!hasScaleFeedback) return;
58
+ Animated.spring(scaleAnim, {
59
+ toValue: PRESS_SCALE,
60
+ useNativeDriver: true,
61
+ ...animation.spring.snappy,
62
+ }).start();
63
+ }, [scaleAnim, hasScaleFeedback]);
64
+
65
+ const onPressOut = useCallback(() => {
66
+ if (!hasScaleFeedback) return;
67
+ Animated.spring(scaleAnim, {
68
+ toValue: 1,
69
+ useNativeDriver: true,
70
+ ...animation.spring.gentle,
71
+ }).start();
72
+ }, [scaleAnim, hasScaleFeedback]);
49
73
 
50
74
  const baseStyles = useMemo((): ViewStyle => {
51
75
  const sizeConfig = SIZE_CONFIG[size];
@@ -120,26 +144,35 @@ const ButtonComponent: React.FC<ButtonProps> = ({
120
144
  }, [variant, size, theme]);
121
145
 
122
146
  const defaultHitSlop = variant === 'icon' ? ICON_HIT_SLOP : undefined;
147
+ const resolvedActiveOpacity = activeOpacity ?? (variant === 'icon' ? 0.7 : 0.8);
123
148
 
124
149
  return (
125
- <TouchableOpacity
126
- style={[baseStyles, disabled && { opacity: 0.5 }, style]}
127
- onPress={onPress}
128
- disabled={disabled}
129
- activeOpacity={activeOpacity ?? (variant === 'icon' ? 0.7 : 0.8)}
130
- hitSlop={hitSlop ?? defaultHitSlop}
131
- accessibilityLabel={accessibilityLabel}
132
- accessibilityHint={accessibilityHint}
133
- accessibilityRole="button"
134
- accessibilityState={{ disabled }}
135
- testID={testID}
136
- >
137
- {iconPosition === 'left' && icon}
138
- {children != null && (
139
- <Text style={[computedTextStyle, textStyle]}>{children}</Text>
140
- )}
141
- {iconPosition === 'right' && icon}
142
- </TouchableOpacity>
150
+ <Animated.View style={hasScaleFeedback ? { transform: [{ scale: scaleAnim }] } : undefined}>
151
+ <Pressable
152
+ style={({ pressed }) => [
153
+ baseStyles,
154
+ disabled && { opacity: 0.5 },
155
+ pressed && !hasScaleFeedback && { opacity: resolvedActiveOpacity },
156
+ style,
157
+ ]}
158
+ onPress={onPress}
159
+ onPressIn={onPressIn}
160
+ onPressOut={onPressOut}
161
+ disabled={disabled}
162
+ hitSlop={hitSlop ?? defaultHitSlop}
163
+ accessibilityLabel={accessibilityLabel}
164
+ accessibilityHint={accessibilityHint}
165
+ accessibilityRole="button"
166
+ accessibilityState={{ disabled }}
167
+ testID={testID}
168
+ >
169
+ {iconPosition === 'left' && icon}
170
+ {children != null && (
171
+ <Text style={[computedTextStyle, textStyle]}>{children}</Text>
172
+ )}
173
+ {iconPosition === 'right' && icon}
174
+ </Pressable>
175
+ </Animated.View>
143
176
  );
144
177
  };
145
178
 
@@ -1,9 +1,17 @@
1
- import React, { memo, useState } from 'react';
2
- import { View, Text, TouchableOpacity } from 'react-native';
1
+ import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
2
+ import { View, Text, TouchableOpacity, Animated, LayoutAnimation, Platform, UIManager } from 'react-native';
3
3
 
4
4
  import { useTheme } from '../theme/use-theme';
5
+ import { animation } from '../styles/tokens';
5
6
  import type { CollapsibleProps } from './types';
6
7
 
8
+ if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
9
+ UIManager.setLayoutAnimationEnabledExperimental(true);
10
+ }
11
+
12
+ const CHEVRON_CLOSED = '0deg';
13
+ const CHEVRON_OPEN = '90deg';
14
+
7
15
  const CollapsibleComponent: React.FC<CollapsibleProps> = ({
8
16
  title,
9
17
  children,
@@ -15,12 +23,38 @@ const CollapsibleComponent: React.FC<CollapsibleProps> = ({
15
23
  }) => {
16
24
  const [isOpen, setIsOpen] = useState(defaultOpen);
17
25
  const theme = useTheme();
26
+ const chevronAnim = useRef(new Animated.Value(defaultOpen ? 1 : 0)).current;
27
+
28
+ useEffect(() => {
29
+ Animated.spring(chevronAnim, {
30
+ toValue: isOpen ? 1 : 0,
31
+ useNativeDriver: true,
32
+ ...animation.spring.gentle,
33
+ }).start();
34
+ }, [isOpen, chevronAnim]);
35
+
36
+ const handleToggle = useCallback(() => {
37
+ LayoutAnimation.configureNext({
38
+ duration: animation.duration.normal,
39
+ update: { type: LayoutAnimation.Types.easeInEaseOut },
40
+ create: { type: LayoutAnimation.Types.easeInEaseOut, property: LayoutAnimation.Properties.opacity },
41
+ delete: { type: LayoutAnimation.Types.easeInEaseOut, property: LayoutAnimation.Properties.opacity },
42
+ });
43
+ setIsOpen((prev) => !prev);
44
+ }, []);
45
+
46
+ const chevronRotation = chevronAnim.interpolate({
47
+ inputRange: [0, 1],
48
+ outputRange: [CHEVRON_CLOSED, CHEVRON_OPEN],
49
+ });
18
50
 
19
51
  return (
20
52
  <View style={style} testID={testID}>
21
53
  <TouchableOpacity
22
- onPress={() => setIsOpen((prev) => !prev)}
54
+ onPress={handleToggle}
23
55
  activeOpacity={0.7}
56
+ accessibilityRole="button"
57
+ accessibilityState={{ expanded: isOpen }}
24
58
  style={{
25
59
  flexDirection: 'row',
26
60
  alignItems: 'center',
@@ -29,15 +63,15 @@ const CollapsibleComponent: React.FC<CollapsibleProps> = ({
29
63
  }}
30
64
  >
31
65
  {chevronIcon ?? (
32
- <Text
66
+ <Animated.Text
33
67
  style={{
34
68
  fontSize: 16,
35
69
  color: theme.colors.textSecondary,
36
- transform: [{ rotate: isOpen ? '90deg' : '0deg' }],
70
+ transform: [{ rotate: chevronRotation }],
37
71
  }}
38
72
  >
39
73
 
40
- </Text>
74
+ </Animated.Text>
41
75
  )}
42
76
  <Text
43
77
  style={[
@@ -2,6 +2,7 @@ import React, { memo, useEffect, useMemo } from 'react';
2
2
  import { View, Text, StyleSheet, type DimensionValue } from 'react-native';
3
3
 
4
4
  import { useTheme } from '../theme/use-theme';
5
+ import { animation } from '../styles/tokens';
5
6
  import { SpinnerIcon } from './SpinnerIcon';
6
7
  import type {
7
8
  LoadingProps,
@@ -108,7 +109,7 @@ const TopLoading: React.FC<TopLoadingProps> = ({
108
109
  // eslint-disable-next-line react-hooks/rules-of-hooks
109
110
  const translateY = useSharedValue(showLoading ? 0 : -targetHeight);
110
111
 
111
- const timingConfig = { duration: 250, easing: Easing.out(Easing.cubic) };
112
+ const timingConfig = { duration: animation.duration.slow, easing: Easing.out(Easing.cubic) };
112
113
 
113
114
  // eslint-disable-next-line react-hooks/rules-of-hooks
114
115
  useEffect(() => {
@@ -1,11 +1,36 @@
1
- import React from 'react';
2
- import { View, type ViewStyle, type TextStyle, StyleSheet } from 'react-native';
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { Animated, View, type ViewStyle, type TextStyle, StyleSheet } from 'react-native';
3
3
 
4
4
  import { useTheme } from '../theme/use-theme';
5
5
 
6
- type SkeletonProps = {
7
- blend?: boolean;
8
- };
6
+ const SHIMMER_DURATION = 1500;
7
+ const SHIMMER_MIN_OPACITY = 0.4;
8
+ const SHIMMER_MAX_OPACITY = 1;
9
+
10
+ function useShimmer() {
11
+ const opacity = useRef(new Animated.Value(SHIMMER_MAX_OPACITY)).current;
12
+
13
+ useEffect(() => {
14
+ const loop = Animated.loop(
15
+ Animated.sequence([
16
+ Animated.timing(opacity, {
17
+ toValue: SHIMMER_MIN_OPACITY,
18
+ duration: SHIMMER_DURATION / 2,
19
+ useNativeDriver: true,
20
+ }),
21
+ Animated.timing(opacity, {
22
+ toValue: SHIMMER_MAX_OPACITY,
23
+ duration: SHIMMER_DURATION / 2,
24
+ useNativeDriver: true,
25
+ }),
26
+ ]),
27
+ );
28
+ loop.start();
29
+ return () => loop.stop();
30
+ }, [opacity]);
31
+
32
+ return opacity;
33
+ }
9
34
 
10
35
  export function Text({
11
36
  blend,
@@ -15,6 +40,7 @@ export function Text({
15
40
  blend?: boolean;
16
41
  }) {
17
42
  const { colors } = useTheme();
43
+ const shimmer = useShimmer();
18
44
  const flattened = StyleSheet.flatten(style);
19
45
  const width = (flattened as ViewStyle)?.width;
20
46
  const lineHeight = (flattened?.lineHeight as number) || 14;
@@ -26,13 +52,13 @@ export function Text({
26
52
  { maxWidth: width as number },
27
53
  { paddingVertical: lineHeight * 0.15 },
28
54
  ]}>
29
- <View
55
+ <Animated.View
30
56
  style={[
31
57
  styles.textInner,
32
58
  {
33
59
  backgroundColor: colors.contrast50,
34
60
  height: lineHeight * 0.7,
35
- opacity: blend ? 0.6 : 1,
61
+ opacity: Animated.multiply(shimmer, blend ? 0.6 : 1),
36
62
  },
37
63
  ]}
38
64
  />
@@ -52,20 +78,22 @@ export function Circle({
52
78
  style?: ViewStyle | ViewStyle[];
53
79
  }) {
54
80
  const { colors } = useTheme();
81
+ const shimmer = useShimmer();
82
+
55
83
  return (
56
- <View
84
+ <Animated.View
57
85
  style={[
58
86
  styles.circle,
59
87
  {
60
88
  backgroundColor: colors.contrast50,
61
89
  width: size,
62
90
  height: size,
63
- opacity: blend ? 0.6 : 1,
91
+ opacity: Animated.multiply(shimmer, blend ? 0.6 : 1),
64
92
  },
65
93
  style,
66
94
  ]}>
67
95
  {children}
68
- </View>
96
+ </Animated.View>
69
97
  );
70
98
  }
71
99
 
@@ -79,15 +107,17 @@ export function Pill({
79
107
  style?: ViewStyle | ViewStyle[];
80
108
  }) {
81
109
  const { colors } = useTheme();
110
+ const shimmer = useShimmer();
111
+
82
112
  return (
83
- <View
113
+ <Animated.View
84
114
  style={[
85
115
  styles.pill,
86
116
  {
87
117
  backgroundColor: colors.contrast50,
88
118
  width: size * 1.618,
89
119
  height: size,
90
- opacity: blend ? 0.6 : 1,
120
+ opacity: Animated.multiply(shimmer, blend ? 0.6 : 1),
91
121
  },
92
122
  style,
93
123
  ]}
@@ -57,6 +57,27 @@ export const borderRadius = {
57
57
  full: 999,
58
58
  } as const;
59
59
 
60
+ /**
61
+ * Gradient definitions for icon system.
62
+ */
63
+ /**
64
+ * Animation timing and spring tokens.
65
+ * Centralized values ensure consistent motion across all components.
66
+ */
67
+ export const animation = {
68
+ duration: {
69
+ instant: 100,
70
+ fast: 150,
71
+ normal: 200,
72
+ slow: 300,
73
+ },
74
+ spring: {
75
+ snappy: { friction: 8, tension: 100 },
76
+ gentle: { friction: 8, tension: 60 },
77
+ bouncy: { friction: 6, tension: 120 },
78
+ },
79
+ } as const;
80
+
60
81
  /**
61
82
  * Gradient definitions for icon system.
62
83
  */
@@ -2,6 +2,7 @@ import React, { memo, useEffect, useRef } from 'react';
2
2
  import { Pressable, Animated } from 'react-native';
3
3
 
4
4
  import { useTheme } from '../theme/use-theme';
5
+ import { animation } from '../styles/tokens';
5
6
  import type { SwitchProps } from './types';
6
7
 
7
8
  const TRACK = { default: { w: 44, h: 26 }, sm: { w: 36, h: 22 } } as const;
@@ -19,8 +20,7 @@ const SwitchComponent = React.forwardRef<React.ElementRef<typeof Pressable>, Swi
19
20
  Animated.spring(anim, {
20
21
  toValue: value ? 1 : 0,
21
22
  useNativeDriver: false,
22
- friction: 8,
23
- tension: 60,
23
+ ...animation.spring.gentle,
24
24
  }).start();
25
25
  }, [value, anim]);
26
26
 
@@ -29,8 +29,7 @@ const SwitchComponent = React.forwardRef<React.ElementRef<typeof Pressable>, Swi
29
29
  Animated.spring(pressAnim, {
30
30
  toValue: 1,
31
31
  useNativeDriver: false,
32
- friction: 8,
33
- tension: 100,
32
+ ...animation.spring.snappy,
34
33
  }).start();
35
34
  };
36
35
 
@@ -38,8 +37,7 @@ const SwitchComponent = React.forwardRef<React.ElementRef<typeof Pressable>, Swi
38
37
  Animated.spring(pressAnim, {
39
38
  toValue: 0,
40
39
  useNativeDriver: false,
41
- friction: 8,
42
- tension: 60,
40
+ ...animation.spring.gentle,
43
41
  }).start();
44
42
  };
45
43
 
@@ -79,7 +77,7 @@ const SwitchComponent = React.forwardRef<React.ElementRef<typeof Pressable>, Swi
79
77
  onPressIn={onPressIn}
80
78
  onPressOut={onPressOut}
81
79
  style={[{ opacity: disabled ? 0.4 : 1 }, style]}
82
- hitSlop={4}
80
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
83
81
  testID={testID}
84
82
  >
85
83
  <Animated.View