@luanthnh/cntt-ui 0.1.5

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 (255) hide show
  1. package/.storybook/globals.d.ts +1 -0
  2. package/.storybook/main.ts +29 -0
  3. package/.storybook/preview.ts +32 -0
  4. package/README.md +86 -0
  5. package/assets/fonts/Montserrat-Black.eot +0 -0
  6. package/assets/fonts/Montserrat-Black.ttf +0 -0
  7. package/assets/fonts/Montserrat-Black.woff +0 -0
  8. package/assets/fonts/Montserrat-Black.woff2 +0 -0
  9. package/assets/fonts/Montserrat-BlackItalic.eot +0 -0
  10. package/assets/fonts/Montserrat-BlackItalic.ttf +0 -0
  11. package/assets/fonts/Montserrat-BlackItalic.woff +0 -0
  12. package/assets/fonts/Montserrat-BlackItalic.woff2 +0 -0
  13. package/assets/fonts/Montserrat-Bold.eot +0 -0
  14. package/assets/fonts/Montserrat-Bold.ttf +0 -0
  15. package/assets/fonts/Montserrat-Bold.woff +0 -0
  16. package/assets/fonts/Montserrat-Bold.woff2 +0 -0
  17. package/assets/fonts/Montserrat-BoldItalic.eot +0 -0
  18. package/assets/fonts/Montserrat-BoldItalic.ttf +0 -0
  19. package/assets/fonts/Montserrat-BoldItalic.woff +0 -0
  20. package/assets/fonts/Montserrat-BoldItalic.woff2 +0 -0
  21. package/assets/fonts/Montserrat-ExtraBold.eot +0 -0
  22. package/assets/fonts/Montserrat-ExtraBold.ttf +0 -0
  23. package/assets/fonts/Montserrat-ExtraBold.woff +0 -0
  24. package/assets/fonts/Montserrat-ExtraBold.woff2 +0 -0
  25. package/assets/fonts/Montserrat-ExtraBoldItalic.eot +0 -0
  26. package/assets/fonts/Montserrat-ExtraBoldItalic.ttf +0 -0
  27. package/assets/fonts/Montserrat-ExtraBoldItalic.woff +0 -0
  28. package/assets/fonts/Montserrat-ExtraBoldItalic.woff2 +0 -0
  29. package/assets/fonts/Montserrat-ExtraLight.eot +0 -0
  30. package/assets/fonts/Montserrat-ExtraLight.ttf +0 -0
  31. package/assets/fonts/Montserrat-ExtraLight.woff +0 -0
  32. package/assets/fonts/Montserrat-ExtraLight.woff2 +0 -0
  33. package/assets/fonts/Montserrat-ExtraLightItalic.eot +0 -0
  34. package/assets/fonts/Montserrat-ExtraLightItalic.ttf +0 -0
  35. package/assets/fonts/Montserrat-ExtraLightItalic.woff +0 -0
  36. package/assets/fonts/Montserrat-ExtraLightItalic.woff2 +0 -0
  37. package/assets/fonts/Montserrat-Italic.eot +0 -0
  38. package/assets/fonts/Montserrat-Italic.ttf +0 -0
  39. package/assets/fonts/Montserrat-Italic.woff +0 -0
  40. package/assets/fonts/Montserrat-Italic.woff2 +0 -0
  41. package/assets/fonts/Montserrat-Light.eot +0 -0
  42. package/assets/fonts/Montserrat-Light.ttf +0 -0
  43. package/assets/fonts/Montserrat-Light.woff +0 -0
  44. package/assets/fonts/Montserrat-Light.woff2 +0 -0
  45. package/assets/fonts/Montserrat-LightItalic.eot +0 -0
  46. package/assets/fonts/Montserrat-LightItalic.ttf +0 -0
  47. package/assets/fonts/Montserrat-LightItalic.woff +0 -0
  48. package/assets/fonts/Montserrat-LightItalic.woff2 +0 -0
  49. package/assets/fonts/Montserrat-Medium.eot +0 -0
  50. package/assets/fonts/Montserrat-Medium.ttf +0 -0
  51. package/assets/fonts/Montserrat-Medium.woff +0 -0
  52. package/assets/fonts/Montserrat-Medium.woff2 +0 -0
  53. package/assets/fonts/Montserrat-MediumItalic.eot +0 -0
  54. package/assets/fonts/Montserrat-MediumItalic.ttf +0 -0
  55. package/assets/fonts/Montserrat-MediumItalic.woff +0 -0
  56. package/assets/fonts/Montserrat-MediumItalic.woff2 +0 -0
  57. package/assets/fonts/Montserrat-Regular.eot +0 -0
  58. package/assets/fonts/Montserrat-Regular.ttf +0 -0
  59. package/assets/fonts/Montserrat-Regular.woff +0 -0
  60. package/assets/fonts/Montserrat-Regular.woff2 +0 -0
  61. package/assets/fonts/Montserrat-SemiBold.eot +0 -0
  62. package/assets/fonts/Montserrat-SemiBold.ttf +0 -0
  63. package/assets/fonts/Montserrat-SemiBold.woff +0 -0
  64. package/assets/fonts/Montserrat-SemiBold.woff2 +0 -0
  65. package/assets/fonts/Montserrat-SemiBoldItalic.eot +0 -0
  66. package/assets/fonts/Montserrat-SemiBoldItalic.ttf +0 -0
  67. package/assets/fonts/Montserrat-SemiBoldItalic.woff +0 -0
  68. package/assets/fonts/Montserrat-SemiBoldItalic.woff2 +0 -0
  69. package/assets/fonts/Montserrat-Thin.eot +0 -0
  70. package/assets/fonts/Montserrat-Thin.ttf +0 -0
  71. package/assets/fonts/Montserrat-Thin.woff +0 -0
  72. package/assets/fonts/Montserrat-Thin.woff2 +0 -0
  73. package/assets/fonts/Montserrat-ThinItalic.eot +0 -0
  74. package/assets/fonts/Montserrat-ThinItalic.ttf +0 -0
  75. package/assets/fonts/Montserrat-ThinItalic.woff +0 -0
  76. package/assets/fonts/Montserrat-ThinItalic.woff2 +0 -0
  77. package/assets/fonts/Montserrat-Variable.eot +0 -0
  78. package/assets/fonts/Montserrat-Variable.ttf +0 -0
  79. package/assets/fonts/Montserrat-Variable.woff +0 -0
  80. package/assets/fonts/Montserrat-Variable.woff2 +0 -0
  81. package/assets/fonts/Montserrat-VariableItalic.eot +0 -0
  82. package/assets/fonts/Montserrat-VariableItalic.ttf +0 -0
  83. package/assets/fonts/Montserrat-VariableItalic.woff +0 -0
  84. package/assets/fonts/Montserrat-VariableItalic.woff2 +0 -0
  85. package/assets/icons/arrow-left.svg +1 -0
  86. package/assets/icons/file.svg +1 -0
  87. package/assets/icons/globe.svg +1 -0
  88. package/assets/icons/logo-line.svg +1 -0
  89. package/assets/icons/next.svg +1 -0
  90. package/assets/icons/panel-left-expand.svg +1 -0
  91. package/assets/icons/placeholder.svg +57 -0
  92. package/assets/icons/vercel.svg +1 -0
  93. package/assets/icons/window.svg +1 -0
  94. package/assets/lotties/error-404.json +19642 -0
  95. package/assets/lotties/error.json +2414 -0
  96. package/assets/lotties/loader.json +305 -0
  97. package/components/Welcome.mdx +74 -0
  98. package/components/lenis/index.tsx +48 -0
  99. package/components/motion/auto-height.tsx +56 -0
  100. package/components/motion/cursor.tsx +108 -0
  101. package/components/motion/highlight.tsx +605 -0
  102. package/components/motion/number-ticker.tsx +55 -0
  103. package/components/motion/slot.tsx +106 -0
  104. package/components/motion/waves.tsx +417 -0
  105. package/components/primitives/tabs.tsx +174 -0
  106. package/components/ui/Accordion/index.stories.tsx +39 -0
  107. package/components/ui/Accordion/index.tsx +170 -0
  108. package/components/ui/Alert/index.stories.tsx +39 -0
  109. package/components/ui/Alert/index.tsx +60 -0
  110. package/components/ui/AlertDialog/index.stories.tsx +47 -0
  111. package/components/ui/AlertDialog/index.tsx +172 -0
  112. package/components/ui/AspectRatio/index.stories.tsx +40 -0
  113. package/components/ui/AspectRatio/index.tsx +9 -0
  114. package/components/ui/Avatar/index.stories.tsx +39 -0
  115. package/components/ui/Avatar/index.tsx +44 -0
  116. package/components/ui/Badge/index.stories.tsx +64 -0
  117. package/components/ui/Badge/index.tsx +46 -0
  118. package/components/ui/Breadcrumb/index.stories.tsx +64 -0
  119. package/components/ui/Breadcrumb/index.tsx +102 -0
  120. package/components/ui/Button/index.stories.tsx +232 -0
  121. package/components/ui/Button/index.tsx +114 -0
  122. package/components/ui/Calendar/index.stories.tsx +20 -0
  123. package/components/ui/Calendar/index.tsx +149 -0
  124. package/components/ui/Card/index.stories.tsx +39 -0
  125. package/components/ui/Card/index.tsx +65 -0
  126. package/components/ui/Carousel/index.stories.tsx +37 -0
  127. package/components/ui/Carousel/index.tsx +242 -0
  128. package/components/ui/Chart/index.stories.tsx +53 -0
  129. package/components/ui/Chart/index.tsx +322 -0
  130. package/components/ui/Checkbox/index.stories.tsx +56 -0
  131. package/components/ui/Checkbox/index.tsx +167 -0
  132. package/components/ui/CircleProcess/index.stories.tsx +29 -0
  133. package/components/ui/CircleProcess/index.tsx +50 -0
  134. package/components/ui/Collapsible/index.stories.tsx +33 -0
  135. package/components/ui/Collapsible/index.tsx +124 -0
  136. package/components/ui/Command/index.stories.tsx +65 -0
  137. package/components/ui/Command/index.tsx +161 -0
  138. package/components/ui/Container/index.stories.tsx +22 -0
  139. package/components/ui/Container/index.tsx +30 -0
  140. package/components/ui/ContextMenu/index.stories.tsx +51 -0
  141. package/components/ui/ContextMenu/index.tsx +224 -0
  142. package/components/ui/Dialog/index.stories.tsx +44 -0
  143. package/components/ui/Dialog/index.tsx +156 -0
  144. package/components/ui/Drawer/index.stories.tsx +54 -0
  145. package/components/ui/Drawer/index.tsx +124 -0
  146. package/components/ui/DropdownMenu/index.stories.tsx +83 -0
  147. package/components/ui/DropdownMenu/index.tsx +231 -0
  148. package/components/ui/Dropzone/index.stories.tsx +18 -0
  149. package/components/ui/Dropzone/index.tsx +47 -0
  150. package/components/ui/Form/date-field.tsx +77 -0
  151. package/components/ui/Form/index.stories.tsx +67 -0
  152. package/components/ui/Form/index.tsx +188 -0
  153. package/components/ui/Form/select-field.tsx +55 -0
  154. package/components/ui/Form/text-area-field.tsx +37 -0
  155. package/components/ui/Form/text-field.tsx +72 -0
  156. package/components/ui/HStack/index.stories.tsx +48 -0
  157. package/components/ui/HStack/index.tsx +73 -0
  158. package/components/ui/HoverCard/index.stories.tsx +38 -0
  159. package/components/ui/HoverCard/index.tsx +38 -0
  160. package/components/ui/Icons/index.stories.tsx +27 -0
  161. package/components/ui/Icons/index.tsx +33 -0
  162. package/components/ui/ImageWithFallback/index.stories.tsx +32 -0
  163. package/components/ui/ImageWithFallback/index.tsx +34 -0
  164. package/components/ui/Input/index.stories.tsx +47 -0
  165. package/components/ui/Input/index.tsx +21 -0
  166. package/components/ui/InputOtp/index.stories.tsx +35 -0
  167. package/components/ui/InputOtp/index.tsx +70 -0
  168. package/components/ui/Label/index.stories.tsx +18 -0
  169. package/components/ui/Label/index.tsx +21 -0
  170. package/components/ui/Marquee/index.stories.tsx +71 -0
  171. package/components/ui/Marquee/index.tsx +65 -0
  172. package/components/ui/Menubar/index.stories.tsx +116 -0
  173. package/components/ui/Menubar/index.tsx +252 -0
  174. package/components/ui/NavigationMenu/index.stories.tsx +112 -0
  175. package/components/ui/NavigationMenu/index.tsx +185 -0
  176. package/components/ui/NoData/index.stories.tsx +24 -0
  177. package/components/ui/NoData/index.tsx +19 -0
  178. package/components/ui/Pagination/index.stories.tsx +53 -0
  179. package/components/ui/Pagination/index.tsx +114 -0
  180. package/components/ui/Popover/index.stories.tsx +31 -0
  181. package/components/ui/Popover/index.tsx +42 -0
  182. package/components/ui/Progress/index.stories.tsx +35 -0
  183. package/components/ui/Progress/index.tsx +28 -0
  184. package/components/ui/RadioGroup/index.stories.tsx +28 -0
  185. package/components/ui/RadioGroup/index.tsx +45 -0
  186. package/components/ui/Resizable/index.stories.tsx +44 -0
  187. package/components/ui/Resizable/index.tsx +54 -0
  188. package/components/ui/ScrollArea/index.stories.tsx +31 -0
  189. package/components/ui/ScrollArea/index.tsx +56 -0
  190. package/components/ui/Select/index.stories.tsx +64 -0
  191. package/components/ui/Select/index.tsx +170 -0
  192. package/components/ui/Separator/index.stories.tsx +31 -0
  193. package/components/ui/Separator/index.tsx +28 -0
  194. package/components/ui/Sheet/index.stories.tsx +45 -0
  195. package/components/ui/Sheet/index.tsx +130 -0
  196. package/components/ui/Sidebar/index.stories.tsx +82 -0
  197. package/components/ui/Sidebar/index.tsx +676 -0
  198. package/components/ui/Skeleton/index.stories.tsx +36 -0
  199. package/components/ui/Skeleton/index.tsx +13 -0
  200. package/components/ui/Slider/index.stories.tsx +48 -0
  201. package/components/ui/Slider/index.tsx +82 -0
  202. package/components/ui/Slot/index.stories.tsx +29 -0
  203. package/components/ui/Slot/index.tsx +106 -0
  204. package/components/ui/Sonner/index.stories.tsx +36 -0
  205. package/components/ui/Sonner/index.tsx +31 -0
  206. package/components/ui/Switch/index.stories.tsx +33 -0
  207. package/components/ui/Switch/index.tsx +28 -0
  208. package/components/ui/Table/index.stories.tsx +74 -0
  209. package/components/ui/Table/index.tsx +95 -0
  210. package/components/ui/Tabs/index.stories.tsx +38 -0
  211. package/components/ui/Tabs/index.tsx +78 -0
  212. package/components/ui/Text/index.stories.tsx +53 -0
  213. package/components/ui/Text/index.tsx +138 -0
  214. package/components/ui/Textarea/index.stories.tsx +25 -0
  215. package/components/ui/Textarea/index.tsx +18 -0
  216. package/components/ui/Toggle/index.stories.tsx +52 -0
  217. package/components/ui/Toggle/index.tsx +46 -0
  218. package/components/ui/ToggleGroup/index.stories.tsx +52 -0
  219. package/components/ui/ToggleGroup/index.tsx +69 -0
  220. package/components/ui/Tooltip/index.stories.tsx +29 -0
  221. package/components/ui/Tooltip/index.tsx +35 -0
  222. package/components/ui/VStack/index.stories.tsx +45 -0
  223. package/components/ui/VStack/index.tsx +69 -0
  224. package/components/ui/colors.stories.tsx +148 -0
  225. package/dist/arrow-left-46B4CAEY.svg +1 -0
  226. package/dist/file-4IXBJF4J.svg +1 -0
  227. package/dist/globe-KVAXBN2U.svg +1 -0
  228. package/dist/index.cjs +6001 -0
  229. package/dist/index.cjs.map +1 -0
  230. package/dist/index.d.cts +693 -0
  231. package/dist/index.d.ts +693 -0
  232. package/dist/index.js +5714 -0
  233. package/dist/index.js.map +1 -0
  234. package/dist/logo-line-QLUD5DAV.svg +1 -0
  235. package/dist/next-HOXZBJQP.svg +1 -0
  236. package/dist/panel-left-expand-SIPFBG4J.svg +1 -0
  237. package/dist/placeholder-H3V4XYVI.svg +57 -0
  238. package/dist/vercel-KFYFHF3A.svg +1 -0
  239. package/dist/window-JNUL4Q2E.svg +1 -0
  240. package/eslint.config.js +10 -0
  241. package/globals.css +994 -0
  242. package/hooks/index.ts +3 -0
  243. package/hooks/use-auto-height.tsx +99 -0
  244. package/hooks/use-controlled-state.tsx +32 -0
  245. package/hooks/use-mobile.ts +19 -0
  246. package/index.ts +58 -0
  247. package/lib/get-strict-context.ts +15 -0
  248. package/lib/utils.ts +10 -0
  249. package/package.json +107 -0
  250. package/scripts/generate-exports.ts +32 -0
  251. package/tsconfig.json +12 -0
  252. package/tsconfig.tsbuildinfo +1 -0
  253. package/tsup.config.ts +11 -0
  254. package/types/svg.d.ts +10 -0
  255. package/vercel.json +5 -0
@@ -0,0 +1,605 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { AnimatePresence, motion, Transition } from 'motion/react';
5
+
6
+ import { cn } from '@/lib/utils';
7
+
8
+ type HighlightMode = 'children' | 'parent';
9
+
10
+ type Bounds = {
11
+ top: number;
12
+ left: number;
13
+ width: number;
14
+ height: number;
15
+ };
16
+
17
+ type HighlightContextType<T extends string> = {
18
+ as?: keyof HTMLElementTagNameMap;
19
+ mode: HighlightMode;
20
+ activeValue: T | null;
21
+ setActiveValue: (value: T | null) => void;
22
+ setBounds: (bounds: DOMRect) => void;
23
+ clearBounds: () => void;
24
+ id: string;
25
+ hover: boolean;
26
+ click: boolean;
27
+ className?: string;
28
+ style?: React.CSSProperties;
29
+ activeClassName?: string;
30
+ setActiveClassName: (className: string) => void;
31
+ transition?: Transition;
32
+ disabled?: boolean;
33
+ enabled?: boolean;
34
+ exitDelay?: number;
35
+ forceUpdateBounds?: boolean;
36
+ };
37
+
38
+ const HighlightContext = React.createContext<HighlightContextType<string> | undefined>(undefined);
39
+
40
+ function useHighlight<T extends string>(): HighlightContextType<T> {
41
+ const context = React.useContext(HighlightContext);
42
+ if (!context) {
43
+ throw new Error('useHighlight must be used within a HighlightProvider');
44
+ }
45
+ return context as unknown as HighlightContextType<T>;
46
+ }
47
+
48
+ type BaseHighlightProps<T extends React.ElementType = 'div'> = {
49
+ as?: T;
50
+ ref?: React.Ref<HTMLDivElement>;
51
+ mode?: HighlightMode;
52
+ value?: string | null;
53
+ defaultValue?: string | null;
54
+ onValueChange?: (value: string | null) => void;
55
+ className?: string;
56
+ style?: React.CSSProperties;
57
+ transition?: Transition;
58
+ hover?: boolean;
59
+ click?: boolean;
60
+ disabled?: boolean;
61
+ enabled?: boolean;
62
+ exitDelay?: number;
63
+ };
64
+
65
+ type ParentModeHighlightProps = {
66
+ boundsOffset?: Partial<Bounds>;
67
+ containerClassName?: string;
68
+ forceUpdateBounds?: boolean;
69
+ };
70
+
71
+ type ControlledParentModeHighlightProps<T extends React.ElementType = 'div'> =
72
+ BaseHighlightProps<T> &
73
+ ParentModeHighlightProps & {
74
+ mode: 'parent';
75
+ controlledItems: true;
76
+ children: React.ReactNode;
77
+ };
78
+
79
+ type ControlledChildrenModeHighlightProps<T extends React.ElementType = 'div'> =
80
+ BaseHighlightProps<T> & {
81
+ mode?: 'children' | undefined;
82
+ controlledItems: true;
83
+ children: React.ReactNode;
84
+ };
85
+
86
+ type UncontrolledParentModeHighlightProps<T extends React.ElementType = 'div'> =
87
+ BaseHighlightProps<T> &
88
+ ParentModeHighlightProps & {
89
+ mode: 'parent';
90
+ controlledItems?: false;
91
+ itemsClassName?: string;
92
+ children: React.ReactElement | React.ReactElement[];
93
+ };
94
+
95
+ type UncontrolledChildrenModeHighlightProps<T extends React.ElementType = 'div'> =
96
+ BaseHighlightProps<T> & {
97
+ mode?: 'children';
98
+ controlledItems?: false;
99
+ itemsClassName?: string;
100
+ children: React.ReactElement | React.ReactElement[];
101
+ };
102
+
103
+ type HighlightProps<T extends React.ElementType = 'div'> =
104
+ | ControlledParentModeHighlightProps<T>
105
+ | ControlledChildrenModeHighlightProps<T>
106
+ | UncontrolledParentModeHighlightProps<T>
107
+ | UncontrolledChildrenModeHighlightProps<T>;
108
+
109
+ function Highlight<T extends React.ElementType = 'div'>({ ref, ...props }: HighlightProps<T>) {
110
+ const {
111
+ as: Component = 'div',
112
+ children,
113
+ value,
114
+ defaultValue,
115
+ onValueChange,
116
+ className,
117
+ style,
118
+ transition = { type: 'spring', stiffness: 350, damping: 35 },
119
+ hover = false,
120
+ click = true,
121
+ enabled = true,
122
+ controlledItems,
123
+ disabled = false,
124
+ exitDelay = 200,
125
+ mode = 'children',
126
+ } = props;
127
+
128
+ const localRef = React.useRef<HTMLDivElement>(null);
129
+ React.useImperativeHandle(ref, () => localRef.current as HTMLDivElement);
130
+
131
+ const [activeValue, setActiveValue] = React.useState<string | null>(
132
+ value ?? defaultValue ?? null,
133
+ );
134
+ const [boundsState, setBoundsState] = React.useState<Bounds | null>(null);
135
+ const [activeClassNameState, setActiveClassNameState] = React.useState<string>('');
136
+
137
+ const onValueChangeRef = React.useRef(onValueChange);
138
+ React.useEffect(() => {
139
+ onValueChangeRef.current = onValueChange;
140
+ }, [onValueChange]);
141
+
142
+ const activeValueRef = React.useRef(activeValue);
143
+ React.useEffect(() => {
144
+ activeValueRef.current = activeValue;
145
+ }, [activeValue]);
146
+
147
+ const safeSetActiveValue = (id: string | null) => {
148
+ setActiveValue((prev) => {
149
+ if (prev === id) return prev;
150
+ if (id !== activeValueRef.current) {
151
+ onValueChangeRef.current?.(id);
152
+ }
153
+ return id;
154
+ });
155
+ };
156
+
157
+ const boundsOffsetRef = React.useRef(
158
+ (props as ParentModeHighlightProps)?.boundsOffset ?? {
159
+ top: 0,
160
+ left: 0,
161
+ width: 0,
162
+ height: 0,
163
+ },
164
+ );
165
+ const boundsOffset = (props as ParentModeHighlightProps)?.boundsOffset;
166
+ React.useEffect(() => {
167
+ boundsOffsetRef.current = boundsOffset ?? {
168
+ top: 0,
169
+ left: 0,
170
+ width: 0,
171
+ height: 0,
172
+ };
173
+ }, [boundsOffset]);
174
+
175
+ const safeSetBoundsRef = React.useRef<(bounds: DOMRect) => void>(() => {});
176
+ const safeSetBounds = (bounds: DOMRect) => {
177
+ if (!localRef.current) return;
178
+
179
+ const boundsOffset = boundsOffsetRef.current;
180
+ const containerRect = localRef.current.getBoundingClientRect();
181
+ const newBounds: Bounds = {
182
+ top: bounds.top - containerRect.top + (boundsOffset.top ?? 0),
183
+ left: bounds.left - containerRect.left + (boundsOffset.left ?? 0),
184
+ width: bounds.width + (boundsOffset.width ?? 0),
185
+ height: bounds.height + (boundsOffset.height ?? 0),
186
+ };
187
+
188
+ setBoundsState((prev) => {
189
+ if (
190
+ prev &&
191
+ prev.top === newBounds.top &&
192
+ prev.left === newBounds.left &&
193
+ prev.width === newBounds.width &&
194
+ prev.height === newBounds.height
195
+ ) {
196
+ return prev;
197
+ }
198
+ return newBounds;
199
+ });
200
+ };
201
+ React.useEffect(() => {
202
+ safeSetBoundsRef.current = safeSetBounds;
203
+ });
204
+
205
+ const clearBounds = () => {
206
+ setBoundsState((prev) => (prev === null ? prev : null));
207
+ };
208
+
209
+ React.useEffect(() => {
210
+ if (value !== undefined) setActiveValue(value);
211
+ else if (defaultValue !== undefined) setActiveValue(defaultValue);
212
+ }, [value, defaultValue]);
213
+
214
+ const id = React.useId();
215
+
216
+ React.useEffect(() => {
217
+ if (mode !== 'parent') return undefined;
218
+ const container = localRef.current;
219
+ if (!container) return;
220
+
221
+ const onScroll = () => {
222
+ if (!activeValue) return;
223
+ const activeEl = container.querySelector<HTMLElement>(
224
+ `[data-value="${activeValue}"][data-highlight="true"]`,
225
+ );
226
+ if (activeEl) safeSetBoundsRef.current?.(activeEl.getBoundingClientRect());
227
+ };
228
+
229
+ container.addEventListener('scroll', onScroll, { passive: true });
230
+ return () => container.removeEventListener('scroll', onScroll);
231
+ }, [mode, activeValue]);
232
+
233
+ const containerClassName = (props as ParentModeHighlightProps)?.containerClassName;
234
+
235
+ const render = (children: React.ReactNode) => {
236
+ if (mode === 'parent') {
237
+ return (
238
+ <Component
239
+ ref={localRef}
240
+ data-slot="motion-highlight-container"
241
+ style={{ position: 'relative', zIndex: 1 }}
242
+ className={containerClassName}
243
+ >
244
+ <AnimatePresence initial={false} mode="wait">
245
+ {boundsState && (
246
+ <motion.div
247
+ data-slot="motion-highlight"
248
+ animate={{
249
+ top: boundsState.top,
250
+ left: boundsState.left,
251
+ width: boundsState.width,
252
+ height: boundsState.height,
253
+ opacity: 1,
254
+ }}
255
+ initial={{
256
+ top: boundsState.top,
257
+ left: boundsState.left,
258
+ width: boundsState.width,
259
+ height: boundsState.height,
260
+ opacity: 0,
261
+ }}
262
+ exit={{
263
+ opacity: 0,
264
+ transition: {
265
+ ...transition,
266
+ delay: (transition?.delay ?? 0) + (exitDelay ?? 0) / 1000,
267
+ },
268
+ }}
269
+ transition={transition}
270
+ style={{ position: 'absolute', zIndex: 0, ...style }}
271
+ className={cn(className, activeClassNameState)}
272
+ />
273
+ )}
274
+ </AnimatePresence>
275
+ {children}
276
+ </Component>
277
+ );
278
+ }
279
+
280
+ return children;
281
+ };
282
+
283
+ return (
284
+ <HighlightContext.Provider
285
+ value={{
286
+ mode,
287
+ activeValue,
288
+ setActiveValue: safeSetActiveValue,
289
+ id,
290
+ hover,
291
+ click,
292
+ className,
293
+ style,
294
+ transition,
295
+ disabled,
296
+ enabled,
297
+ exitDelay,
298
+ setBounds: safeSetBounds,
299
+ clearBounds,
300
+ activeClassName: activeClassNameState,
301
+ setActiveClassName: setActiveClassNameState,
302
+ forceUpdateBounds: (props as ParentModeHighlightProps)?.forceUpdateBounds,
303
+ }}
304
+ >
305
+ {enabled
306
+ ? controlledItems
307
+ ? render(children)
308
+ : render(
309
+ React.Children.map(children, (child, index) => (
310
+ <HighlightItem key={index} className={props?.itemsClassName}>
311
+ {child}
312
+ </HighlightItem>
313
+ )),
314
+ )
315
+ : children}
316
+ </HighlightContext.Provider>
317
+ );
318
+ }
319
+
320
+ function getNonOverridingDataAttributes(
321
+ element: React.ReactElement,
322
+ dataAttributes: Record<string, unknown>,
323
+ ): Record<string, unknown> {
324
+ return Object.keys(dataAttributes).reduce<Record<string, unknown>>((acc, key) => {
325
+ if ((element.props as Record<string, unknown>)[key] === undefined) {
326
+ acc[key] = dataAttributes[key];
327
+ }
328
+ return acc;
329
+ }, {});
330
+ }
331
+
332
+ type ExtendedChildProps = React.ComponentProps<'div'> & {
333
+ id?: string;
334
+ ref?: React.Ref<HTMLElement>;
335
+ 'data-active'?: string;
336
+ 'data-value'?: string;
337
+ 'data-disabled'?: boolean;
338
+ 'data-highlight'?: boolean;
339
+ 'data-slot'?: string;
340
+ };
341
+
342
+ type HighlightItemProps<T extends React.ElementType = 'div'> = React.ComponentProps<T> & {
343
+ as?: T;
344
+ children: React.ReactElement;
345
+ id?: string;
346
+ value?: string;
347
+ className?: string;
348
+ style?: React.CSSProperties;
349
+ transition?: Transition;
350
+ activeClassName?: string;
351
+ disabled?: boolean;
352
+ exitDelay?: number;
353
+ asChild?: boolean;
354
+ forceUpdateBounds?: boolean;
355
+ };
356
+
357
+ function HighlightItem<T extends React.ElementType>({
358
+ ref,
359
+ as,
360
+ children,
361
+ id,
362
+ value,
363
+ className,
364
+ style,
365
+ transition,
366
+ disabled = false,
367
+ activeClassName,
368
+ exitDelay,
369
+ asChild = false,
370
+ forceUpdateBounds,
371
+ ...props
372
+ }: HighlightItemProps<T>) {
373
+ const itemId = React.useId();
374
+ const {
375
+ activeValue,
376
+ setActiveValue,
377
+ mode,
378
+ setBounds,
379
+ clearBounds,
380
+ hover,
381
+ click,
382
+ enabled,
383
+ className: contextClassName,
384
+ style: contextStyle,
385
+ transition: contextTransition,
386
+ id: contextId,
387
+ disabled: contextDisabled,
388
+ exitDelay: contextExitDelay,
389
+ forceUpdateBounds: contextForceUpdateBounds,
390
+ setActiveClassName,
391
+ } = useHighlight();
392
+
393
+ const Component = as ?? 'div';
394
+ const element = children as React.ReactElement<ExtendedChildProps>;
395
+ const childValue = id ?? value ?? element.props?.['data-value'] ?? element.props?.id ?? itemId;
396
+ const isActive = activeValue === childValue;
397
+ const isDisabled = disabled === undefined ? contextDisabled : disabled;
398
+ const itemTransition = transition ?? contextTransition;
399
+
400
+ const localRef = React.useRef<HTMLElement | null>(null);
401
+ React.useImperativeHandle(ref, () => localRef.current as HTMLDivElement);
402
+
403
+ const setRef = React.useCallback((node: HTMLElement | null) => {
404
+ localRef.current = node;
405
+ }, []);
406
+
407
+ React.useEffect(() => {
408
+ if (mode !== 'parent') return;
409
+ let rafId: number;
410
+ let previousBounds: Bounds | null = null;
411
+ const shouldUpdateBounds =
412
+ forceUpdateBounds === true || (contextForceUpdateBounds && forceUpdateBounds !== false);
413
+
414
+ const updateBounds = () => {
415
+ if (!localRef.current) return;
416
+
417
+ const bounds = localRef.current.getBoundingClientRect();
418
+
419
+ if (shouldUpdateBounds) {
420
+ if (
421
+ previousBounds &&
422
+ previousBounds.top === bounds.top &&
423
+ previousBounds.left === bounds.left &&
424
+ previousBounds.width === bounds.width &&
425
+ previousBounds.height === bounds.height
426
+ ) {
427
+ rafId = requestAnimationFrame(updateBounds);
428
+ return;
429
+ }
430
+ previousBounds = bounds;
431
+ rafId = requestAnimationFrame(updateBounds);
432
+ }
433
+
434
+ setBounds(bounds);
435
+ };
436
+
437
+ if (isActive) {
438
+ updateBounds();
439
+ setActiveClassName(activeClassName ?? '');
440
+ } else if (!activeValue) clearBounds();
441
+
442
+ if (shouldUpdateBounds) return () => cancelAnimationFrame(rafId);
443
+ return undefined;
444
+ }, [
445
+ mode,
446
+ isActive,
447
+ activeValue,
448
+ setBounds,
449
+ clearBounds,
450
+ activeClassName,
451
+ setActiveClassName,
452
+ forceUpdateBounds,
453
+ contextForceUpdateBounds,
454
+ ]);
455
+
456
+ if (!React.isValidElement(children)) return children;
457
+
458
+ const dataAttributes = {
459
+ 'data-active': isActive ? 'true' : 'false',
460
+ 'aria-selected': isActive,
461
+ 'data-disabled': isDisabled,
462
+ 'data-value': childValue,
463
+ 'data-highlight': true,
464
+ };
465
+
466
+ const commonHandlers = hover
467
+ ? {
468
+ onMouseEnter: (e: React.MouseEvent<HTMLDivElement>) => {
469
+ setActiveValue(childValue);
470
+ element.props.onMouseEnter?.(e);
471
+ },
472
+ onMouseLeave: (e: React.MouseEvent<HTMLDivElement>) => {
473
+ setActiveValue(null);
474
+ element.props.onMouseLeave?.(e);
475
+ },
476
+ }
477
+ : click
478
+ ? {
479
+ onClick: (e: React.MouseEvent<HTMLDivElement>) => {
480
+ setActiveValue(childValue);
481
+ element.props.onClick?.(e);
482
+ },
483
+ }
484
+ : {};
485
+
486
+ if (asChild) {
487
+ if (mode === 'children') {
488
+ return React.cloneElement(
489
+ element,
490
+ {
491
+ key: childValue,
492
+ ref: setRef,
493
+ className: cn('relative', element.props.className),
494
+ ...getNonOverridingDataAttributes(element, {
495
+ ...dataAttributes,
496
+ 'data-slot': 'motion-highlight-item-container',
497
+ }),
498
+ ...commonHandlers,
499
+ ...props,
500
+ },
501
+ <>
502
+ <AnimatePresence initial={false} mode="wait">
503
+ {isActive && !isDisabled && (
504
+ <motion.div
505
+ layoutId={`transition-background-${contextId}`}
506
+ data-slot="motion-highlight"
507
+ style={{
508
+ position: 'absolute',
509
+ zIndex: 0,
510
+ ...contextStyle,
511
+ ...style,
512
+ }}
513
+ className={cn(contextClassName, activeClassName)}
514
+ transition={itemTransition}
515
+ initial={{ opacity: 0 }}
516
+ animate={{ opacity: 1 }}
517
+ exit={{
518
+ opacity: 0,
519
+ transition: {
520
+ ...itemTransition,
521
+ delay:
522
+ (itemTransition?.delay ?? 0) + (exitDelay ?? contextExitDelay ?? 0) / 1000,
523
+ },
524
+ }}
525
+ {...dataAttributes}
526
+ />
527
+ )}
528
+ </AnimatePresence>
529
+
530
+ <Component
531
+ data-slot="motion-highlight-item"
532
+ style={{ position: 'relative', zIndex: 1 }}
533
+ className={className}
534
+ {...dataAttributes}
535
+ >
536
+ {children}
537
+ </Component>
538
+ </>,
539
+ );
540
+ }
541
+
542
+ return React.cloneElement(element, {
543
+ ref: setRef,
544
+ ...getNonOverridingDataAttributes(element, {
545
+ ...dataAttributes,
546
+ 'data-slot': 'motion-highlight-item',
547
+ }),
548
+ ...commonHandlers,
549
+ });
550
+ }
551
+
552
+ return enabled ? (
553
+ <Component
554
+ key={childValue}
555
+ ref={localRef}
556
+ data-slot="motion-highlight-item-container"
557
+ className={cn(mode === 'children' && 'relative', className)}
558
+ {...dataAttributes}
559
+ {...props}
560
+ {...commonHandlers}
561
+ >
562
+ {mode === 'children' && (
563
+ <AnimatePresence initial={false} mode="wait">
564
+ {isActive && !isDisabled && (
565
+ <motion.div
566
+ layoutId={`transition-background-${contextId}`}
567
+ data-slot="motion-highlight"
568
+ style={{
569
+ position: 'absolute',
570
+ zIndex: 0,
571
+ ...contextStyle,
572
+ ...style,
573
+ }}
574
+ className={cn(contextClassName, activeClassName)}
575
+ transition={itemTransition}
576
+ initial={{ opacity: 0 }}
577
+ animate={{ opacity: 1 }}
578
+ exit={{
579
+ opacity: 0,
580
+ transition: {
581
+ ...itemTransition,
582
+ delay: (itemTransition?.delay ?? 0) + (exitDelay ?? contextExitDelay ?? 0) / 1000,
583
+ },
584
+ }}
585
+ {...dataAttributes}
586
+ />
587
+ )}
588
+ </AnimatePresence>
589
+ )}
590
+
591
+ {React.cloneElement(element, {
592
+ style: { position: 'relative', zIndex: 1 },
593
+ className: element.props.className,
594
+ ...getNonOverridingDataAttributes(element, {
595
+ ...dataAttributes,
596
+ 'data-slot': 'motion-highlight-item',
597
+ }),
598
+ })}
599
+ </Component>
600
+ ) : (
601
+ children
602
+ );
603
+ }
604
+
605
+ export { Highlight, HighlightItem, useHighlight, type HighlightProps, type HighlightItemProps };
@@ -0,0 +1,55 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef } from 'react';
4
+ import { useInView, useMotionValue, useSpring } from 'motion/react';
5
+
6
+ import { cn } from '@/lib/utils';
7
+
8
+ export default function NumberTicker({
9
+ value,
10
+ direction = 'up',
11
+ delay = 0,
12
+ subValue = '',
13
+ preValue = '',
14
+ className,
15
+ }: {
16
+ value: number;
17
+ direction?: 'up' | 'down';
18
+ className?: string;
19
+ delay?: number; // delay in s
20
+ subValue?: string;
21
+ preValue?: string;
22
+ }) {
23
+ const ref = useRef<HTMLSpanElement>(null);
24
+ const motionValue = useMotionValue(direction === 'down' ? value : 0);
25
+ const springValue = useSpring(motionValue, {
26
+ damping: 60,
27
+ stiffness: 100,
28
+ });
29
+ const isInView = useInView(ref, { once: true, margin: '0px' });
30
+
31
+ useEffect(() => {
32
+ if (isInView) {
33
+ setTimeout(() => {
34
+ motionValue.set(direction === 'down' ? 0 : value);
35
+ }, delay * 1000);
36
+ }
37
+ }, [motionValue, isInView, delay, value, direction]);
38
+
39
+ useEffect(() => {
40
+ const updateText = (val: number) => {
41
+ if (ref.current) {
42
+ ref.current.textContent = `${preValue}${Intl.NumberFormat('en-US').format(Math.round(val))}${subValue}`;
43
+ }
44
+ };
45
+
46
+ const initialValue = direction === 'down' ? value : 0;
47
+ updateText(initialValue);
48
+
49
+ return springValue.on('change', (latest: number) => {
50
+ updateText(latest);
51
+ });
52
+ }, [preValue, springValue, subValue, value, direction]);
53
+
54
+ return <span className={cn('text-primary-50 inline-block tabular-nums', className)} ref={ref} />;
55
+ }