@cleartrip/ct-design-nav-tabs 4.0.0 → 5.1.0

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 (75) hide show
  1. package/README.md +69 -0
  2. package/dist/FlatTab/FlatTab.d.ts +0 -1
  3. package/dist/FlatTab/FlatTab.d.ts.map +1 -1
  4. package/dist/FlatTab/FlatTabContainer/index.d.ts +7 -0
  5. package/dist/FlatTab/FlatTabContainer/index.d.ts.map +1 -0
  6. package/dist/FlatTab/FlatTabContainer/type.d.ts +8 -0
  7. package/dist/FlatTab/FlatTabContainer/type.d.ts.map +1 -0
  8. package/dist/FlatTab/style.d.ts +5 -9
  9. package/dist/FlatTab/style.d.ts.map +1 -1
  10. package/dist/FlatTab/type.d.ts +34 -3
  11. package/dist/FlatTab/type.d.ts.map +1 -1
  12. package/dist/Tab.d.ts +5 -0
  13. package/dist/Tab.d.ts.map +1 -0
  14. package/dist/Tab.native.d.ts +5 -0
  15. package/dist/Tab.native.d.ts.map +1 -0
  16. package/dist/TabContainer/TabContainer.d.ts +4 -1
  17. package/dist/TabContainer/TabContainer.d.ts.map +1 -1
  18. package/dist/TabContainer/type.d.ts +3 -3
  19. package/dist/TabContainer/type.d.ts.map +1 -1
  20. package/dist/constants.d.ts +5 -0
  21. package/dist/constants.d.ts.map +1 -0
  22. package/dist/ct-design-nav-tabs.browser.cjs.js +1 -1
  23. package/dist/ct-design-nav-tabs.browser.cjs.js.map +1 -1
  24. package/dist/ct-design-nav-tabs.browser.esm.js +1 -1
  25. package/dist/ct-design-nav-tabs.browser.esm.js.map +1 -1
  26. package/dist/ct-design-nav-tabs.cjs.js +170 -85
  27. package/dist/ct-design-nav-tabs.cjs.js.map +1 -1
  28. package/dist/ct-design-nav-tabs.esm.js +168 -79
  29. package/dist/ct-design-nav-tabs.esm.js.map +1 -1
  30. package/dist/ct-design-nav-tabs.umd.js +168 -124
  31. package/dist/ct-design-nav-tabs.umd.js.map +1 -1
  32. package/dist/index.d.ts +3 -2
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/style.d.ts +1 -1
  35. package/dist/style.d.ts.map +1 -1
  36. package/dist/type.d.ts +18 -21
  37. package/dist/type.d.ts.map +1 -1
  38. package/package.json +31 -14
  39. package/src/FlatTab/FlatTab.tsx +163 -0
  40. package/src/FlatTab/FlatTabContainer/index.tsx +43 -0
  41. package/src/FlatTab/FlatTabContainer/type.ts +8 -0
  42. package/src/FlatTab/style.ts +53 -0
  43. package/src/FlatTab/type.ts +57 -0
  44. package/src/Tab.native.tsx +270 -0
  45. package/src/Tab.tsx +155 -0
  46. package/src/TabContainer/TabContainer.tsx +29 -0
  47. package/src/TabContainer/type.ts +7 -0
  48. package/src/constants.ts +4 -0
  49. package/src/index.ts +3 -0
  50. package/src/style.ts +8 -0
  51. package/src/type.ts +75 -0
  52. package/dist/FlatTab/StyledFlatTab/StyledFlatTab.d.ts +0 -7
  53. package/dist/FlatTab/StyledFlatTab/StyledFlatTab.d.ts.map +0 -1
  54. package/dist/FlatTab/StyledFlatTab/index.d.ts +0 -2
  55. package/dist/FlatTab/StyledFlatTab/index.d.ts.map +0 -1
  56. package/dist/FlatTab/StyledFlatTab/style.d.ts +0 -6
  57. package/dist/FlatTab/StyledFlatTab/style.d.ts.map +0 -1
  58. package/dist/FlatTab/StyledFlatTab/type.d.ts +0 -6
  59. package/dist/FlatTab/StyledFlatTab/type.d.ts.map +0 -1
  60. package/dist/FlatTab/index.d.ts +0 -2
  61. package/dist/FlatTab/index.d.ts.map +0 -1
  62. package/dist/NavTabs.d.ts +0 -5
  63. package/dist/NavTabs.d.ts.map +0 -1
  64. package/dist/StyledCounter/StyledCounter.d.ts +0 -4
  65. package/dist/StyledCounter/StyledCounter.d.ts.map +0 -1
  66. package/dist/StyledCounter/index.d.ts +0 -2
  67. package/dist/StyledCounter/index.d.ts.map +0 -1
  68. package/dist/StyledCounter/style.d.ts +0 -7
  69. package/dist/StyledCounter/style.d.ts.map +0 -1
  70. package/dist/StyledCounter/type.d.ts +0 -6
  71. package/dist/StyledCounter/type.d.ts.map +0 -1
  72. package/dist/TabContainer/index.d.ts +0 -2
  73. package/dist/TabContainer/index.d.ts.map +0 -1
  74. package/dist/TabContainer/style.d.ts +0 -7
  75. package/dist/TabContainer/style.d.ts.map +0 -1
package/package.json CHANGED
@@ -1,35 +1,50 @@
1
1
  {
2
2
  "name": "@cleartrip/ct-design-nav-tabs",
3
- "version": "4.0.0",
3
+ "version": "5.1.0",
4
4
  "description": "NavTabs Component",
5
5
  "types": "dist/index.d.ts",
6
- "main": "dist/ct-design-nav-tabs.cjs.js",
6
+ "main": "./dist/ct-design-nav-tabs.cjs.js",
7
7
  "jsnext:main": "dist/ct-design-nav-tabs.esm.js",
8
8
  "module": "dist/ct-design-nav-tabs.esm.js",
9
+ "react-native": "src/index.ts",
9
10
  "sideEffects": false,
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/ct-design-nav-tabs.esm.js",
15
+ "default": "./dist/ct-design-nav-tabs.cjs.js"
16
+ }
17
+ },
10
18
  "browser": {
11
19
  "./dist/ct-design-nav-tabs.esm.js": "./dist/ct-design-nav-tabs.browser.esm.js",
12
20
  "./dist/ct-design-nav-tabs.cjs.js": "./dist/ct-design-nav-tabs.browser.cjs.js"
13
21
  },
14
22
  "files": [
15
- "dist"
23
+ "dist",
24
+ "src"
16
25
  ],
17
26
  "dependencies": {
18
- "@cleartrip/ct-design-tokens": "4.0.0",
19
- "@cleartrip/ct-design-theme": "4.0.0",
20
- "@cleartrip/ct-design-types": "4.0.0",
21
- "@cleartrip/ct-design-container": "4.0.0",
22
- "@cleartrip/ct-design-typography": "4.0.0",
23
- "@cleartrip/ct-design-chip": "4.0.0",
24
- "@cleartrip/ct-design-horizontal-scroll": "4.0.0"
27
+ "@emotion/react": "^11.14.0",
28
+ "@emotion/styled": "^11.14.0",
29
+ "@cleartrip/ct-design-tokens": "5.1.0",
30
+ "@cleartrip/ct-design-theme": "5.1.0",
31
+ "@cleartrip/ct-design-types": "5.1.0",
32
+ "@cleartrip/ct-design-container": "5.1.0",
33
+ "@cleartrip/ct-design-typography": "5.1.0",
34
+ "@cleartrip/ct-design-chip": "5.1.0",
35
+ "@cleartrip/ct-design-id-container": "5.1.0",
36
+ "@cleartrip/ct-design-style-manager": "5.1.0",
37
+ "@cleartrip/ct-design-scroll-container": "5.1.0",
38
+ "@cleartrip/ct-design-horizontal-scroll": "5.1.0",
39
+ "@cleartrip/ct-design-use-merge-refs": "5.1.0"
25
40
  },
26
41
  "devDependencies": {
27
- "@cleartrip/ct-design-icons": "26.0.0"
42
+ "@emotion/babel-plugin": "^11.12.0",
43
+ "@cleartrip/ct-design-icons": "6.0.0"
28
44
  },
29
45
  "peerDependencies": {
30
46
  "react": ">=16.8.0",
31
- "react-dom": ">=16.8.0",
32
- "styled-components": "^5.3.6"
47
+ "react-dom": ">=16.8.0"
33
48
  },
34
49
  "publishConfig": {
35
50
  "access": "public"
@@ -40,6 +55,8 @@
40
55
  "test": "echo \"Error: no test specified\" && exit 1",
41
56
  "watch-package": "rollup -c -w",
42
57
  "build-package": "rollup -c",
43
- "build-package:clean": "rm -rf dist && rollup -c"
58
+ "build-package:clean": "rm -rf dist && rollup -c",
59
+ "publish-package:local": "yalc publish --no-scripts",
60
+ "publish-package:local:registry": "pnpm publish --registry http://localhost:4873 --no-git-checks --access public"
44
61
  }
45
62
  }
@@ -0,0 +1,163 @@
1
+ import { useTheme } from '@cleartrip/ct-design-theme';
2
+ import { makeStyles, useStyles } from '@cleartrip/ct-design-style-manager';
3
+ import { Container } from '@cleartrip/ct-design-container';
4
+ import { Typography } from '@cleartrip/ct-design-typography';
5
+ import { TypographyColor } from '@cleartrip/ct-design-typography';
6
+ import { IDContainer } from '@cleartrip/ct-design-id-container';
7
+
8
+ import { FlatTabProps } from './type';
9
+ import { TabSize, IconPosition } from './type';
10
+ import { getLabelVariant, getTabLabelColor, getTabStyles } from './style';
11
+ import FlatTabContainer from './FlatTabContainer';
12
+
13
+ const styles = makeStyles((theme) => {
14
+ return {
15
+ root: {
16
+ position: 'relative',
17
+ },
18
+ tabContainer: {
19
+ flexDirection: 'row',
20
+ alignItems: 'center',
21
+ justifyContent: 'center',
22
+ },
23
+ iconContainer: {
24
+ flexDirection: 'row',
25
+ alignItems: 'center',
26
+ justifyContent: 'center',
27
+ },
28
+ countContainer: {
29
+ flexDirection: 'row',
30
+ alignItems: 'center',
31
+ justifyContent: 'center',
32
+ marginLeft: theme.spacing[1],
33
+ marginRight: theme.spacing[1],
34
+ height: theme.size[4],
35
+ width: theme.size[4],
36
+ },
37
+ countSubContainer: {
38
+ flexDirection: 'row',
39
+ alignItems: 'center',
40
+ justifyContent: 'center',
41
+ padding: theme.spacing['0.5'],
42
+ width: '100%',
43
+ borderRadius: theme.border.radius[32],
44
+ },
45
+ pr2: {
46
+ paddingRight: theme.spacing[2],
47
+ },
48
+ pl2: {
49
+ paddingLeft: theme.spacing[2],
50
+ },
51
+ };
52
+ });
53
+
54
+ const FlatTab: React.FC<FlatTabProps> = ({
55
+ label,
56
+ size = TabSize.SMALL,
57
+ showIcon = false,
58
+ iconPosition = IconPosition.LEFT,
59
+ Icon = undefined,
60
+ showCounter = false,
61
+ count = -1,
62
+ isSelected = false,
63
+ onClick,
64
+ tabWidth,
65
+ counterBgColor,
66
+ nonSelectedCounterBgColor,
67
+ styleConfig = {},
68
+ id,
69
+ onLayout,
70
+ hideBottomBorder = false,
71
+ }) => {
72
+ const theme = useTheme();
73
+ const {
74
+ containerWrapper: customContainerWrapper = [],
75
+ countContainer: customCountContainer = [],
76
+ countSubContainer: customCountSubContainer = [],
77
+ countTypography: customCountTypographyStyles = [],
78
+ iconContainer: customIconContainerStyles = [],
79
+ labelTypography: customLabelStyles = [],
80
+ root: customRootStyles = [],
81
+ tabContainer: customTabContainer = [],
82
+ selectedRoot: customSelectedTab = [],
83
+ } = styleConfig;
84
+ const tabStyles = getTabStyles({ isSelected, theme, tabWidth });
85
+ const showCountPill = showCounter && count > 0;
86
+
87
+ const dynmaicStyles = useStyles(() => {
88
+ return {
89
+ countSubContainer: {
90
+ backgroundColor: isSelected ? counterBgColor : nonSelectedCounterBgColor,
91
+ },
92
+ };
93
+ }, [isSelected]);
94
+
95
+ const renderLabelComponent = () => {
96
+ if (typeof label === 'string') {
97
+ return (
98
+ <Typography
99
+ variant={getLabelVariant({ size })}
100
+ colorCode={getTabLabelColor({ isSelected, theme })}
101
+ styleConfig={{ root: customLabelStyles }}
102
+ >
103
+ {label}
104
+ </Typography>
105
+ );
106
+ }
107
+ return label;
108
+ };
109
+
110
+ return (
111
+ <IDContainer id={id} onLayout={onLayout}>
112
+ <Container
113
+ onClick={onClick}
114
+ id={id}
115
+ styleConfig={{ root: [styles.root, ...customRootStyles, ...(isSelected ? customSelectedTab : [])] }}
116
+ >
117
+ <FlatTabContainer
118
+ width={tabStyles?.width}
119
+ borderBottomWidth={hideBottomBorder ? 0 : tabStyles?.borderBottomWidth}
120
+ borderBottomColor={hideBottomBorder ? 'transparent' : tabStyles?.borderBottomColor}
121
+ rootStyles={customContainerWrapper}
122
+ >
123
+ <Container
124
+ styleConfig={{
125
+ root: [styles.tabContainer, ...customTabContainer],
126
+ }}
127
+ >
128
+ {Icon && showIcon && iconPosition === IconPosition.LEFT && (
129
+ <Container styleConfig={{ root: [styles.iconContainer, styles.pr2, ...customIconContainerStyles] }}>
130
+ {Icon}
131
+ </Container>
132
+ )}
133
+ {renderLabelComponent()}
134
+ {Icon && showIcon && iconPosition === IconPosition.RIGHT && (
135
+ <Container styleConfig={{ root: [styles.iconContainer, styles.pl2, ...customIconContainerStyles] }}>
136
+ {Icon}
137
+ </Container>
138
+ )}
139
+ {showCountPill && (
140
+ <Container styleConfig={{ root: [styles.countContainer, ...customCountContainer] }}>
141
+ <Container
142
+ styleConfig={{
143
+ root: [styles.countSubContainer, dynmaicStyles.countSubContainer, ...customCountSubContainer],
144
+ }}
145
+ >
146
+ <Typography
147
+ variant={'TAG'}
148
+ color={TypographyColor.NEUTRAL}
149
+ styleConfig={{ root: customCountTypographyStyles }}
150
+ >
151
+ {count}
152
+ </Typography>
153
+ </Container>
154
+ </Container>
155
+ )}
156
+ </Container>
157
+ </FlatTabContainer>
158
+ </Container>
159
+ </IDContainer>
160
+ );
161
+ };
162
+
163
+ export default FlatTab;
@@ -0,0 +1,43 @@
1
+ import { Styles } from '@cleartrip/ct-design-types';
2
+ import { Container } from '@cleartrip/ct-design-container';
3
+ import { makeStyles, useStyles } from '@cleartrip/ct-design-style-manager';
4
+
5
+ import { StyledFlatTabProps } from './type';
6
+
7
+ const styles = makeStyles((theme) => {
8
+ return {
9
+ root: {
10
+ padding: theme.spacing[2],
11
+ flexDirection: 'row',
12
+ alignTtems: 'center',
13
+ justifyContent: 'center',
14
+ whiteSpace: 'nowrap',
15
+ },
16
+ };
17
+ });
18
+
19
+ const FlatTabContainer: React.FC<StyledFlatTabProps & { rootStyles: Styles[] }> = ({
20
+ width,
21
+ borderBottomColor,
22
+ borderBottomWidth,
23
+ children,
24
+ rootStyles: customRootStyles = [],
25
+ }) => {
26
+ const dynamicStyles = useStyles(
27
+ (_) => {
28
+ return {
29
+ root: {
30
+ width: width,
31
+ borderBottomWidth: borderBottomWidth,
32
+ borderBottomColor: borderBottomColor,
33
+ },
34
+ };
35
+ },
36
+ [borderBottomColor, borderBottomWidth, width],
37
+ );
38
+ return (
39
+ <Container styleConfig={{ root: [styles.root, dynamicStyles.root, ...customRootStyles] }}>{children}</Container>
40
+ );
41
+ };
42
+
43
+ export default FlatTabContainer;
@@ -0,0 +1,8 @@
1
+ import { ReactNode } from 'react';
2
+
3
+ export interface StyledFlatTabProps {
4
+ borderBottomWidth?: number;
5
+ borderBottomColor?: string;
6
+ width?: number | '100%' | 'auto';
7
+ children?: ReactNode;
8
+ }
@@ -0,0 +1,53 @@
1
+ import { TypographyVariant } from '@cleartrip/ct-design-typography';
2
+ import { Theme } from '@cleartrip/ct-design-theme';
3
+
4
+ import { TabSize, TabSizeType } from './type';
5
+
6
+ export const getLabelVariant = ({ size }: { size: TabSizeType }) => {
7
+ switch (size) {
8
+ case TabSize.SMALL: {
9
+ return TypographyVariant.B2;
10
+ }
11
+ case TabSize.MEDIUM: {
12
+ return TypographyVariant.B1;
13
+ }
14
+ case TabSize.LARGE: {
15
+ return TypographyVariant.B1;
16
+ }
17
+ default: {
18
+ return TypographyVariant.B2;
19
+ }
20
+ }
21
+ };
22
+
23
+ export const getTabLabelColor = ({ isSelected, theme }: { isSelected: boolean; theme: Theme }) => {
24
+ if (isSelected) {
25
+ return theme.color.tab.selectedPrimaryLabel;
26
+ } else {
27
+ return theme.color.tab.nonSelectedPrimaryLabel;
28
+ }
29
+ };
30
+
31
+ export const getTabStyles = ({
32
+ isSelected,
33
+ theme,
34
+ tabWidth,
35
+ }: {
36
+ isSelected: boolean;
37
+ theme: Theme;
38
+ tabWidth?: number | 'auto';
39
+ }) => {
40
+ if (isSelected) {
41
+ return {
42
+ borderBottomColor: theme.color.border.primary,
43
+ borderBottomWidth: theme.border.width.lg,
44
+ width: tabWidth || 'auto',
45
+ };
46
+ } else {
47
+ return {
48
+ borderBottomWidth: theme.border.width.lg,
49
+ width: tabWidth || 'auto',
50
+ borderBottomColor: theme.color.border.neutral100,
51
+ };
52
+ }
53
+ };
@@ -0,0 +1,57 @@
1
+ import { ReactNode } from 'react';
2
+
3
+ import { Styles, TextStyle } from '@cleartrip/ct-design-types';
4
+
5
+ export const enum IconPosition {
6
+ LEFT = 'left',
7
+ RIGHT = 'right',
8
+ TOP = 'top',
9
+ }
10
+
11
+ /** Available tab size variants */
12
+ export enum TabSize {
13
+ SMALL = 'small',
14
+ MEDIUM = 'medium',
15
+ LARGE = 'large',
16
+ }
17
+
18
+ /** String literal type for tab sizes */
19
+ export type TabSizeType = `${TabSize}`;
20
+
21
+ export interface FlatTabStyleConfigProps {
22
+ root?: Styles[];
23
+ containerWrapper?: Styles[];
24
+ tabContainer?: Styles[];
25
+ iconContainer?: Styles[];
26
+ labelTypography?: TextStyle[];
27
+ countContainer?: Styles[];
28
+ countSubContainer?: Styles[];
29
+ countTypography?: TextStyle[];
30
+ selectedRoot?: Styles[];
31
+ }
32
+
33
+ export interface FlatTabProps {
34
+ size?: TabSizeType;
35
+ label: string;
36
+ showIcon?: boolean;
37
+ iconPosition?: `${IconPosition}`;
38
+ Icon?: ReactNode;
39
+ showCounter?: boolean;
40
+ count?: number;
41
+ isSelected?: boolean;
42
+ onClick: () => void;
43
+ tabWidth?: number | 'auto';
44
+ counterBgColor?: string;
45
+ nonSelectedCounterBgColor?: string;
46
+ styleConfig?: FlatTabStyleConfigProps;
47
+ id?: string;
48
+ onLayout?: (event: {
49
+ layout: {
50
+ height: number;
51
+ width: number;
52
+ x: number;
53
+ y: number;
54
+ };
55
+ }) => void;
56
+ hideBottomBorder?: boolean;
57
+ }
@@ -0,0 +1,270 @@
1
+ import { forwardRef, useCallback, useEffect, useRef } from 'react';
2
+ import Animated, { useSharedValue, useAnimatedStyle, withTiming, Easing } from 'react-native-reanimated';
3
+
4
+ import { IconPosition } from '@cleartrip/ct-design-types';
5
+ import { useTheme } from '@cleartrip/ct-design-theme';
6
+ import { makeStyles, useStyles } from '@cleartrip/ct-design-style-manager';
7
+ import { Container } from '@cleartrip/ct-design-container';
8
+ import { HorizontalScroll } from '@cleartrip/ct-design-horizontal-scroll';
9
+ import { Chip } from '@cleartrip/ct-design-chip';
10
+
11
+ import { getTabContainerStyles } from './style';
12
+ import TabContainer from './TabContainer/TabContainer';
13
+ import FlatTab from './FlatTab/FlatTab';
14
+ import { NavTabsProps } from './type';
15
+ import { TabVariants } from './constants';
16
+ import { ScrollContainerRef } from '@cleartrip/ct-design-scroll-container';
17
+ import { TabSize } from './FlatTab/type';
18
+
19
+ const SCROLL_PADDING = 32;
20
+
21
+ const staticStyles = makeStyles((theme) => ({
22
+ scrollContainer: {
23
+ backgroundColor: 'white',
24
+ },
25
+ root: {
26
+ width: '100%',
27
+ },
28
+ indicator: {
29
+ position: 'absolute',
30
+ bottom: 0,
31
+ left: 0,
32
+ height: theme.border.width.lg,
33
+ backgroundColor: theme.color.border.primary,
34
+ },
35
+ }));
36
+
37
+ const NavTabs = forwardRef<ScrollContainerRef, NavTabsProps>(
38
+ ({
39
+ tabType = TabVariants.FLAT,
40
+ tabList,
41
+ selectedTab,
42
+ showIcon = false,
43
+ tabGap,
44
+ onClick,
45
+ iconPosition = IconPosition.LEFT,
46
+ tabSize = TabSize.SMALL,
47
+ showCounter = false,
48
+ counterBgColor,
49
+ nonSelectedCounterBgColor,
50
+ containerSpace,
51
+ tabWidth,
52
+ showBottomBorder = true,
53
+ styleConfig = {},
54
+ navTabsIdPrefix = '',
55
+ rootProps,
56
+ }) => {
57
+ const theme = useTheme();
58
+ const { chip, container = [], flatTab, root, tabContainer = [], tabTypeContainer = [] } = styleConfig;
59
+ const showBottomLine = showBottomBorder && tabType === TabVariants.FLAT;
60
+ const tabContainerStyles = getTabContainerStyles({ showBottomLine, theme });
61
+
62
+ const _counterBgColor = counterBgColor || theme.color.background.link2;
63
+ const _nonSelectedCounterBgColor = nonSelectedCounterBgColor || theme.color.background.defaultDark;
64
+
65
+ // Layout and scroll state
66
+ const tabPositionsRef = useRef<Array<{ x: number; width: number } | undefined>>([]);
67
+ const containerWidthRef = useRef(0);
68
+ const scrollOffsetRef = useRef(0);
69
+ const prevScrollXRef = useRef(0);
70
+ const isAutoScrollingRef = useRef(false);
71
+ const internalScrollRef = useRef<ScrollContainerRef>(null);
72
+ const scrollEndTimerRef = useRef<NodeJS.Timeout>(undefined);
73
+
74
+ // Reanimated shared values for the sliding indicator (both run on UI thread)
75
+ const indicatorX = useSharedValue(0);
76
+ const indicatorWidth = useSharedValue(0);
77
+ const indicatorOpacity = useSharedValue(0);
78
+
79
+ // Keep effective padding current without re-creating callbacks
80
+ const effectivePaddingRef = useRef(containerSpace ?? theme.spacing[4]);
81
+ effectivePaddingRef.current = containerSpace ?? theme.spacing[4];
82
+
83
+ const easeInConfig = { duration: 250, easing: Easing.in(Easing.cubic) };
84
+
85
+ const animateIndicatorToIndex = useCallback(
86
+ (index: number) => {
87
+ const pos = tabPositionsRef.current[index];
88
+ if (!pos) return;
89
+ indicatorX.value = withTiming(effectivePaddingRef.current + pos.x, easeInConfig);
90
+ indicatorWidth.value = withTiming(pos.width, easeInConfig);
91
+ if (indicatorOpacity.value === 0) {
92
+ indicatorOpacity.value = withTiming(1, { duration: 150 });
93
+ }
94
+ },
95
+ // eslint-disable-next-line react-hooks/exhaustive-deps
96
+ [indicatorX, indicatorWidth, indicatorOpacity],
97
+ );
98
+
99
+ const indicatorStyle = useAnimatedStyle(() => ({
100
+ transform: [{ translateX: indicatorX.value - 16 }],
101
+ width: indicatorWidth.value,
102
+ opacity: indicatorOpacity.value,
103
+ }));
104
+
105
+ // Auto-scroll selected tab into view and animate the indicator
106
+ useEffect(() => {
107
+ if (!selectedTab) return;
108
+ const index = tabList.findIndex((t) => t.id === selectedTab);
109
+ if (index < 0) return;
110
+
111
+ animateIndicatorToIndex(index);
112
+
113
+ const pos = tabPositionsRef.current[index];
114
+ if (!pos || containerWidthRef.current === 0) return;
115
+
116
+ const absoluteX = effectivePaddingRef.current + pos.x;
117
+ const scrollX = scrollOffsetRef.current;
118
+ const visibleWidth = containerWidthRef.current;
119
+
120
+ if (absoluteX < scrollX + SCROLL_PADDING) {
121
+ isAutoScrollingRef.current = true;
122
+ internalScrollRef.current?.scrollTo?.(Math.max(0, absoluteX - SCROLL_PADDING), 0);
123
+ setTimeout(() => {
124
+ isAutoScrollingRef.current = false;
125
+ }, 500);
126
+ } else if (absoluteX + pos.width > scrollX + visibleWidth - SCROLL_PADDING) {
127
+ isAutoScrollingRef.current = true;
128
+ internalScrollRef.current?.scrollTo?.(absoluteX + pos.width - visibleWidth + SCROLL_PADDING, 0);
129
+ setTimeout(() => {
130
+ isAutoScrollingRef.current = false;
131
+ }, 500);
132
+ }
133
+ }, [selectedTab, tabList, animateIndicatorToIndex]);
134
+
135
+ // Cleanup scroll-end timer on unmount
136
+ useEffect(() => {
137
+ return () => {
138
+ // eslint-disable-next-line react-hooks/exhaustive-deps
139
+ clearTimeout(scrollEndTimerRef.current);
140
+ };
141
+ }, []);
142
+
143
+ // Track scroll offset and smart-select the edge visible tab after scroll settles
144
+ const handleScroll = useCallback((event: { nativeEvent: { contentOffset: { x: number } } }) => {
145
+ const x = event.nativeEvent.contentOffset.x;
146
+ prevScrollXRef.current = x;
147
+ scrollOffsetRef.current = x;
148
+
149
+ if (isAutoScrollingRef.current) return;
150
+
151
+ clearTimeout(scrollEndTimerRef.current);
152
+ }, []);
153
+
154
+ // Record each tab's position and width; trigger indicator once positions are known
155
+ const handleTabLayout = useCallback(
156
+ (index: number, event: { layout: { x: number; width: number } }) => {
157
+ tabPositionsRef.current[index] = { x: event.layout.x, width: event.layout.width };
158
+
159
+ const selectedIndex = tabList.findIndex((t) => t.id === selectedTab);
160
+ if (index === selectedIndex) {
161
+ animateIndicatorToIndex(index);
162
+ }
163
+ },
164
+ [tabList, selectedTab, animateIndicatorToIndex],
165
+ );
166
+
167
+ const renderChipTab = () => {
168
+ return tabList.map((tab) => {
169
+ const { label, id, Icon = undefined, count } = tab || {};
170
+ return (
171
+ <Chip
172
+ label={label}
173
+ key={id}
174
+ isSelected={selectedTab === id}
175
+ {...{
176
+ prefixIcon: iconPosition === IconPosition.LEFT ? Icon : null,
177
+ topIcon: iconPosition === IconPosition.TOP ? Icon : null,
178
+ suffixIcon: iconPosition === IconPosition.RIGHT ? Icon : null,
179
+ }}
180
+ onClick={() => onClick(id)}
181
+ showCounter={showCounter}
182
+ count={count}
183
+ styleConfig={chip}
184
+ id={navTabsIdPrefix + id}
185
+ />
186
+ );
187
+ });
188
+ };
189
+
190
+ const renderFlatTab = () => {
191
+ return tabList.map((tab, index) => {
192
+ const { label, id, Icon = undefined, count } = tab || {};
193
+ return (
194
+ <FlatTab
195
+ key={id}
196
+ label={label}
197
+ onClick={() => onClick(id)}
198
+ size={tabSize}
199
+ isSelected={selectedTab === id}
200
+ showCounter={showCounter}
201
+ count={count}
202
+ counterBgColor={_counterBgColor}
203
+ nonSelectedCounterBgColor={_nonSelectedCounterBgColor}
204
+ showIcon={showIcon}
205
+ iconPosition={iconPosition}
206
+ Icon={Icon}
207
+ tabWidth={tabWidth}
208
+ styleConfig={flatTab}
209
+ id={navTabsIdPrefix + id}
210
+ onLayout={(event) => handleTabLayout(index, event)}
211
+ hideBottomBorder
212
+ />
213
+ );
214
+ });
215
+ };
216
+
217
+ const renderTabType = () => {
218
+ switch (tabType) {
219
+ case TabVariants.FLAT:
220
+ return renderFlatTab();
221
+ case TabVariants.CHIP:
222
+ return renderChipTab();
223
+ default:
224
+ return renderFlatTab();
225
+ }
226
+ };
227
+
228
+ const dynamicStyles = useStyles(
229
+ (theme) => ({
230
+ tabTypeContainer: {
231
+ flexDirection: 'row',
232
+ alignItems: 'center',
233
+ columnGap: tabGap || theme?.spacing[4],
234
+ paddingLeft: containerSpace || theme?.spacing[4],
235
+ paddingRight: containerSpace || theme?.spacing[4],
236
+ },
237
+ }),
238
+ [tabGap, containerSpace],
239
+ );
240
+
241
+ return (
242
+ <HorizontalScroll
243
+ {...rootProps}
244
+ styleConfig={{ childContainer: [...(root || []), staticStyles.scrollContainer] }}
245
+ ref={internalScrollRef}
246
+ onLayout={(event) => {
247
+ containerWidthRef.current = event.layout.width;
248
+ }}
249
+ onScroll={(event) => {
250
+ handleScroll(event);
251
+ }}
252
+ scrollEventThrottle={16}
253
+ >
254
+ <Container styleConfig={{ root: [staticStyles.root, ...container] }}>
255
+ <TabContainer {...tabContainerStyles} rootStyles={tabContainer}>
256
+ <Container styleConfig={{ root: [dynamicStyles.tabTypeContainer, ...tabTypeContainer] }}>
257
+ {renderTabType()}
258
+ {tabType === TabVariants.FLAT && (
259
+ <Animated.View pointerEvents='none' style={[staticStyles.indicator, indicatorStyle]} />
260
+ )}
261
+ </Container>
262
+ </TabContainer>
263
+ </Container>
264
+ </HorizontalScroll>
265
+ );
266
+ },
267
+ );
268
+
269
+ NavTabs.displayName = 'NavTabs';
270
+ export default NavTabs;