cpk-ui 0.5.1 → 0.5.2

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.
@@ -0,0 +1,87 @@
1
+ import type { PropsWithChildren } from 'react';
2
+ import React from 'react';
3
+ import type { ViewStyle } from 'react-native';
4
+ import { Animated } from 'react-native';
5
+ export type PinchZoomProps = PropsWithChildren<{
6
+ /**
7
+ * Custom view style.
8
+ * @warning Passing `transform` in style will disable pinch-zoom functionality.
9
+ * Use a wrapper View for custom transforms instead.
10
+ */
11
+ style?: ViewStyle;
12
+ children?: any;
13
+ /** Callback fired when zoom scale changes */
14
+ onScaleChanged?(value: number): void;
15
+ /** Callback fired when content position changes */
16
+ onTranslateChanged?(valueXY: {
17
+ x: number;
18
+ y: number;
19
+ }): void;
20
+ /** Callback fired after gesture animation completes (decay or snap-back) */
21
+ onRelease?(): void;
22
+ /**
23
+ * Allow unrestricted overflow on specific axes.
24
+ * When true, allows overflow. When false/undefined, clamps to bounds.
25
+ */
26
+ allowEmpty?: {
27
+ x?: boolean;
28
+ y?: boolean;
29
+ };
30
+ /**
31
+ * Auto-snap to bounds after release.
32
+ * @default true
33
+ */
34
+ fixOverflowAfterRelease?: boolean;
35
+ /** Test ID for component testing */
36
+ testID?: string;
37
+ }>;
38
+ export interface PinchZoomRef {
39
+ animatedValue: {
40
+ scale: Animated.Value;
41
+ translate: Animated.ValueXY;
42
+ };
43
+ setValues(_: {
44
+ scale?: number;
45
+ translate?: {
46
+ x: number;
47
+ y: number;
48
+ };
49
+ }): void;
50
+ }
51
+ declare const PinchZoomRoot: React.ForwardRefExoticComponent<{
52
+ /**
53
+ * Custom view style.
54
+ * @warning Passing `transform` in style will disable pinch-zoom functionality.
55
+ * Use a wrapper View for custom transforms instead.
56
+ */
57
+ style?: ViewStyle;
58
+ children?: any;
59
+ /** Callback fired when zoom scale changes */
60
+ onScaleChanged?(value: number): void;
61
+ /** Callback fired when content position changes */
62
+ onTranslateChanged?(valueXY: {
63
+ x: number;
64
+ y: number;
65
+ }): void;
66
+ /** Callback fired after gesture animation completes (decay or snap-back) */
67
+ onRelease?(): void;
68
+ /**
69
+ * Allow unrestricted overflow on specific axes.
70
+ * When true, allows overflow. When false/undefined, clamps to bounds.
71
+ */
72
+ allowEmpty?: {
73
+ x?: boolean;
74
+ y?: boolean;
75
+ };
76
+ /**
77
+ * Auto-snap to bounds after release.
78
+ * @default true
79
+ */
80
+ fixOverflowAfterRelease?: boolean;
81
+ /** Test ID for component testing */
82
+ testID?: string;
83
+ } & {
84
+ children?: React.ReactNode | undefined;
85
+ } & React.RefAttributes<PinchZoomRef>>;
86
+ export { PinchZoomRoot as PinchZoom };
87
+ //# sourceMappingURL=PinchZoom.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PinchZoom.d.ts","sourceRoot":"","sources":["../../../../src/components/uis/PinchZoom/PinchZoom.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,iBAAiB,EAAM,MAAM,OAAO,CAAC;AAClD,OAAO,KAMN,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EAIV,SAAS,EACV,MAAM,cAAc,CAAC;AACtB,OAAO,EAAC,QAAQ,EAAe,MAAM,cAAc,CAAC;AAEpD,MAAM,MAAM,cAAc,GAAG,iBAAiB,CAAC;IAC7C;;;;OAIG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,QAAQ,CAAC,EAAE,GAAG,CAAC;IACf,6CAA6C;IAC7C,cAAc,CAAC,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,mDAAmD;IACnD,kBAAkB,CAAC,CAAC,OAAO,EAAE;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAC,GAAG,IAAI,CAAC;IAC3D,4EAA4E;IAC5E,SAAS,CAAC,IAAI,IAAI,CAAC;IACnB;;;OAGG;IACH,UAAU,CAAC,EAAE;QAAC,CAAC,CAAC,EAAE,OAAO,CAAC;QAAC,CAAC,CAAC,EAAE,OAAO,CAAA;KAAC,CAAC;IACxC;;;OAGG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,oCAAoC;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC,CAAC;AAEH,MAAM,WAAW,YAAY;IAC3B,aAAa,EAAE;QAAC,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC;QAAC,SAAS,EAAE,QAAQ,CAAC,OAAO,CAAA;KAAC,CAAC;IACpE,SAAS,CAAC,CAAC,EAAE;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE;YAAC,CAAC,EAAE,MAAM,CAAC;YAAC,CAAC,EAAE,MAAM,CAAA;SAAC,CAAA;KAAC,GAAG,IAAI,CAAC;CAC1E;AAsXD,QAAA,MAAM,aAAa;IApZjB;;;;OAIG;YACK,SAAS;eACN,GAAG;IACd,6CAA6C;2BACtB,MAAM,GAAG,IAAI;IACpC,mDAAmD;iCACtB;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAC,GAAG,IAAI;IAC1D,4EAA4E;kBAC9D,IAAI;IAClB;;;OAGG;iBACU;QAAC,CAAC,CAAC,EAAE,OAAO,CAAC;QAAC,CAAC,CAAC,EAAE,OAAO,CAAA;KAAC;IACvC;;;OAGG;8BACuB,OAAO;IACjC,oCAAoC;aAC3B,MAAM;;;sCA4XwD,CAAC;AAE1E,OAAO,EAAC,aAAa,IAAI,SAAS,EAAC,CAAC"}
@@ -0,0 +1,255 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { forwardRef, useEffect, useImperativeHandle, useRef, useState, } from 'react';
3
+ import { Animated, PanResponder } from 'react-native';
4
+ function getDistanceFromTouches(touches) {
5
+ const [touch1, touch2] = touches;
6
+ return Math.sqrt((touch1.pageX - touch2.pageX) ** 2 + (touch1.pageY - touch2.pageY) ** 2);
7
+ }
8
+ function getRelativeTouchesCenterPosition(touches, layout, transformCache) {
9
+ const pageX = (touches[0].pageX + touches[1].pageX) / 2 - layout.width / 2 - layout.pageX;
10
+ const pageY = (touches[0].pageY + touches[1].pageY) / 2 -
11
+ layout.height / 2 -
12
+ layout.pageY;
13
+ return {
14
+ locationX: (pageX - transformCache.translateX) / transformCache.scale,
15
+ locationY: (pageY - transformCache.translateY) / transformCache.scale,
16
+ pageX,
17
+ pageY,
18
+ };
19
+ }
20
+ function PinchZoom({ style, children, onScaleChanged, onRelease, onTranslateChanged, allowEmpty, fixOverflowAfterRelease = true, testID = 'pinch-zoom-container', }, ref) {
21
+ const containerView = useRef(null);
22
+ const scale = useRef(new Animated.Value(1)).current;
23
+ const translate = useRef(new Animated.ValueXY({ x: 0, y: 0 })).current;
24
+ const transformCache = useRef({
25
+ scale: 1,
26
+ translateX: 0,
27
+ translateY: 0,
28
+ }).current;
29
+ const lastTransform = useRef({ scale: 1, translateX: 0, translateY: 0 });
30
+ const initialDistance = useRef(undefined);
31
+ const initialTouchesCenter = useRef(undefined);
32
+ const layout = useRef(undefined);
33
+ const decayingTranslateAnimation = useRef(undefined);
34
+ const isResponderActive = useRef(false);
35
+ const movingVelocity = useRef(undefined);
36
+ containerView.current?.measure((x, y, width, height, pageX, pageY) => {
37
+ layout.current = { width, height, pageX, pageY };
38
+ });
39
+ useEffect(() => {
40
+ scale.addListener(({ value }) => {
41
+ transformCache.scale = value;
42
+ onScaleChanged?.(value);
43
+ });
44
+ const id = translate.addListener(({ x, y }) => {
45
+ if (decayingTranslateAnimation.current &&
46
+ layout.current &&
47
+ !isResponderActive.current) {
48
+ const overflowX = !allowEmpty?.x &&
49
+ Math.abs(x) > ((transformCache.scale - 1) * layout.current.width) / 2;
50
+ const overflowY = !allowEmpty?.y &&
51
+ Math.abs(y) >
52
+ ((transformCache.scale - 1) * layout.current.height) / 2;
53
+ if (overflowX || overflowY) {
54
+ decayingTranslateAnimation.current?.stop();
55
+ decayingTranslateAnimation.current = undefined;
56
+ translate.setValue({
57
+ x: overflowX
58
+ ? (Math.sign(x) *
59
+ (transformCache.scale - 1) *
60
+ layout.current.width) /
61
+ 2
62
+ : x,
63
+ y: overflowY
64
+ ? (Math.sign(y) *
65
+ (transformCache.scale - 1) *
66
+ layout.current.height) /
67
+ 2
68
+ : y,
69
+ });
70
+ }
71
+ }
72
+ transformCache.translateX = x;
73
+ transformCache.translateY = y;
74
+ onTranslateChanged?.({ x, y });
75
+ });
76
+ return () => {
77
+ scale.removeAllListeners();
78
+ translate.removeListener(id);
79
+ };
80
+ }, [
81
+ onScaleChanged,
82
+ onTranslateChanged,
83
+ scale,
84
+ transformCache,
85
+ translate,
86
+ allowEmpty?.x,
87
+ allowEmpty?.y,
88
+ ]);
89
+ const [panResponder, setPanResponder] = useState();
90
+ useEffect(() => {
91
+ setPanResponder(PanResponder.create({
92
+ onMoveShouldSetPanResponder: () => true,
93
+ onPanResponderGrant: ({ nativeEvent }) => {
94
+ isResponderActive.current = true;
95
+ movingVelocity.current = undefined;
96
+ if (decayingTranslateAnimation.current) {
97
+ decayingTranslateAnimation.current.stop();
98
+ }
99
+ const { touches } = nativeEvent;
100
+ lastTransform.current = { ...transformCache };
101
+ initialDistance.current = undefined;
102
+ if (layout.current != null) {
103
+ if (touches.length === 2) {
104
+ initialDistance.current = getDistanceFromTouches(touches);
105
+ initialTouchesCenter.current = getRelativeTouchesCenterPosition(touches, layout.current, transformCache);
106
+ }
107
+ else {
108
+ initialDistance.current = undefined;
109
+ }
110
+ }
111
+ },
112
+ onPanResponderMove: ({ nativeEvent }, gestureState) => {
113
+ const { touches } = nativeEvent;
114
+ if (layout.current == null) {
115
+ return;
116
+ }
117
+ if (movingVelocity.current) {
118
+ movingVelocity.current = {
119
+ x: (movingVelocity.current.x + gestureState.vx) / 2,
120
+ y: (movingVelocity.current.y + gestureState.vy) / 2,
121
+ };
122
+ }
123
+ else {
124
+ movingVelocity.current = { x: gestureState.vx, y: gestureState.vy };
125
+ }
126
+ if (touches.length === 2) {
127
+ if (initialDistance.current &&
128
+ initialTouchesCenter.current &&
129
+ layout.current) {
130
+ const newScale = Math.max(1, (getDistanceFromTouches(touches) / initialDistance.current) *
131
+ lastTransform.current.scale);
132
+ const { pageX, pageY } = getRelativeTouchesCenterPosition(touches, layout.current, transformCache);
133
+ scale.setValue(newScale);
134
+ const newTranslateX = pageX - initialTouchesCenter.current.locationX * newScale;
135
+ const newTranslateY = pageY - initialTouchesCenter.current.locationY * newScale;
136
+ translate.setValue({
137
+ x: allowEmpty?.x
138
+ ? newTranslateX
139
+ : Math.min(Math.abs(newTranslateX), ((newScale - 1) * layout.current.width) / 2) * Math.sign(newTranslateX),
140
+ y: allowEmpty?.y
141
+ ? newTranslateY
142
+ : Math.min(Math.abs(newTranslateY), ((newScale - 1) * layout.current.height) / 2) * Math.sign(newTranslateY),
143
+ });
144
+ }
145
+ else {
146
+ initialDistance.current = getDistanceFromTouches(touches);
147
+ initialTouchesCenter.current = getRelativeTouchesCenterPosition(touches, layout.current, transformCache);
148
+ }
149
+ }
150
+ else if (touches.length === 1) {
151
+ if (initialDistance.current) {
152
+ return;
153
+ }
154
+ const newTranslateX = lastTransform.current.translateX + gestureState.dx;
155
+ const newTranslateY = lastTransform.current.translateY + gestureState.dy;
156
+ translate.setValue({
157
+ x: newTranslateX,
158
+ y: newTranslateY,
159
+ });
160
+ }
161
+ },
162
+ onPanResponderRelease: () => {
163
+ isResponderActive.current = false;
164
+ if (layout.current == null) {
165
+ return;
166
+ }
167
+ const overflowX = !allowEmpty?.x &&
168
+ Math.abs(transformCache.translateX) >
169
+ ((transformCache.scale - 1) * layout.current.width) / 2;
170
+ const overflowY = !allowEmpty?.y &&
171
+ Math.abs(transformCache.translateY) >
172
+ ((transformCache.scale - 1) * layout.current.height) / 2;
173
+ if (overflowX || overflowY) {
174
+ if (!fixOverflowAfterRelease) {
175
+ onRelease?.();
176
+ return;
177
+ }
178
+ decayingTranslateAnimation.current?.stop();
179
+ decayingTranslateAnimation.current = undefined;
180
+ const toValue = {
181
+ x: overflowX
182
+ ? (Math.sign(transformCache.translateX) *
183
+ (transformCache.scale - 1) *
184
+ layout.current.width) /
185
+ 2
186
+ : transformCache.translateX,
187
+ y: overflowY
188
+ ? (Math.sign(transformCache.translateY) *
189
+ (transformCache.scale - 1) *
190
+ layout.current.height) /
191
+ 2
192
+ : transformCache.translateY,
193
+ };
194
+ Animated.timing(translate, {
195
+ toValue,
196
+ duration: 100,
197
+ useNativeDriver: true,
198
+ }).start(() => {
199
+ onRelease?.();
200
+ });
201
+ }
202
+ else {
203
+ decayingTranslateAnimation.current = Animated.decay(translate, {
204
+ velocity: movingVelocity.current ?? { x: 0, y: 0 },
205
+ useNativeDriver: true,
206
+ });
207
+ decayingTranslateAnimation.current.start(() => {
208
+ decayingTranslateAnimation.current = undefined;
209
+ onRelease?.();
210
+ });
211
+ }
212
+ },
213
+ }));
214
+ }, [
215
+ allowEmpty?.x,
216
+ allowEmpty?.y,
217
+ fixOverflowAfterRelease,
218
+ onRelease,
219
+ scale,
220
+ transformCache,
221
+ translate,
222
+ ]);
223
+ useImperativeHandle(ref, () => ({
224
+ animatedValue: { scale, translate },
225
+ setValues: (values) => {
226
+ values.scale != null && scale.setValue(values.scale);
227
+ values.translate != null && translate.setValue(values.translate);
228
+ },
229
+ }));
230
+ // Warn if style.transform is provided, as it will disable pinch-zoom
231
+ if (__DEV__ && style?.transform) {
232
+ console.warn('PinchZoom: passing transform in style prop will disable pinch-zoom functionality. ' +
233
+ 'Use a wrapper View for custom transforms instead.');
234
+ }
235
+ return (_jsx(Animated.View
236
+ // @ts-ignore - ref type issue with NativeMethods
237
+ , {
238
+ // @ts-ignore - ref type issue with NativeMethods
239
+ ref: (pinchViewRef) => {
240
+ containerView.current = pinchViewRef;
241
+ }, style: [
242
+ style,
243
+ style?.transform
244
+ ? {}
245
+ : {
246
+ transform: [
247
+ { translateX: translate.x },
248
+ { translateY: translate.y },
249
+ { scale },
250
+ ],
251
+ },
252
+ ], testID: testID, ...(panResponder?.panHandlers || {}), children: children }));
253
+ }
254
+ const PinchZoomRoot = forwardRef(PinchZoom);
255
+ export { PinchZoomRoot as PinchZoom };
package/index.d.ts CHANGED
@@ -13,6 +13,7 @@ export * from './components/uis/Hr/Hr';
13
13
  export * from './components/uis/Icon/Icon';
14
14
  export * from './components/uis/IconButton/IconButton';
15
15
  export * from './components/uis/LoadingIndicator/LoadingIndicator';
16
+ export * from './components/uis/PinchZoom/PinchZoom';
16
17
  export * from './components/uis/Rating/Rating';
17
18
  export * from './components/uis/RadioGroup/RadioGroup';
18
19
  export * from './components/uis/StatusbarBrightness/StatusBarBrightness';
package/index.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AACA,cAAc,6CAA6C,CAAC;AAC5D,cAAc,uCAAuC,CAAC;AAGtD,cAAc,iDAAiD,CAAC;AAGhE,cAAc,aAAa,CAAC;AAG5B,cAAc,sCAAsC,CAAC;AACrD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,oCAAoC,CAAC;AACnD,cAAc,kDAAkD,CAAC;AACjE,cAAc,oCAAoC,CAAC;AACnD,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,wCAAwC,CAAC;AACvD,cAAc,oDAAoD,CAAC;AACnE,cAAc,gCAAgC,CAAC;AAC/C,cAAc,wCAAwC,CAAC;AACvD,cAAc,0DAA0D,CAAC;AACzE,cAAc,4CAA4C,CAAC;AAC3D,cAAc,wCAAwC,CAAC;AAGvD,cAAc,SAAS,CAAC;AAGxB,cAAc,SAAS,CAAC;AAGxB,cAAc,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AACA,cAAc,6CAA6C,CAAC;AAC5D,cAAc,uCAAuC,CAAC;AAGtD,cAAc,iDAAiD,CAAC;AAGhE,cAAc,aAAa,CAAC;AAG5B,cAAc,sCAAsC,CAAC;AACrD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,oCAAoC,CAAC;AACnD,cAAc,kDAAkD,CAAC;AACjE,cAAc,oCAAoC,CAAC;AACnD,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,wCAAwC,CAAC;AACvD,cAAc,oDAAoD,CAAC;AACnE,cAAc,sCAAsC,CAAC;AACrD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,wCAAwC,CAAC;AACvD,cAAc,0DAA0D,CAAC;AACzE,cAAc,4CAA4C,CAAC;AAC3D,cAAc,wCAAwC,CAAC;AAGvD,cAAc,SAAS,CAAC;AAGxB,cAAc,SAAS,CAAC;AAGxB,cAAc,SAAS,CAAC"}
package/index.js CHANGED
@@ -17,6 +17,7 @@ export * from './components/uis/Hr/Hr';
17
17
  export * from './components/uis/Icon/Icon';
18
18
  export * from './components/uis/IconButton/IconButton';
19
19
  export * from './components/uis/LoadingIndicator/LoadingIndicator';
20
+ export * from './components/uis/PinchZoom/PinchZoom';
20
21
  export * from './components/uis/Rating/Rating';
21
22
  export * from './components/uis/RadioGroup/RadioGroup';
22
23
  export * from './components/uis/StatusbarBrightness/StatusBarBrightness';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cpk-ui",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "main": "index",
5
5
  "react-native": "index",
6
6
  "module": "index",
@@ -0,0 +1,266 @@
1
+ import {action} from '@storybook/addon-actions';
2
+ import type {Meta, StoryObj} from '@storybook/react';
3
+ import React from 'react';
4
+ import {Image, Text, View} from 'react-native';
5
+ import {css} from 'kstyled';
6
+
7
+ import {withThemeProvider} from '../../../../.storybook/decorators';
8
+ import {PinchZoom} from './PinchZoom';
9
+
10
+ const meta = {
11
+ title: 'PinchZoom',
12
+ component: PinchZoom,
13
+ parameters: {
14
+ docs: {
15
+ description: {
16
+ component: `
17
+ A powerful PinchZoom component that enables pinch-to-zoom and pan gestures for images and content.
18
+
19
+ ## Features
20
+ - **Pinch to Zoom**: Two-finger pinch gesture for zooming in/out
21
+ - **Pan Gesture**: Single-finger drag to move zoomed content
22
+ - **Momentum Scrolling**: Smooth deceleration after pan gesture release
23
+ - **Boundary Detection**: Prevents excessive panning beyond content bounds
24
+ - **Customizable Constraints**: Control overflow behavior on X and Y axes
25
+ - **Programmatic Control**: Imperative API to set scale and translation values
26
+
27
+ ## Props
28
+ - \`onScaleChanged\`: Callback fired when zoom scale changes
29
+ - \`onTranslateChanged\`: Callback fired when content position changes
30
+ - \`onRelease\`: Callback fired after gesture animation completes (decay or snap-back)
31
+ - \`allowEmpty\`: When \`true\`, allows unrestricted overflow on specific axes (x/y). When \`false\` or undefined, clamps translation to content bounds
32
+ - \`fixOverflowAfterRelease\`: Auto-snap to bounds after release (default: true). When \`false\`, \`onRelease\` fires immediately without snap animation
33
+ - \`style\`: Custom view style. **Warning**: Passing \`transform\` in style will disable pinch-zoom. Use a wrapper View for custom transforms
34
+
35
+ ## Usage
36
+ \`\`\`tsx
37
+ <PinchZoom
38
+ onScaleChanged={(scale) => console.log('Scale:', scale)}
39
+ onTranslateChanged={({x, y}) => console.log('Position:', x, y)}
40
+ onRelease={() => console.log('Released')}
41
+ >
42
+ <Image
43
+ source={{uri: 'https://example.com/image.jpg'}}
44
+ style={{width: 300, height: 300}}
45
+ />
46
+ </PinchZoom>
47
+ \`\`\`
48
+
49
+ ## Imperative API
50
+ \`\`\`tsx
51
+ const pinchZoomRef = useRef<PinchZoomRef>(null);
52
+
53
+ // Set zoom and position programmatically
54
+ pinchZoomRef.current?.setValues({
55
+ scale: 2,
56
+ translate: {x: 50, y: 50}
57
+ });
58
+ \`\`\`
59
+ `,
60
+ },
61
+ },
62
+ },
63
+ argTypes: {
64
+ fixOverflowAfterRelease: {
65
+ control: 'boolean',
66
+ description: 'Automatically snap content back to bounds after gesture release',
67
+ defaultValue: true,
68
+ },
69
+ },
70
+ decorators: [withThemeProvider],
71
+ } satisfies Meta<typeof PinchZoom>;
72
+
73
+ export default meta;
74
+
75
+ type Story = StoryObj<typeof meta>;
76
+
77
+ export const WithImage: Story = {
78
+ args: {
79
+ onScaleChanged: action('onScaleChanged'),
80
+ onTranslateChanged: action('onTranslateChanged'),
81
+ onRelease: action('onRelease'),
82
+ fixOverflowAfterRelease: true,
83
+ },
84
+ render: (args) => (
85
+ <View
86
+ style={css`
87
+ flex: 1;
88
+ justify-content: center;
89
+ align-items: center;
90
+ background-color: #f5f5f5;
91
+ `}
92
+ >
93
+ <PinchZoom {...args}>
94
+ <Image
95
+ source={{
96
+ uri: 'https://picsum.photos/300/300',
97
+ }}
98
+ style={css`
99
+ width: 300px;
100
+ height: 300px;
101
+ border-radius: 8px;
102
+ `}
103
+ />
104
+ </PinchZoom>
105
+ </View>
106
+ ),
107
+ };
108
+
109
+ export const WithColorBox: Story = {
110
+ args: {
111
+ onScaleChanged: action('onScaleChanged'),
112
+ onTranslateChanged: action('onTranslateChanged'),
113
+ onRelease: action('onRelease'),
114
+ fixOverflowAfterRelease: true,
115
+ },
116
+ render: (args) => (
117
+ <View
118
+ style={css`
119
+ flex: 1;
120
+ justify-content: center;
121
+ align-items: center;
122
+ background-color: #f5f5f5;
123
+ `}
124
+ >
125
+ <PinchZoom {...args}>
126
+ <View
127
+ style={css`
128
+ width: 250px;
129
+ height: 250px;
130
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
131
+ border-radius: 16px;
132
+ justify-content: center;
133
+ align-items: center;
134
+ shadow-color: #000;
135
+ shadow-offset: 0px 4px;
136
+ shadow-opacity: 0.3;
137
+ shadow-radius: 8px;
138
+ elevation: 5;
139
+ `}
140
+ >
141
+ <Text
142
+ style={css`
143
+ color: white;
144
+ font-size: 24px;
145
+ font-weight: bold;
146
+ `}
147
+ >
148
+ Pinch to Zoom
149
+ </Text>
150
+ <Text
151
+ style={css`
152
+ color: rgba(255, 255, 255, 0.9);
153
+ font-size: 14px;
154
+ margin-top: 8px;
155
+ `}
156
+ >
157
+ Try pinching and dragging!
158
+ </Text>
159
+ </View>
160
+ </PinchZoom>
161
+ </View>
162
+ ),
163
+ };
164
+
165
+ export const WithoutBoundarySnap: Story = {
166
+ args: {
167
+ onScaleChanged: action('onScaleChanged'),
168
+ onTranslateChanged: action('onTranslateChanged'),
169
+ onRelease: action('onRelease'),
170
+ fixOverflowAfterRelease: false,
171
+ },
172
+ render: (args) => (
173
+ <View
174
+ style={css`
175
+ flex: 1;
176
+ justify-content: center;
177
+ align-items: center;
178
+ background-color: #f5f5f5;
179
+ `}
180
+ >
181
+ <PinchZoom {...args}>
182
+ <Image
183
+ source={{
184
+ uri: 'https://picsum.photos/300/400',
185
+ }}
186
+ style={css`
187
+ width: 300px;
188
+ height: 400px;
189
+ border-radius: 8px;
190
+ `}
191
+ />
192
+ </PinchZoom>
193
+ <Text
194
+ style={css`
195
+ position: absolute;
196
+ bottom: 40px;
197
+ color: #666;
198
+ font-size: 12px;
199
+ `}
200
+ >
201
+ fixOverflowAfterRelease = false
202
+ </Text>
203
+ </View>
204
+ ),
205
+ };
206
+
207
+ export const WithCustomStyle: Story = {
208
+ args: {
209
+ onScaleChanged: action('onScaleChanged'),
210
+ onTranslateChanged: action('onTranslateChanged'),
211
+ onRelease: action('onRelease'),
212
+ fixOverflowAfterRelease: true,
213
+ },
214
+ render: (args) => (
215
+ <View
216
+ style={css`
217
+ flex: 1;
218
+ justify-content: center;
219
+ align-items: center;
220
+ background-color: #1a1a1a;
221
+ `}
222
+ >
223
+ <PinchZoom
224
+ {...args}
225
+ style={css`
226
+ border-width: 3px;
227
+ border-color: #667eea;
228
+ border-radius: 12px;
229
+ overflow: hidden;
230
+ `}
231
+ >
232
+ <View
233
+ style={css`
234
+ width: 280px;
235
+ height: 280px;
236
+ background-color: #2d2d2d;
237
+ justify-content: center;
238
+ align-items: center;
239
+ padding: 20px;
240
+ `}
241
+ >
242
+ <Text
243
+ style={css`
244
+ color: #667eea;
245
+ font-size: 20px;
246
+ font-weight: bold;
247
+ text-align: center;
248
+ `}
249
+ >
250
+ Custom Styled
251
+ </Text>
252
+ <Text
253
+ style={css`
254
+ color: #999;
255
+ font-size: 14px;
256
+ text-align: center;
257
+ margin-top: 12px;
258
+ `}
259
+ >
260
+ This PinchZoom has custom border and background styles
261
+ </Text>
262
+ </View>
263
+ </PinchZoom>
264
+ </View>
265
+ ),
266
+ };
@@ -0,0 +1,113 @@
1
+ import '@testing-library/jest-native/extend-expect';
2
+
3
+ import React, {type ReactElement} from 'react';
4
+ import {Text, View} from 'react-native';
5
+ import type {RenderAPI} from '@testing-library/react-native';
6
+ import {fireEvent, render} from '@testing-library/react-native';
7
+
8
+ import {createComponent} from '../../../../test/testUtils';
9
+ import {PinchZoom} from './PinchZoom';
10
+ import type {PinchZoomProps} from './PinchZoom';
11
+
12
+ let testingLib: RenderAPI;
13
+
14
+ const Component = ({props}: {props?: PinchZoomProps}): ReactElement =>
15
+ createComponent(
16
+ <PinchZoom {...props}>
17
+ <View testID="child-view">
18
+ <Text>Test Child</Text>
19
+ </View>
20
+ </PinchZoom>,
21
+ );
22
+
23
+ describe('[PinchZoom]', () => {
24
+ it('should render without crashing', () => {
25
+ testingLib = render(Component({}));
26
+
27
+ const json = testingLib.toJSON();
28
+ expect(json).toBeTruthy();
29
+ });
30
+
31
+ it('should render children correctly', () => {
32
+ testingLib = render(Component({}));
33
+
34
+ expect(testingLib.getByTestId('child-view')).toBeTruthy();
35
+ expect(testingLib.getByText('Test Child')).toBeTruthy();
36
+ });
37
+
38
+ it('should render with custom testID', () => {
39
+ testingLib = render(Component({props: {testID: 'custom-pinch-zoom'}}));
40
+
41
+ expect(testingLib.getByTestId('custom-pinch-zoom')).toBeTruthy();
42
+ });
43
+
44
+ it('should call onScaleChanged callback', () => {
45
+ const onScaleChanged = jest.fn();
46
+ testingLib = render(Component({props: {onScaleChanged}}));
47
+
48
+ // Note: Actual scale changes would require gesture simulation
49
+ // which is complex in unit tests. This test verifies the prop is passed.
50
+ expect(onScaleChanged).toBeDefined();
51
+ });
52
+
53
+ it('should call onTranslateChanged callback', () => {
54
+ const onTranslateChanged = jest.fn();
55
+ testingLib = render(Component({props: {onTranslateChanged}}));
56
+
57
+ // Note: Actual translation changes would require gesture simulation
58
+ // which is complex in unit tests. This test verifies the prop is passed.
59
+ expect(onTranslateChanged).toBeDefined();
60
+ });
61
+
62
+ it('should call onRelease callback', () => {
63
+ const onRelease = jest.fn();
64
+ testingLib = render(Component({props: {onRelease}}));
65
+
66
+ expect(onRelease).toBeDefined();
67
+ });
68
+
69
+ it('should render with custom style', () => {
70
+ const customStyle = {backgroundColor: 'red', width: 300, height: 300};
71
+ testingLib = render(Component({props: {style: customStyle}}));
72
+
73
+ const container = testingLib.getByTestId('pinch-zoom-container');
74
+ expect(container).toHaveStyle(customStyle);
75
+ });
76
+
77
+ it('should accept fixOverflowAfterRelease prop', () => {
78
+ testingLib = render(Component({props: {fixOverflowAfterRelease: false}}));
79
+
80
+ expect(testingLib.getByTestId('pinch-zoom-container')).toBeTruthy();
81
+ });
82
+
83
+ it('should accept allowEmpty prop', () => {
84
+ testingLib = render(
85
+ Component({props: {allowEmpty: {x: true, y: false}}}),
86
+ );
87
+
88
+ expect(testingLib.getByTestId('pinch-zoom-container')).toBeTruthy();
89
+ });
90
+
91
+ it('should render with all props combined', () => {
92
+ const onScaleChanged = jest.fn();
93
+ const onTranslateChanged = jest.fn();
94
+ const onRelease = jest.fn();
95
+
96
+ testingLib = render(
97
+ Component({
98
+ props: {
99
+ testID: 'full-props-test',
100
+ style: {backgroundColor: 'blue'},
101
+ onScaleChanged,
102
+ onTranslateChanged,
103
+ onRelease,
104
+ fixOverflowAfterRelease: true,
105
+ allowEmpty: {x: false, y: true},
106
+ },
107
+ }),
108
+ );
109
+
110
+ expect(testingLib.getByTestId('full-props-test')).toBeTruthy();
111
+ expect(testingLib.getByTestId('child-view')).toBeTruthy();
112
+ });
113
+ });
@@ -0,0 +1,424 @@
1
+ import type {PropsWithChildren, Ref} from 'react';
2
+ import React, {
3
+ forwardRef,
4
+ useEffect,
5
+ useImperativeHandle,
6
+ useRef,
7
+ useState,
8
+ } from 'react';
9
+ import type {
10
+ NativeMethods,
11
+ NativeTouchEvent,
12
+ PanResponderInstance,
13
+ ViewStyle,
14
+ } from 'react-native';
15
+ import {Animated, PanResponder} from 'react-native';
16
+
17
+ export type PinchZoomProps = PropsWithChildren<{
18
+ /**
19
+ * Custom view style.
20
+ * @warning Passing `transform` in style will disable pinch-zoom functionality.
21
+ * Use a wrapper View for custom transforms instead.
22
+ */
23
+ style?: ViewStyle;
24
+ children?: any;
25
+ /** Callback fired when zoom scale changes */
26
+ onScaleChanged?(value: number): void;
27
+ /** Callback fired when content position changes */
28
+ onTranslateChanged?(valueXY: {x: number; y: number}): void;
29
+ /** Callback fired after gesture animation completes (decay or snap-back) */
30
+ onRelease?(): void;
31
+ /**
32
+ * Allow unrestricted overflow on specific axes.
33
+ * When true, allows overflow. When false/undefined, clamps to bounds.
34
+ */
35
+ allowEmpty?: {x?: boolean; y?: boolean};
36
+ /**
37
+ * Auto-snap to bounds after release.
38
+ * @default true
39
+ */
40
+ fixOverflowAfterRelease?: boolean;
41
+ /** Test ID for component testing */
42
+ testID?: string;
43
+ }>;
44
+
45
+ export interface PinchZoomRef {
46
+ animatedValue: {scale: Animated.Value; translate: Animated.ValueXY};
47
+ setValues(_: {scale?: number; translate?: {x: number; y: number}}): void;
48
+ }
49
+
50
+ type TouchePosition = Pick<
51
+ NativeTouchEvent,
52
+ 'locationX' | 'locationY' | 'pageX' | 'pageY'
53
+ >;
54
+
55
+ function getDistanceFromTouches(touches: NativeTouchEvent[]): number {
56
+ const [touch1, touch2] = touches;
57
+
58
+ return Math.sqrt(
59
+ (touch1.pageX - touch2.pageX) ** 2 + (touch1.pageY - touch2.pageY) ** 2,
60
+ );
61
+ }
62
+
63
+ function getRelativeTouchesCenterPosition(
64
+ touches: NativeTouchEvent[],
65
+ layout: {width: number; height: number; pageX: number; pageY: number},
66
+ transformCache: {scale: number; translateX: number; translateY: number},
67
+ ): TouchePosition {
68
+ const pageX =
69
+ (touches[0].pageX + touches[1].pageX) / 2 - layout.width / 2 - layout.pageX;
70
+
71
+ const pageY =
72
+ (touches[0].pageY + touches[1].pageY) / 2 -
73
+ layout.height / 2 -
74
+ layout.pageY;
75
+
76
+ return {
77
+ locationX: (pageX - transformCache.translateX) / transformCache.scale,
78
+ locationY: (pageY - transformCache.translateY) / transformCache.scale,
79
+ pageX,
80
+ pageY,
81
+ };
82
+ }
83
+
84
+ function PinchZoom(
85
+ {
86
+ style,
87
+ children,
88
+ onScaleChanged,
89
+ onRelease,
90
+ onTranslateChanged,
91
+ allowEmpty,
92
+ fixOverflowAfterRelease = true,
93
+ testID = 'pinch-zoom-container',
94
+ }: PinchZoomProps,
95
+ ref: Ref<PinchZoomRef>,
96
+ ): JSX.Element {
97
+ const containerView = useRef<NativeMethods | null>(null);
98
+ const scale = useRef(new Animated.Value(1)).current;
99
+ const translate = useRef(new Animated.ValueXY({x: 0, y: 0})).current;
100
+
101
+ const transformCache = useRef({
102
+ scale: 1,
103
+ translateX: 0,
104
+ translateY: 0,
105
+ }).current;
106
+
107
+ const lastTransform = useRef({scale: 1, translateX: 0, translateY: 0});
108
+ const initialDistance = useRef<number | undefined>(undefined);
109
+ const initialTouchesCenter = useRef<TouchePosition | undefined>(undefined);
110
+
111
+ type Layout = {
112
+ width: number;
113
+ height: number;
114
+ pageX: number;
115
+ pageY: number;
116
+ };
117
+
118
+ const layout = useRef<Layout | undefined>(undefined);
119
+
120
+ const decayingTranslateAnimation = useRef<
121
+ Animated.CompositeAnimation | undefined
122
+ >(undefined);
123
+ const isResponderActive = useRef(false);
124
+ const movingVelocity = useRef<{x: number; y: number} | undefined>(undefined);
125
+
126
+ containerView.current?.measure((x, y, width, height, pageX, pageY) => {
127
+ layout.current = {width, height, pageX, pageY};
128
+ });
129
+
130
+ useEffect(() => {
131
+ scale.addListener(({value}) => {
132
+ transformCache.scale = value;
133
+ onScaleChanged?.(value);
134
+ });
135
+
136
+ const id = translate.addListener(({x, y}) => {
137
+ if (
138
+ decayingTranslateAnimation.current &&
139
+ layout.current &&
140
+ !isResponderActive.current
141
+ ) {
142
+ const overflowX =
143
+ !allowEmpty?.x &&
144
+ Math.abs(x) > ((transformCache.scale - 1) * layout.current.width) / 2;
145
+
146
+ const overflowY =
147
+ !allowEmpty?.y &&
148
+ Math.abs(y) >
149
+ ((transformCache.scale - 1) * layout.current.height) / 2;
150
+
151
+ if (overflowX || overflowY) {
152
+ decayingTranslateAnimation.current?.stop();
153
+ decayingTranslateAnimation.current = undefined;
154
+
155
+ translate.setValue({
156
+ x: overflowX
157
+ ? (Math.sign(x) *
158
+ (transformCache.scale - 1) *
159
+ layout.current.width) /
160
+ 2
161
+ : x,
162
+ y: overflowY
163
+ ? (Math.sign(y) *
164
+ (transformCache.scale - 1) *
165
+ layout.current.height) /
166
+ 2
167
+ : y,
168
+ });
169
+ }
170
+ }
171
+
172
+ transformCache.translateX = x;
173
+ transformCache.translateY = y;
174
+ onTranslateChanged?.({x, y});
175
+ });
176
+
177
+ return () => {
178
+ scale.removeAllListeners();
179
+ translate.removeListener(id);
180
+ };
181
+ }, [
182
+ onScaleChanged,
183
+ onTranslateChanged,
184
+ scale,
185
+ transformCache,
186
+ translate,
187
+ allowEmpty?.x,
188
+ allowEmpty?.y,
189
+ ]);
190
+
191
+ const [panResponder, setPanResponder] = useState<PanResponderInstance>();
192
+
193
+ useEffect(() => {
194
+ setPanResponder(
195
+ PanResponder.create({
196
+ onMoveShouldSetPanResponder: () => true,
197
+ onPanResponderGrant: ({nativeEvent}) => {
198
+ isResponderActive.current = true;
199
+ movingVelocity.current = undefined;
200
+
201
+ if (decayingTranslateAnimation.current) {
202
+ decayingTranslateAnimation.current.stop();
203
+ }
204
+
205
+ const {touches} = nativeEvent;
206
+
207
+ lastTransform.current = {...transformCache};
208
+
209
+ initialDistance.current = undefined;
210
+
211
+ if (layout.current != null) {
212
+ if (touches.length === 2) {
213
+ initialDistance.current = getDistanceFromTouches(touches);
214
+
215
+ initialTouchesCenter.current = getRelativeTouchesCenterPosition(
216
+ touches,
217
+ layout.current,
218
+ transformCache,
219
+ );
220
+ } else {
221
+ initialDistance.current = undefined;
222
+ }
223
+ }
224
+ },
225
+ onPanResponderMove: ({nativeEvent}, gestureState) => {
226
+ const {touches} = nativeEvent;
227
+
228
+ if (layout.current == null) {
229
+ return;
230
+ }
231
+
232
+ if (movingVelocity.current) {
233
+ movingVelocity.current = {
234
+ x: (movingVelocity.current.x + gestureState.vx) / 2,
235
+ y: (movingVelocity.current.y + gestureState.vy) / 2,
236
+ };
237
+ } else {
238
+ movingVelocity.current = {x: gestureState.vx, y: gestureState.vy};
239
+ }
240
+
241
+ if (touches.length === 2) {
242
+ if (
243
+ initialDistance.current &&
244
+ initialTouchesCenter.current &&
245
+ layout.current
246
+ ) {
247
+ const newScale = Math.max(
248
+ 1,
249
+ (getDistanceFromTouches(touches) / initialDistance.current) *
250
+ lastTransform.current.scale,
251
+ );
252
+
253
+ const {pageX, pageY} = getRelativeTouchesCenterPosition(
254
+ touches,
255
+ layout.current,
256
+ transformCache,
257
+ );
258
+
259
+ scale.setValue(newScale);
260
+
261
+ const newTranslateX =
262
+ pageX - initialTouchesCenter.current.locationX * newScale;
263
+
264
+ const newTranslateY =
265
+ pageY - initialTouchesCenter.current.locationY * newScale;
266
+
267
+ translate.setValue({
268
+ x: allowEmpty?.x
269
+ ? newTranslateX
270
+ : Math.min(
271
+ Math.abs(newTranslateX),
272
+ ((newScale - 1) * layout.current.width) / 2,
273
+ ) * Math.sign(newTranslateX),
274
+ y: allowEmpty?.y
275
+ ? newTranslateY
276
+ : Math.min(
277
+ Math.abs(newTranslateY),
278
+ ((newScale - 1) * layout.current.height) / 2,
279
+ ) * Math.sign(newTranslateY),
280
+ });
281
+ } else {
282
+ initialDistance.current = getDistanceFromTouches(touches);
283
+
284
+ initialTouchesCenter.current = getRelativeTouchesCenterPosition(
285
+ touches,
286
+ layout.current,
287
+ transformCache,
288
+ );
289
+ }
290
+ } else if (touches.length === 1) {
291
+ if (initialDistance.current) {
292
+ return;
293
+ }
294
+
295
+ const newTranslateX =
296
+ lastTransform.current.translateX + gestureState.dx;
297
+
298
+ const newTranslateY =
299
+ lastTransform.current.translateY + gestureState.dy;
300
+
301
+ translate.setValue({
302
+ x: newTranslateX,
303
+ y: newTranslateY,
304
+ });
305
+ }
306
+ },
307
+ onPanResponderRelease: () => {
308
+ isResponderActive.current = false;
309
+
310
+ if (layout.current == null) {
311
+ return;
312
+ }
313
+
314
+ const overflowX =
315
+ !allowEmpty?.x &&
316
+ Math.abs(transformCache.translateX) >
317
+ ((transformCache.scale - 1) * layout.current.width) / 2;
318
+
319
+ const overflowY =
320
+ !allowEmpty?.y &&
321
+ Math.abs(transformCache.translateY) >
322
+ ((transformCache.scale - 1) * layout.current.height) / 2;
323
+
324
+ if (overflowX || overflowY) {
325
+ if (!fixOverflowAfterRelease) {
326
+ onRelease?.();
327
+
328
+ return;
329
+ }
330
+
331
+ decayingTranslateAnimation.current?.stop();
332
+ decayingTranslateAnimation.current = undefined;
333
+
334
+ const toValue = {
335
+ x: overflowX
336
+ ? (Math.sign(transformCache.translateX) *
337
+ (transformCache.scale - 1) *
338
+ layout.current.width) /
339
+ 2
340
+ : transformCache.translateX,
341
+ y: overflowY
342
+ ? (Math.sign(transformCache.translateY) *
343
+ (transformCache.scale - 1) *
344
+ layout.current.height) /
345
+ 2
346
+ : transformCache.translateY,
347
+ };
348
+
349
+ Animated.timing(translate, {
350
+ toValue,
351
+ duration: 100,
352
+ useNativeDriver: true,
353
+ }).start(() => {
354
+ onRelease?.();
355
+ });
356
+ } else {
357
+ decayingTranslateAnimation.current = Animated.decay(translate, {
358
+ velocity: movingVelocity.current ?? {x: 0, y: 0},
359
+ useNativeDriver: true,
360
+ });
361
+
362
+ decayingTranslateAnimation.current.start(() => {
363
+ decayingTranslateAnimation.current = undefined;
364
+ onRelease?.();
365
+ });
366
+ }
367
+ },
368
+ }),
369
+ );
370
+ }, [
371
+ allowEmpty?.x,
372
+ allowEmpty?.y,
373
+ fixOverflowAfterRelease,
374
+ onRelease,
375
+ scale,
376
+ transformCache,
377
+ translate,
378
+ ]);
379
+
380
+ useImperativeHandle(ref, () => ({
381
+ animatedValue: {scale, translate},
382
+ setValues: (values) => {
383
+ values.scale != null && scale.setValue(values.scale);
384
+ values.translate != null && translate.setValue(values.translate);
385
+ },
386
+ }));
387
+
388
+ // Warn if style.transform is provided, as it will disable pinch-zoom
389
+ if (__DEV__ && style?.transform) {
390
+ console.warn(
391
+ 'PinchZoom: passing transform in style prop will disable pinch-zoom functionality. ' +
392
+ 'Use a wrapper View for custom transforms instead.',
393
+ );
394
+ }
395
+
396
+ return (
397
+ <Animated.View
398
+ // @ts-ignore - ref type issue with NativeMethods
399
+ ref={(pinchViewRef: NativeMethods | null) => {
400
+ containerView.current = pinchViewRef;
401
+ }}
402
+ style={[
403
+ style,
404
+ style?.transform
405
+ ? {}
406
+ : {
407
+ transform: [
408
+ {translateX: translate.x},
409
+ {translateY: translate.y},
410
+ {scale},
411
+ ],
412
+ },
413
+ ]}
414
+ testID={testID}
415
+ {...(panResponder?.panHandlers || {})}
416
+ >
417
+ {children}
418
+ </Animated.View>
419
+ );
420
+ }
421
+
422
+ const PinchZoomRoot = forwardRef<PinchZoomRef, PinchZoomProps>(PinchZoom);
423
+
424
+ export {PinchZoomRoot as PinchZoom};
package/src/index.tsx CHANGED
@@ -20,6 +20,7 @@ export * from './components/uis/Hr/Hr';
20
20
  export * from './components/uis/Icon/Icon';
21
21
  export * from './components/uis/IconButton/IconButton';
22
22
  export * from './components/uis/LoadingIndicator/LoadingIndicator';
23
+ export * from './components/uis/PinchZoom/PinchZoom';
23
24
  export * from './components/uis/Rating/Rating';
24
25
  export * from './components/uis/RadioGroup/RadioGroup';
25
26
  export * from './components/uis/StatusbarBrightness/StatusBarBrightness';