apex-ui-kit 2.3.1 → 2.4.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.
package/ReadMe.md CHANGED
@@ -1,75 +1,255 @@
1
- # Apex UI Kit
2
-
3
- Apex UI Kit is a modern, blazing-fast, beautifully animated React UI library built with Tailwind CSS, GSAP, and Framer Motion. Perfect for modern apps, portfolios, dashboards, and high-performance UIs.
4
-
5
- ---
6
-
7
- ## 🌐 Website & Docs
8
-
9
- - **Main Site:** [https://apex-ui.in/](https://apex-ui.in/)
10
- - **Introduction:** [https://apex-ui.in/components/docs/getting-started/introduction](https://apex-ui.in/components/docs/getting-started/introduction)
11
- - **Components:** [https://apex-ui.in/components/glass-navbar](https://apex-ui.in/components/glass-navbar)
12
-
13
- ## 🚀 Installation
14
-
15
- ### 1. Install the package
16
-
17
- ```bash
18
- npm install apex-ui-kit
19
- ```
20
-
21
- ### 2. Add a component using the CLI
22
-
23
- ```bash
24
- npx apex-ui-kit add hyper-card
25
- ```
26
-
27
- ---
28
-
29
- ## 🧩 Usage Example
30
-
31
- ```jsx
32
- import HyperCard from "./ApexUI-Kit/HyperCard/HyperCard.jsx";
33
-
34
- const App = () => {
35
- return (
36
- <div>
37
- <HyperCard
38
- starColor="#9f7aea"
39
- glow={true}
40
- beamCount={10}
41
- starCount={250}
42
- />
43
- </div>
44
- );
45
- };
46
-
47
- export default App;
48
- ```
49
-
50
- ---
51
-
52
- ## 📦 Features
53
-
54
- - Modern, animated React components
55
- - Easy CLI for adding components
56
- - Built with Tailwind CSS, GSAP, Framer Motion
57
- - Perfect for dashboards, portfolios, and modern web apps
58
-
59
- ---
60
-
61
- ## 📚 Documentation
62
-
63
- - More components and usage: [Coming Soon]
64
-
65
- ---
66
-
67
- ## 🛠️ Contributing
68
-
69
- Pull requests are welcome! For major changes, please open an issue first to discuss what you would like to change.
70
-
71
- ---
72
-
73
- ## 📝 License
74
-
75
- MIT
1
+ # ApexUI Kit
2
+
3
+ [![npm version](https://img.shields.io/npm/v/apex-ui-kit?color=a3e635&label=npm)](https://www.npmjs.com/package/apex-ui-kit)
4
+ [![npm downloads](https://img.shields.io/npm/dm/apex-ui-kit?color=a3e635)](https://www.npmjs.com/package/apex-ui-kit)
5
+ [![license](https://img.shields.io/npm/l/apex-ui-kit?color=a3e635)](./Licence.md)
6
+
7
+ **93 free, animated UI components for React.** Pick one, add it with a single command, and the source lands in your project so you can read it and change anything.
8
+
9
+ Built with React, Tailwind CSS and Framer Motion, plus GSAP, Canvas and WebGL where a component needs them.
10
+
11
+ - Website: [apex-ui.in](https://apex-ui.in)
12
+ - All components: [apex-ui.in/components](https://apex-ui.in/components)
13
+ - Getting started: [Introduction](https://apex-ui.in/components/docs/getting-started/introduction)
14
+ - CLI guide: [ApexUI CLI setup](https://apex-ui.in/components/docs/getting-started/installation/apexui-cli)
15
+
16
+ ## Why ApexUI
17
+
18
+ - **93 components** across backgrounds, cards, text effects, navigation, buttons, interactive UI, AI interfaces, loaders and maps.
19
+ - **You own the code.** The CLI copies plain `.jsx` files into your project, no runtime package to keep in sync.
20
+ - **Every docs page has** a live preview, a playground to tweak props, a Desktop / Tablet / Mobile preview, the full props table and a ready prompt you can paste into ChatGPT or Claude.
21
+ - **Smooth on real devices.** Animations stick to transform and opacity, respect reduced motion, and work with mouse, touch and keyboard.
22
+ - **Light and dark** themes on the components that have a surface (`theme="light" | "dark"`).
23
+
24
+ ## Requirements
25
+
26
+ - React 18 or newer
27
+ - Tailwind CSS (v4 recommended, see the [Tailwind setup guide](https://apex-ui.in/components/docs/getting-started/installation/tailwind-setup))
28
+
29
+ ## Quick start
30
+
31
+ 1. Install the CLI package:
32
+
33
+ ```bash
34
+ npm install apex-ui-kit
35
+ ```
36
+
37
+ 2. Add any component by its CLI name:
38
+
39
+ ```bash
40
+ npx apex-ui-kit add gooey-nav
41
+ ```
42
+
43
+ The CLI asks where to put the files, installs the shared dependencies (Framer Motion, GSAP, Three.js, Lucide icons and a few more, plus anything that one component needs, such as `ogl`), and copies the component to:
44
+
45
+ ```
46
+ <your folder>/ApexUI-Kit/<ComponentName>/
47
+ ```
48
+
49
+ 3. Import it and use it:
50
+
51
+ ```jsx
52
+ import GooeyNav from "./ApexUI-Kit/GooeyNav/GooeyNav.jsx";
53
+
54
+ export default function App() {
55
+ return (
56
+ <GooeyNav
57
+ accentColor="#a3e635"
58
+ items={[
59
+ { label: "Home", href: "/" },
60
+ { label: "Components", href: "/components" },
61
+ { label: "Docs", href: "/docs" },
62
+ ]}
63
+ />
64
+ );
65
+ }
66
+ ```
67
+
68
+ ## Example: AI prompt with a model picker
69
+
70
+ ```bash
71
+ npx apex-ui-kit add ai-model-prompt
72
+ ```
73
+
74
+ ```jsx
75
+ import { useState } from "react";
76
+ import { DEFAULT_LLM_MODELS, AiModelPrompt } from "./ApexUI-Kit/AiModelPrompt";
77
+
78
+ export function PromptComposer() {
79
+ const [modelValue, setModelValue] = useState("claude-fable-5");
80
+ const [configurations, setConfigurations] = useState({});
81
+ const [prompt, setPrompt] = useState("");
82
+
83
+ return (
84
+ <AiModelPrompt
85
+ models={DEFAULT_LLM_MODELS}
86
+ value={modelValue}
87
+ onModelChange={(model) => setModelValue(model.value)}
88
+ configurations={configurations}
89
+ onConfigurationChange={(_, __, next) => setConfigurations(next)}
90
+ prompt={prompt}
91
+ onPromptChange={setPrompt}
92
+ onSubmit={({ model, configuration, prompt }) => sendToYourApi({ model, configuration, prompt })}
93
+ />
94
+ );
95
+ }
96
+ ```
97
+
98
+ Every component's props, variants and copy-ready code are on its docs page.
99
+
100
+ ## Components
101
+
102
+ ### Backgrounds (12)
103
+
104
+ | Component | CLI name |
105
+ | --- | --- |
106
+ | [Aurora Background](https://apex-ui.in/components/aurora-background) | `aurora-background` |
107
+ | [BluePrint Background](https://apex-ui.in/components/blueprint-background) | `blueprint-background` |
108
+ | [Dot Wave Background](https://apex-ui.in/components/dot-wave-background) | `dot-wave-background` |
109
+ | [Kinetic Threads Background](https://apex-ui.in/components/kinetic-threads-background) | `kinetic-threads-background` |
110
+ | [Lens Flare Background](https://apex-ui.in/components/lens-flare-background) | `lens-flare-background` |
111
+ | [Liquid Gradient](https://apex-ui.in/components/liquid-gradient) | `liquid-gradient` |
112
+ | [Luminous Particle Ocean](https://apex-ui.in/components/luminous-particle-ocean) | `luminous-particle-ocean` |
113
+ | [Orbs Background](https://apex-ui.in/components/orbs-background) | `orbs-background` |
114
+ | [Rain Background](https://apex-ui.in/components/rain-background) | `rain-background` |
115
+ | [Retro Grid 3D](https://apex-ui.in/components/retro-grid-3d) | `retro-grid-3d` |
116
+ | [Ripple Background](https://apex-ui.in/components/ripple-background) | `ripple-background` |
117
+ | [Smoke Background](https://apex-ui.in/components/smoke-background) | `smoke-background` |
118
+
119
+ ### Cards (17)
120
+
121
+ | Component | CLI name |
122
+ | --- | --- |
123
+ | [Bounce Cards](https://apex-ui.in/components/bounce-cards) | `bounce-cards` |
124
+ | [Card Stack](https://apex-ui.in/components/card-stack) | `card-stack` |
125
+ | [Card Swap 3D](https://apex-ui.in/components/card-swap-3d) | `card-swap-3d` |
126
+ | [Cards](https://apex-ui.in/components/cards) | `basic-card`, `meteor-card` |
127
+ | [Folder](https://apex-ui.in/components/folder) | `folder` |
128
+ | [Glare Card](https://apex-ui.in/components/glare-card) | `glare-card` |
129
+ | [Holo Card](https://apex-ui.in/components/holo-card) | `holo-card` |
130
+ | [Hyper Card](https://apex-ui.in/components/hyper-card) | `hyper-card` |
131
+ | [Iso Stack](https://apex-ui.in/components/iso-stack) | `iso-stack` |
132
+ | [Parallax Card](https://apex-ui.in/components/parallax-card) | `parallax-card` |
133
+ | [Pixel Transition](https://apex-ui.in/components/pixel-transition) | `pixel-transition` |
134
+ | [Pricing Cards](https://apex-ui.in/components/pricing-cards) | `pricing-cards` |
135
+ | [Profile](https://apex-ui.in/components/profile) | `flip-profile`, `parallax-profile`, `profile-card` |
136
+ | [Smart Grid Card](https://apex-ui.in/components/smart-grid-card) | `smart-grid-card` |
137
+ | [Spotlight Grid](https://apex-ui.in/components/spotlight-grid) | `spotlight-grid` |
138
+ | [Swipe Cards](https://apex-ui.in/components/swipe-cards) | `swipe-cards` |
139
+ | [Testimonials](https://apex-ui.in/components/testimonials) | `testimonials` |
140
+
141
+ ### Text & Motion (18)
142
+
143
+ | Component | CLI name |
144
+ | --- | --- |
145
+ | [3D Text Ring](https://apex-ui.in/components/text-ring-3d) | `text-ring-3d` |
146
+ | [Accordion Marquee](https://apex-ui.in/components/accordion-marquee) | `accordion-marquee` |
147
+ | [Beam Bar](https://apex-ui.in/components/beam-bar) | `beam-bar` |
148
+ | [Blur Reveal Text](https://apex-ui.in/components/blur-reveal-text) | `blur-reveal-text` |
149
+ | [Circular Text](https://apex-ui.in/components/circular-text) | `circular-text` |
150
+ | [Extruded Text](https://apex-ui.in/components/extruded-text) | `extruded-text` |
151
+ | [Flip Clock](https://apex-ui.in/components/flip-clock) | `flip-clock` |
152
+ | [Glitch Text](https://apex-ui.in/components/glitch-text) | `glitch-text` |
153
+ | [Hover Text](https://apex-ui.in/components/hover-text) | `hover-text` |
154
+ | [Mask View](https://apex-ui.in/components/mask-view) | `mask-view` |
155
+ | [Proximity Text](https://apex-ui.in/components/proximity-text) | `proximity-text` |
156
+ | [Rolling Counter](https://apex-ui.in/components/rolling-counter) | `rolling-counter` |
157
+ | [Rotating Text](https://apex-ui.in/components/rotating-text) | `rotating-text` |
158
+ | [Scramble Text](https://apex-ui.in/components/scramble-text) | `scramble-text` |
159
+ | [Scroll Marquee](https://apex-ui.in/components/scroll-marquee) | `scroll-marquee` |
160
+ | [Shiny Text](https://apex-ui.in/components/shiny-text) | `shiny-text` |
161
+ | [True Focus](https://apex-ui.in/components/true-focus) | `true-focus` |
162
+ | [Typewriter Text](https://apex-ui.in/components/typewriter-text) | `typewriter-text` |
163
+
164
+ ### Navigation (8)
165
+
166
+ | Component | CLI name |
167
+ | --- | --- |
168
+ | [Animated Tabs](https://apex-ui.in/components/animated-tabs) | `animated-tabs` |
169
+ | [Floating Navbar](https://apex-ui.in/components/floating-navbar) | `floating-navbar` |
170
+ | [Gooey Nav](https://apex-ui.in/components/gooey-nav) | `gooey-nav` |
171
+ | [Magnify Dock](https://apex-ui.in/components/magnify-dock) | `magnify-dock` |
172
+ | [Nav DropDown](https://apex-ui.in/components/nav-drop-down) | `nav-dropdown` |
173
+ | [Nav Menu](https://apex-ui.in/components/nav-menu) | `nav-menu` |
174
+ | [Sidebar Nav](https://apex-ui.in/components/sidebar-nav) | `sidebar-nav` |
175
+ | [Theme Toggle](https://apex-ui.in/components/theme-toggle) | `theme-toggle` |
176
+
177
+ ### Buttons (3)
178
+
179
+ | Component | CLI name |
180
+ | --- | --- |
181
+ | [Animated Buttons](https://apex-ui.in/components/animated-buttons) | `animated-button`, `hold-to-confirm` |
182
+ | [Animated Switches](https://apex-ui.in/components/animated-switches) | `animated-switch` |
183
+ | [Star Border](https://apex-ui.in/components/star-border) | `star-border` |
184
+
185
+ ### Interactive (22)
186
+
187
+ | Component | CLI name |
188
+ | --- | --- |
189
+ | [3D Carousel](https://apex-ui.in/components/3d-carousel) | `3d-carousel`, `webgl-carousel` |
190
+ | [Avatar](https://apex-ui.in/components/avatar) | `avatar` |
191
+ | [Carousel](https://apex-ui.in/components/carousel) | `carousel`, `parallax-carousel`, `drag-carousel` |
192
+ | [Click Spark](https://apex-ui.in/components/click-spark) | `click-spark` |
193
+ | [Coverflow 3D](https://apex-ui.in/components/coverflow-3d) | `coverflow-3d` |
194
+ | [Cube Gallery](https://apex-ui.in/components/cube-gallery) | `cube-gallery` |
195
+ | [Cursor Trail](https://apex-ui.in/components/cursor-trail) | `cursor-trail` |
196
+ | [Dynamic Island](https://apex-ui.in/components/dynamic-island) | `dynamic-island` |
197
+ | [Edge Glow Form](https://apex-ui.in/components/edge-glow-form) | `edge-glow-form` |
198
+ | [Elastic Slider](https://apex-ui.in/components/elastic-slider) | `elastic-slider` |
199
+ | [FAQ Accordion](https://apex-ui.in/components/faq-accordion) | `faq-accordion` |
200
+ | [Flip Book](https://apex-ui.in/components/flip-book) | `flip-book` |
201
+ | [Image Compare](https://apex-ui.in/components/image-compare) | `image-compare` |
202
+ | [Liquid Blob 3D](https://apex-ui.in/components/liquid-blob-3d) | `liquid-blob-3d` |
203
+ | [Magnet Lines](https://apex-ui.in/components/magnet-lines) | `magnet-lines` |
204
+ | [Magnetic Cursor](https://apex-ui.in/components/magnetic-cursor) | `magnetic-cursor` |
205
+ | [Particle Morph](https://apex-ui.in/components/particle-morph) | `particle-morph` |
206
+ | [Pointer Follower](https://apex-ui.in/components/pointer-follower) | `pointer-follower` |
207
+ | [Receipt Printer](https://apex-ui.in/components/receipt-printer) | `receipt-printer` |
208
+ | [Stepper](https://apex-ui.in/components/stepper) | `stepper` |
209
+ | [Toast Stack](https://apex-ui.in/components/toast-stack) | `toast-stack` |
210
+ | [ToolTip](https://apex-ui.in/components/tool-tip) | `tooltip` |
211
+
212
+ ### AI (10)
213
+
214
+ | Component | CLI name |
215
+ | --- | --- |
216
+ | [AI Chat](https://apex-ui.in/components/ai-chat) | `ai-chat` |
217
+ | [AI Model Prompt](https://apex-ui.in/components/ai-model-prompt) | `ai-model-prompt` |
218
+ | [AI Prompt Box](https://apex-ui.in/components/ai-prompt-box) | `ai-prompt-box` |
219
+ | [AI Response](https://apex-ui.in/components/ai-response) | `ai-response` |
220
+ | [AI Thinking](https://apex-ui.in/components/ai-thinking) | `ai-thinking` |
221
+ | [AI Voice Bars](https://apex-ui.in/components/ai-voice-bars) | `ai-voice-bars` |
222
+ | [AI Voice Blob](https://apex-ui.in/components/ai-voice-blob) | `ai-voice-blob` |
223
+ | [AI Voice Orb](https://apex-ui.in/components/ai-voice-orb) | `ai-voice-orb` |
224
+ | [AI Voice Ring](https://apex-ui.in/components/ai-voice-ring) | `ai-voice-ring` |
225
+ | [AI Voice Wave](https://apex-ui.in/components/ai-voice-wave) | `ai-voice-wave` |
226
+
227
+ ### Loaders (1)
228
+
229
+ | Component | CLI name |
230
+ | --- | --- |
231
+ | [Loading Screens](https://apex-ui.in/components/loading-screens) | `loading-screen` |
232
+
233
+ ### Maps (2)
234
+
235
+ | Component | CLI name |
236
+ | --- | --- |
237
+ | [Dot Globe](https://apex-ui.in/components/dot-globe) | `dot-globe` |
238
+ | [World Map](https://apex-ui.in/components/world-map) | `world-map` |
239
+
240
+ ## What's new in 2.4
241
+
242
+ - 24 new components, including three.js 3D (Liquid Blob 3D, Particle Morph, Retro Grid 3D), the AI voice family (Wave, Bars, Ring, Blob), Gooey Nav (WebGL metaball pill), Coverflow 3D, Card Swap 3D, 3D Text Ring, Elastic Slider, Stepper and Pixel Transition.
243
+ - AI Model Prompt now exports `DEFAULT_LLM_MODELS` and an index file, so you can import both from the folder.
244
+ - Loading Screens show real progress: `useLoadProgress` loads your images, files or code and drives the bar and the count.
245
+ - Hold to Confirm follows a delete flow with an `UndoNotice` that counts down before the action is final.
246
+ - Sliders and drag components no longer stop midway when the page text gets selected.
247
+ - Stepper and Animated Tabs keep a steady height, Shiny Text loops seamlessly, Cube Gallery is a real 3D cube and Flip Book is a hardcover book you can drag.
248
+
249
+ ## Contributing
250
+
251
+ Issues and pull requests are welcome on [GitHub](https://github.com/Mohan-Kumar-Dalei/ApexUI). For bigger changes, please open an issue first so we can talk it through.
252
+
253
+ ## License
254
+
255
+ MIT
@@ -0,0 +1,174 @@
1
+ import React, { useEffect, useRef } from "react";
2
+ import { Color, IcosahedronGeometry, Mesh, PerspectiveCamera, Scene, ShaderMaterial, WebGLRenderer } from "three";
3
+
4
+ /*
5
+ * Liquid Blob 3D: a glossy blob whose surface flows with 3D noise, shaded
6
+ * with an iridescent fresnel rim. It leans toward the pointer and ripples
7
+ * harder while hovered. One mesh, one shader, no textures, so it stays
8
+ * light. Pauses when off screen and frees the GPU on unmount.
9
+ */
10
+
11
+ const NOISE = `
12
+ vec3 mod289(vec3 x){return x-floor(x*(1./289.))*289.;}
13
+ vec4 mod289(vec4 x){return x-floor(x*(1./289.))*289.;}
14
+ vec4 permute(vec4 x){return mod289(((x*34.)+1.)*x);}
15
+ vec4 taylorInvSqrt(vec4 r){return 1.79284291400159-.85373472095314*r;}
16
+ float snoise(vec3 v){
17
+ const vec2 C=vec2(1./6.,1./3.);const vec4 D=vec4(0.,.5,1.,2.);
18
+ vec3 i=floor(v+dot(v,C.yyy));vec3 x0=v-i+dot(i,C.xxx);
19
+ vec3 g=step(x0.yzx,x0.xyz);vec3 l=1.-g;vec3 i1=min(g.xyz,l.zxy);vec3 i2=max(g.xyz,l.zxy);
20
+ vec3 x1=x0-i1+C.xxx;vec3 x2=x0-i2+C.yyy;vec3 x3=x0-D.yyy;i=mod289(i);
21
+ vec4 p=permute(permute(permute(i.z+vec4(0.,i1.z,i2.z,1.))+i.y+vec4(0.,i1.y,i2.y,1.))+i.x+vec4(0.,i1.x,i2.x,1.));
22
+ float n_=.142857142857;vec3 ns=n_*D.wyz-D.xzx;vec4 j=p-49.*floor(p*ns.z*ns.z);
23
+ vec4 x_=floor(j*ns.z);vec4 y_=floor(j-7.*x_);vec4 x=x_*ns.x+ns.yyyy;vec4 y=y_*ns.x+ns.yyyy;vec4 h=1.-abs(x)-abs(y);
24
+ vec4 b0=vec4(x.xy,y.xy);vec4 b1=vec4(x.zw,y.zw);vec4 s0=floor(b0)*2.+1.;vec4 s1=floor(b1)*2.+1.;vec4 sh=-step(h,vec4(0.));
25
+ vec4 a0=b0.xzyw+s0.xzyw*sh.xxyy;vec4 a1=b1.xzyw+s1.xzyw*sh.zzww;
26
+ vec3 p0=vec3(a0.xy,h.x);vec3 p1=vec3(a0.zw,h.y);vec3 p2=vec3(a1.xy,h.z);vec3 p3=vec3(a1.zw,h.w);
27
+ vec4 norm=taylorInvSqrt(vec4(dot(p0,p0),dot(p1,p1),dot(p2,p2),dot(p3,p3)));p0*=norm.x;p1*=norm.y;p2*=norm.z;p3*=norm.w;
28
+ vec4 m=max(.6-vec4(dot(x0,x0),dot(x1,x1),dot(x2,x2),dot(x3,x3)),0.);m=m*m;
29
+ return 42.*dot(m*m,vec4(dot(p0,x0),dot(p1,x1),dot(p2,x2),dot(p3,x3)));
30
+ }`;
31
+
32
+ const VERT = `
33
+ uniform float uTime; uniform float uAmp; uniform float uFreq;
34
+ varying vec3 vNormal; varying vec3 vView; varying float vNoise;
35
+ ${NOISE}
36
+ float field(vec3 p){ return snoise(p * uFreq + vec3(uTime * .3)) + .18 * snoise(p * uFreq * 2. - vec3(uTime * .2)); }
37
+ void main(){
38
+ float n = field(normal);
39
+ vNoise = n;
40
+ vec3 pos = position + normal * n * uAmp;
41
+ // Rebuild the normal from two nearby displaced points so the shading follows the waves.
42
+ vec3 t = normalize(cross(normal, abs(normal.y) < .99 ? vec3(0.,1.,0.) : vec3(1.,0.,0.)));
43
+ vec3 b = normalize(cross(normal, t));
44
+ float e = .01;
45
+ vec3 n1 = normalize(normal + t * e); vec3 n2 = normalize(normal + b * e);
46
+ vec3 p1 = n1 * (1. + field(n1) * uAmp); vec3 p2 = n2 * (1. + field(n2) * uAmp);
47
+ vec3 nrm = normalize(cross(p1 - pos, p2 - pos));
48
+ if (dot(nrm, normal) < 0.) nrm = -nrm;
49
+ vec4 mv = modelViewMatrix * vec4(pos, 1.);
50
+ vView = -mv.xyz;
51
+ vNormal = normalize(normalMatrix * nrm);
52
+ gl_Position = projectionMatrix * mv;
53
+ }`;
54
+
55
+ const FRAG = `
56
+ uniform vec3 uC1; uniform vec3 uC2; uniform vec3 uC3;
57
+ varying vec3 vNormal; varying vec3 vView; varying float vNoise;
58
+ void main(){
59
+ vec3 n = normalize(vNormal); vec3 v = normalize(vView);
60
+ float fr = pow(1. - max(dot(n, v), 0.), 2.4);
61
+ vec3 col = mix(uC1, uC2, smoothstep(-.7, .7, vNoise));
62
+ col = mix(col, uC3, fr * .9);
63
+ vec3 l = normalize(vec3(.45, .8, .6));
64
+ float diff = max(dot(n, l), 0.);
65
+ float spec = pow(max(dot(reflect(-l, n), v), 0.), 40.);
66
+ col = col * (.55 + .55 * diff) + spec * .8 + fr * .25;
67
+ gl_FragColor = vec4(col, 1.);
68
+ }`;
69
+
70
+ const LiquidBlob3D = ({ colors = ["#a3e635", "#22d3ee", "#a78bfa"], size = 22, speed = 1, distortion = 0.24, detail = 48, interactive = true, className = "" }) => {
71
+ const mount = useRef(null);
72
+ const opts = useRef({ colors, speed, distortion, interactive });
73
+ opts.current = { colors, speed, distortion, interactive };
74
+
75
+ useEffect(() => {
76
+ const el = mount.current;
77
+ const renderer = new WebGLRenderer({ antialias: true, alpha: true, powerPreference: "high-performance" });
78
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
79
+ el.appendChild(renderer.domElement);
80
+ renderer.domElement.style.display = "block";
81
+
82
+ const scene = new Scene();
83
+ const camera = new PerspectiveCamera(35, 1, 0.1, 50);
84
+ camera.position.z = 4.2;
85
+ const uniforms = {
86
+ uTime: { value: 0 },
87
+ uAmp: { value: distortion },
88
+ uFreq: { value: 0.85 },
89
+ uC1: { value: new Color(colors[0]) },
90
+ uC2: { value: new Color(colors[1] ?? colors[0]) },
91
+ uC3: { value: new Color(colors[2] ?? colors[1] ?? colors[0]) },
92
+ };
93
+ const geometry = new IcosahedronGeometry(1, detail);
94
+ const material = new ShaderMaterial({ vertexShader: VERT, fragmentShader: FRAG, uniforms });
95
+ const mesh = new Mesh(geometry, material);
96
+ scene.add(mesh);
97
+
98
+ const resize = () => {
99
+ const { width, height } = el.getBoundingClientRect();
100
+ if (!width || !height) return;
101
+ renderer.setSize(width, height, false);
102
+ renderer.domElement.style.width = "100%";
103
+ renderer.domElement.style.height = "100%";
104
+ camera.aspect = width / height;
105
+ camera.updateProjectionMatrix();
106
+ };
107
+ const ro = new ResizeObserver(resize);
108
+ ro.observe(el);
109
+ resize();
110
+
111
+ const pointer = { x: 0, y: 0, hover: 0 };
112
+ const onMove = (e) => {
113
+ const r = el.getBoundingClientRect();
114
+ pointer.x = ((e.clientX - r.left) / r.width) * 2 - 1;
115
+ pointer.y = ((e.clientY - r.top) / r.height) * 2 - 1;
116
+ pointer.hover = 1;
117
+ };
118
+ const onLeave = () => {
119
+ pointer.x = 0;
120
+ pointer.y = 0;
121
+ pointer.hover = 0;
122
+ };
123
+ el.addEventListener("pointermove", onMove);
124
+ el.addEventListener("pointerleave", onLeave);
125
+
126
+ const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
127
+ let visible = true;
128
+ const io = new IntersectionObserver(([entry]) => (visible = entry.isIntersecting));
129
+ io.observe(el);
130
+
131
+ let raf = 0;
132
+ let last = performance.now();
133
+ let amp = distortion;
134
+ const frame = (now) => {
135
+ raf = requestAnimationFrame(frame);
136
+ const dt = Math.min(0.05, (now - last) / 1000);
137
+ last = now;
138
+ if (!visible) return;
139
+ const o = opts.current;
140
+ uniforms.uTime.value += dt * o.speed * (reduced ? 0.15 : 1);
141
+ const targetAmp = o.distortion * (o.interactive && pointer.hover ? 1.55 : 1);
142
+ amp += (targetAmp - amp) * 0.06;
143
+ uniforms.uAmp.value = amp;
144
+ uniforms.uC1.value.set(o.colors[0]);
145
+ uniforms.uC2.value.set(o.colors[1] ?? o.colors[0]);
146
+ uniforms.uC3.value.set(o.colors[2] ?? o.colors[1] ?? o.colors[0]);
147
+ const tx = o.interactive ? pointer.y * 0.5 : 0;
148
+ const ty = o.interactive ? pointer.x * 0.7 : 0;
149
+ mesh.rotation.x += (tx - mesh.rotation.x) * 0.05;
150
+ mesh.rotation.y += (ty + uniforms.uTime.value * 0.15 - mesh.rotation.y) * 0.05;
151
+ renderer.render(scene, camera);
152
+ };
153
+ raf = requestAnimationFrame(frame);
154
+
155
+ return () => {
156
+ cancelAnimationFrame(raf);
157
+ ro.disconnect();
158
+ io.disconnect();
159
+ el.removeEventListener("pointermove", onMove);
160
+ el.removeEventListener("pointerleave", onLeave);
161
+ geometry.dispose();
162
+ material.dispose();
163
+ renderer.dispose();
164
+ renderer.forceContextLoss();
165
+ renderer.domElement.remove();
166
+ };
167
+ // Colors, speed and distortion are read live from opts; only geometry detail rebuilds.
168
+ // eslint-disable-next-line react-hooks/exhaustive-deps
169
+ }, [detail]);
170
+
171
+ return <div ref={mount} className={`relative touch-none ${className}`} style={{ width: `min(${size}rem, 90vw)`, aspectRatio: "1" }} aria-hidden="true" />;
172
+ };
173
+
174
+ export default LiquidBlob3D;
@@ -52,7 +52,7 @@ function useProgress(progress, duration, loop, onComplete) {
52
52
  if (controlled && progress >= 100) doneRef.current?.();
53
53
  }, [controlled, progress]);
54
54
 
55
- return Math.max(0, Math.min(100, controlled ? progress : auto));
55
+ return Math.round(Math.max(0, Math.min(100, controlled ? progress : auto)));
56
56
  }
57
57
 
58
58
  function Bar({ value, color, track }) {
@@ -64,11 +64,11 @@ function Bar({ value, color, track }) {
64
64
  }
65
65
 
66
66
  // Progress row shared by the game variants.
67
- function Footer({ value, color, track }) {
67
+ function Footer({ value, color, track, statusText }) {
68
68
  return (
69
69
  <div className="mt-5 w-full">
70
70
  <div className="mb-2 flex items-center justify-between text-[0.75rem]">
71
- <span className="opacity-60">{messageFor(value)}…</span>
71
+ <span className="opacity-60">{statusText ?? `${messageFor(value)}…`}</span>
72
72
  <span className="font-mono font-semibold tabular-nums">{value}%</span>
73
73
  </div>
74
74
  <Bar value={value} color={color} track={track} />
@@ -160,7 +160,7 @@ function Blocks({ value, color, track, moving }) {
160
160
 
161
161
  const ORB_LIFE = 1.5; // seconds before an orb escapes
162
162
 
163
- function CatchGame({ value, color, track, dark }) {
163
+ function CatchGame({ value, color, track, dark, statusText }) {
164
164
  const [score, setScore] = useState(0);
165
165
  const [best, setBest] = useState(0);
166
166
  const [combo, setCombo] = useState(0);
@@ -287,14 +287,14 @@ function CatchGame({ value, color, track, dark }) {
287
287
  </div>
288
288
  ))}
289
289
  </motion.div>
290
- <Footer value={value} color={color} track={track} />
290
+ <Footer value={value} color={color} track={track} statusText={statusText} />
291
291
  </div>
292
292
  );
293
293
  }
294
294
 
295
295
  /* --------------------------------------------------------------- Runner game */
296
296
 
297
- function RunnerGame({ value, color, track, dark }) {
297
+ function RunnerGame({ value, color, track, dark, statusText }) {
298
298
  const canvasRef = useRef(null);
299
299
  const jumpRef = useRef(() => {});
300
300
  const [score, setScore] = useState(0);
@@ -542,7 +542,7 @@ function RunnerGame({ value, color, track, dark }) {
542
542
  )}
543
543
  </AnimatePresence>
544
544
  </div>
545
- <Footer value={value} color={color} track={track} />
545
+ <Footer value={value} color={color} track={track} statusText={statusText} />
546
546
  </div>
547
547
  );
548
548
  }
@@ -552,7 +552,7 @@ function RunnerGame({ value, color, track, dark }) {
552
552
  const LANES = 3;
553
553
  const TRAFFIC = ["#f43f5e", "#38bdf8", "#f59e0b", "#a78bfa", "#e5e7eb"];
554
554
 
555
- function RacerGame({ value, color, track, dark }) {
555
+ function RacerGame({ value, color, track, dark, statusText }) {
556
556
  const canvasRef = useRef(null);
557
557
  const moveRef = useRef(() => {});
558
558
  const [score, setScore] = useState(0);
@@ -752,7 +752,7 @@ function RacerGame({ value, color, track, dark }) {
752
752
  )}
753
753
  </AnimatePresence>
754
754
  </div>
755
- <Footer value={value} color={color} track={track} />
755
+ <Footer value={value} color={color} track={track} statusText={statusText} />
756
756
  </div>
757
757
  );
758
758
  }
@@ -767,13 +767,14 @@ const LoadingScreen = ({
767
767
  title = "Loading",
768
768
  fullScreen = false,
769
769
  onComplete,
770
+ statusText,
770
771
  className = "",
771
772
  }) => {
772
773
  const moving = !useReducedMotion();
773
774
  const value = useProgress(progress, duration, loop, onComplete);
774
775
  const dark = theme !== "light";
775
776
  const track = dark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.08)";
776
- const shared = { value, color: accentColor, track, moving, dark };
777
+ const shared = { value, color: accentColor, track, moving, dark, statusText };
777
778
  const game = variant === "catch" || variant === "runner" || variant === "racer";
778
779
 
779
780
  return (
@@ -0,0 +1,78 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+
3
+ /*
4
+ * Real loading progress for <LoadingScreen progress={...} />.
5
+ *
6
+ * Give it the things your page needs before it can show, and it loads them
7
+ * and reports how many are done:
8
+ *
9
+ * const { progress, loaded, total, done } = useLoadProgress([
10
+ * "/hero.jpg", // images are decoded
11
+ * "/data/products.json", // anything else is fetched
12
+ * () => import("./Dashboard.jsx"), // or any function returning a promise
13
+ * ]);
14
+ * <LoadingScreen variant="runner" progress={progress} statusText={`${loaded} of ${total} loaded`} />
15
+ *
16
+ * Items load in parallel; progress is the share that has finished (0 to 100),
17
+ * so the bar and the number always match what really happened. A failed item
18
+ * still counts as finished (see `errors`) so the screen never gets stuck.
19
+ * Pass a stable list (define it outside the component or memoize it).
20
+ */
21
+
22
+ const IMAGE = /\.(png|jpe?g|gif|webp|avif|svg)(\?|#|$)/i;
23
+
24
+ function loadItem(item) {
25
+ if (typeof item === "function") return Promise.resolve().then(item);
26
+ if (item && typeof item.then === "function") return item;
27
+ if (typeof item === "string") {
28
+ if (IMAGE.test(item)) {
29
+ return new Promise((resolve, reject) => {
30
+ const img = new Image();
31
+ img.onload = () => (img.decode ? img.decode().then(resolve, resolve) : resolve());
32
+ img.onerror = reject;
33
+ img.src = item;
34
+ });
35
+ }
36
+ return fetch(item).then((r) => {
37
+ if (!r.ok) throw new Error(`${r.status} ${item}`);
38
+ return r.blob();
39
+ });
40
+ }
41
+ return Promise.resolve();
42
+ }
43
+
44
+ export function useLoadProgress(items, { enabled = true } = {}) {
45
+ const list = useRef(items);
46
+ list.current = items;
47
+ const total = items.length;
48
+ const key = `${total}|${items.map((i) => (typeof i === "string" ? i : typeof i)).join("|")}`;
49
+ const [state, setState] = useState(() => ({ loaded: 0, total, errors: 0, progress: total ? 0 : 100, done: !total }));
50
+
51
+ useEffect(() => {
52
+ if (!enabled) return undefined;
53
+ const work = list.current;
54
+ const count = work.length;
55
+ if (!count) {
56
+ setState({ loaded: 0, total: 0, errors: 0, progress: 100, done: true });
57
+ return undefined;
58
+ }
59
+ let cancelled = false;
60
+ let loaded = 0;
61
+ let errors = 0;
62
+ setState({ loaded: 0, total: count, errors: 0, progress: 0, done: false });
63
+ const finish = (ok) => {
64
+ if (cancelled) return;
65
+ loaded += 1;
66
+ if (!ok) errors += 1;
67
+ setState({ loaded, total: count, errors, progress: Math.round((loaded / count) * 100), done: loaded === count });
68
+ };
69
+ work.forEach((item) => loadItem(item).then(() => finish(true), () => finish(false)));
70
+ return () => {
71
+ cancelled = true;
72
+ };
73
+ }, [key, enabled]);
74
+
75
+ return state;
76
+ }
77
+
78
+ export default useLoadProgress;
@@ -0,0 +1,216 @@
1
+ import React, { useEffect, useRef, useState } from "react";
2
+ import { AdditiveBlending, BufferAttribute, BufferGeometry, Color, PerspectiveCamera, Points, Scene, ShaderMaterial, WebGLRenderer } from "three";
3
+
4
+ /*
5
+ * Particle Morph: thousands of glowing particles that flow between shapes:
6
+ * sphere, cube, torus, DNA helix and a wave. It morphs on a timer or when
7
+ * you click, drifts with the pointer, and each particle takes a slightly
8
+ * different path so the change looks like a swarm, not a snap. One draw
9
+ * call; pauses off screen and frees the GPU on unmount.
10
+ */
11
+
12
+ const SHAPES = {
13
+ sphere: (i, n) => {
14
+ // Fibonacci sphere: even spacing.
15
+ const y = 1 - (i / (n - 1)) * 2;
16
+ const r = Math.sqrt(1 - y * y);
17
+ const t = i * 2.399963;
18
+ return [Math.cos(t) * r * 1.6, y * 1.6, Math.sin(t) * r * 1.6];
19
+ },
20
+ cube: () => {
21
+ const s = 1.25;
22
+ const p = [(Math.random() * 2 - 1) * s, (Math.random() * 2 - 1) * s, (Math.random() * 2 - 1) * s];
23
+ p[Math.floor(Math.random() * 3)] = Math.sign(Math.random() - 0.5) * s;
24
+ return p;
25
+ },
26
+ torus: () => {
27
+ const u = Math.random() * Math.PI * 2;
28
+ const v = Math.random() * Math.PI * 2;
29
+ const R = 1.3;
30
+ const r = 0.45;
31
+ return [(R + r * Math.cos(v)) * Math.cos(u), r * Math.sin(v), (R + r * Math.cos(v)) * Math.sin(u)];
32
+ },
33
+ helix: (i, n) => {
34
+ const t = (i / n) * Math.PI * 8;
35
+ const y = (i / n) * 3.4 - 1.7;
36
+ const strand = i % 3;
37
+ if (strand === 2) {
38
+ // Rungs between the two strands.
39
+ const k = Math.random();
40
+ const a = Math.round(t / 0.6) * 0.6;
41
+ return [Math.cos(a) * 0.8 * (1 - 2 * k), y, Math.sin(a) * 0.8 * (1 - 2 * k)];
42
+ }
43
+ const a = t + strand * Math.PI;
44
+ return [Math.cos(a) * 0.8, y, Math.sin(a) * 0.8];
45
+ },
46
+ wave: (i, n) => {
47
+ const side = Math.ceil(Math.sqrt(n));
48
+ const x = ((i % side) / side) * 4 - 2;
49
+ const z = (Math.floor(i / side) / side) * 4 - 2;
50
+ return [x, Math.sin(x * 2) * Math.cos(z * 2) * 0.35, z];
51
+ },
52
+ };
53
+
54
+ const VERT = `
55
+ attribute float aSeed;
56
+ uniform float uTime; uniform float uSize; uniform float uDpr;
57
+ varying float vSeed;
58
+ void main(){
59
+ vSeed = aSeed;
60
+ vec3 p = position;
61
+ p += 0.012 * vec3(sin(uTime * 1.3 + aSeed * 30.), cos(uTime * 1.1 + aSeed * 20.), sin(uTime * .9 + aSeed * 40.));
62
+ vec4 mv = modelViewMatrix * vec4(p, 1.);
63
+ gl_PointSize = uSize * uDpr * (0.6 + aSeed * 0.8) * (11. / -mv.z);
64
+ gl_Position = projectionMatrix * mv;
65
+ }`;
66
+
67
+ const FRAG = `
68
+ uniform vec3 uC1; uniform vec3 uC2;
69
+ varying float vSeed;
70
+ void main(){
71
+ float d = length(gl_PointCoord - .5);
72
+ float a = smoothstep(.5, .0, d) * .85;
73
+ vec3 col = mix(uC1, uC2, vSeed);
74
+ gl_FragColor = vec4(col * a, a);
75
+ }`;
76
+
77
+ const ORDER = ["sphere", "cube", "torus", "helix", "wave"];
78
+
79
+ const ParticleMorph = ({ shapes = ORDER, count = 6000, colors = ["#a3e635", "#22d3ee"], interval = 3500, pointSize = 3, autoPlay = true, height = 26, className = "" }) => {
80
+ const mount = useRef(null);
81
+ const api = useRef(null);
82
+ const [index, setIndex] = useState(0);
83
+ const list = shapes.filter((s) => SHAPES[s]);
84
+ const opts = useRef({ colors, pointSize });
85
+ opts.current = { colors, pointSize };
86
+
87
+ useEffect(() => {
88
+ const el = mount.current;
89
+ const renderer = new WebGLRenderer({ antialias: false, alpha: true, powerPreference: "high-performance" });
90
+ const dpr = Math.min(window.devicePixelRatio || 1, 2);
91
+ renderer.setPixelRatio(dpr);
92
+ el.appendChild(renderer.domElement);
93
+ renderer.domElement.style.display = "block";
94
+ const scene = new Scene();
95
+ const camera = new PerspectiveCamera(40, 1, 0.1, 50);
96
+ camera.position.z = 6;
97
+
98
+ const n = count;
99
+ const pos = new Float32Array(n * 3);
100
+ const seeds = new Float32Array(n);
101
+ const targets = list.map((s) => {
102
+ const arr = new Float32Array(n * 3);
103
+ for (let i = 0; i < n; i++) arr.set(SHAPES[s](i, n), i * 3);
104
+ return arr;
105
+ });
106
+ pos.set(targets[0]);
107
+ for (let i = 0; i < n; i++) seeds[i] = Math.random();
108
+ const geometry = new BufferGeometry();
109
+ geometry.setAttribute("position", new BufferAttribute(pos, 3));
110
+ geometry.setAttribute("aSeed", new BufferAttribute(seeds, 1));
111
+ const uniforms = { uTime: { value: 0 }, uSize: { value: pointSize }, uDpr: { value: dpr }, uC1: { value: new Color(colors[0]) }, uC2: { value: new Color(colors[1] ?? colors[0]) } };
112
+ const material = new ShaderMaterial({ vertexShader: VERT, fragmentShader: FRAG, uniforms, transparent: true, depthWrite: false, blending: AdditiveBlending });
113
+ const points = new Points(geometry, material);
114
+ scene.add(points);
115
+
116
+ const resize = () => {
117
+ const { width, height: h } = el.getBoundingClientRect();
118
+ if (!width || !h) return;
119
+ renderer.setSize(width, h, false);
120
+ renderer.domElement.style.width = "100%";
121
+ renderer.domElement.style.height = "100%";
122
+ camera.aspect = width / h;
123
+ // Keep the shape in frame on narrow screens.
124
+ camera.position.z = width / h < 1 ? 6 / Math.max(0.55, width / h) : 6;
125
+ camera.updateProjectionMatrix();
126
+ };
127
+ const ro = new ResizeObserver(resize);
128
+ ro.observe(el);
129
+ resize();
130
+
131
+ const pointer = { x: 0, y: 0 };
132
+ const onMove = (e) => {
133
+ const r = el.getBoundingClientRect();
134
+ pointer.x = ((e.clientX - r.left) / r.width) * 2 - 1;
135
+ pointer.y = ((e.clientY - r.top) / r.height) * 2 - 1;
136
+ };
137
+ el.addEventListener("pointermove", onMove);
138
+ let visible = true;
139
+ const io = new IntersectionObserver(([entry]) => (visible = entry.isIntersecting));
140
+ io.observe(el);
141
+
142
+ let target = targets[0];
143
+ // Per-particle speed so the swarm flows rather than snapping.
144
+ const rate = new Float32Array(n);
145
+ for (let i = 0; i < n; i++) rate[i] = 0.025 + seeds[i] * 0.05;
146
+ api.current = { go: (k) => (target = targets[((k % targets.length) + targets.length) % targets.length]) };
147
+
148
+ let raf = 0;
149
+ let last = performance.now();
150
+ const frame = (now) => {
151
+ raf = requestAnimationFrame(frame);
152
+ const dt = Math.min(0.05, (now - last) / 1000);
153
+ last = now;
154
+ if (!visible) return;
155
+ uniforms.uTime.value += dt;
156
+ uniforms.uSize.value = opts.current.pointSize;
157
+ uniforms.uC1.value.set(opts.current.colors[0]);
158
+ uniforms.uC2.value.set(opts.current.colors[1] ?? opts.current.colors[0]);
159
+ for (let i = 0; i < n * 3; i++) pos[i] += (target[i] - pos[i]) * rate[(i / 3) | 0] * (dt * 60);
160
+ geometry.attributes.position.needsUpdate = true;
161
+ points.rotation.y += dt * 0.15;
162
+ points.rotation.x += (pointer.y * 0.4 - points.rotation.x) * 0.04;
163
+ points.rotation.z += (-pointer.x * 0.15 - points.rotation.z) * 0.04;
164
+ renderer.render(scene, camera);
165
+ };
166
+ raf = requestAnimationFrame(frame);
167
+
168
+ return () => {
169
+ cancelAnimationFrame(raf);
170
+ ro.disconnect();
171
+ io.disconnect();
172
+ el.removeEventListener("pointermove", onMove);
173
+ geometry.dispose();
174
+ material.dispose();
175
+ renderer.dispose();
176
+ renderer.forceContextLoss();
177
+ renderer.domElement.remove();
178
+ api.current = null;
179
+ };
180
+ // Rebuild only when the particle set changes; colors and size are live.
181
+ // eslint-disable-next-line react-hooks/exhaustive-deps
182
+ }, [count, list.join(",")]);
183
+
184
+ useEffect(() => {
185
+ api.current?.go(index);
186
+ }, [index]);
187
+
188
+ useEffect(() => {
189
+ if (!autoPlay) return undefined;
190
+ const id = setInterval(() => setIndex((i) => i + 1), interval);
191
+ return () => clearInterval(id);
192
+ }, [autoPlay, interval]);
193
+
194
+ const current = list[((index % list.length) + list.length) % list.length];
195
+ return (
196
+ <div className={`relative flex w-full flex-col items-center gap-4 ${className}`}>
197
+ <div ref={mount} className="w-full cursor-pointer touch-none" style={{ height: `${height}rem` }} onClick={() => setIndex((i) => i + 1)} aria-hidden="true" />
198
+ <div className="flex flex-wrap justify-center gap-1.5" role="group" aria-label="Shape">
199
+ {list.map((s, i) => (
200
+ <button
201
+ key={s}
202
+ type="button"
203
+ onClick={() => setIndex(i)}
204
+ aria-pressed={current === s}
205
+ className={`rounded-full px-3 py-1 text-xs font-medium capitalize transition-colors ${current === s ? "text-black" : "opacity-60 hover:opacity-100"}`}
206
+ style={current === s ? { background: colors[0] } : undefined}
207
+ >
208
+ {s}
209
+ </button>
210
+ ))}
211
+ </div>
212
+ </div>
213
+ );
214
+ };
215
+
216
+ export default ParticleMorph;
@@ -16,14 +16,21 @@ const DEFAULT_PLANS = [
16
16
 
17
17
  function Price({ value, currency }) {
18
18
  return (
19
- <span className="relative inline-flex h-[1.1em] overflow-hidden align-bottom tabular-nums">
20
- <AnimatePresence mode="popLayout" initial={false}>
19
+ // Old and new numbers share one grid cell (no layout popping, no flicker);
20
+ // an invisible copy of the current value sets the width.
21
+ <span className="relative inline-grid h-[1.1em] overflow-hidden align-bottom tabular-nums">
22
+ <span aria-hidden="true" className="invisible col-start-1 row-start-1">
23
+ {currency}
24
+ {value}
25
+ </span>
26
+ <AnimatePresence initial={false}>
21
27
  <motion.span
22
28
  key={value}
29
+ className="col-start-1 row-start-1"
23
30
  initial={{ y: "100%", opacity: 0 }}
24
- animate={{ y: 0, opacity: 1 }}
31
+ animate={{ y: "0%", opacity: 1 }}
25
32
  exit={{ y: "-100%", opacity: 0 }}
26
- transition={{ type: "spring", stiffness: 380, damping: 30 }}
33
+ transition={{ type: "spring", stiffness: 380, damping: 32 }}
27
34
  >
28
35
  {currency}
29
36
  {value}
@@ -0,0 +1,158 @@
1
+ import React, { useEffect, useRef } from "react";
2
+ import { Color, DoubleSide, Mesh, PerspectiveCamera, PlaneGeometry, Scene, ShaderMaterial, WebGLRenderer } from "three";
3
+
4
+ /*
5
+ * Retro Grid 3D: a synthwave landscape. Glowing grid lines run over rolling
6
+ * hills that scroll toward you, with a flat road down the middle and a
7
+ * striped sun on the horizon. Two planes and two small shaders, so it's
8
+ * cheap to run as a hero background. Put your content on top as children.
9
+ */
10
+
11
+ const NOISE = `
12
+ float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
13
+ float noise(vec2 p){ vec2 i = floor(p); vec2 f = fract(p); vec2 u = f * f * (3. - 2. * f);
14
+ return mix(mix(hash(i), hash(i + vec2(1., 0.)), u.x), mix(hash(i + vec2(0., 1.)), hash(i + vec2(1., 1.)), u.x), u.y); }`;
15
+
16
+ const GROUND_VERT = `
17
+ uniform float uTime; uniform float uSpeed; uniform float uHeight;
18
+ varying vec2 vGrid; varying float vDepth; varying float vH;
19
+ ${NOISE}
20
+ void main(){
21
+ vec3 p = position;
22
+ float z = p.y - uTime * uSpeed; // plane is rotated, so y is depth
23
+ float road = smoothstep(1.2, 3.2, abs(p.x)); // flat strip in the middle
24
+ float h = (noise(vec2(p.x * .35, z * .35)) * .7 + noise(vec2(p.x * .9, z * .9)) * .3) * uHeight * road;
25
+ p.z += h;
26
+ vH = h / max(uHeight, .001);
27
+ vGrid = vec2(p.x, z);
28
+ vec4 mv = modelViewMatrix * vec4(p, 1.);
29
+ vDepth = -mv.z;
30
+ gl_Position = projectionMatrix * mv;
31
+ }`;
32
+
33
+ const GROUND_FRAG = `
34
+ uniform vec3 uLine; uniform vec3 uGround; uniform float uFog;
35
+ varying vec2 vGrid; varying float vDepth; varying float vH;
36
+ void main(){
37
+ vec2 g = abs(fract(vGrid) - .5) / fwidth(vGrid);
38
+ float line = 1. - min(min(g.x, g.y), 1.);
39
+ vec3 col = mix(uGround, uGround * 1.6, vH);
40
+ col += uLine * line * 1.4;
41
+ float fog = smoothstep(uFog * .25, uFog, vDepth);
42
+ gl_FragColor = vec4(col, 1. - fog);
43
+ }`;
44
+
45
+ const SUN_VERT = `varying vec2 vUv; void main(){ vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.); }`;
46
+ const SUN_FRAG = `
47
+ uniform vec3 uTop; uniform vec3 uBottom; uniform float uTime;
48
+ varying vec2 vUv;
49
+ void main(){
50
+ vec2 c = vUv - .5;
51
+ float d = length(c);
52
+ float disc = smoothstep(.5, .49, d);
53
+ // Horizontal gaps in the lower half that widen toward the bottom and drift down.
54
+ float y = vUv.y;
55
+ float t = clamp((.55 - y) / .55, 0., 1.);
56
+ float gap = y < .55 ? step(1. - t * .75, fract(y * 16. + uTime * .3)) : 0.;
57
+ vec3 col = mix(uBottom, uTop, y);
58
+ float glow = smoothstep(.75, .3, d) * .35;
59
+ float a = disc * (1. - gap);
60
+ gl_FragColor = vec4(col * (a + glow), a + glow * .6);
61
+ }`;
62
+
63
+ const RetroGrid3D = ({ lineColor = "#e879f9", groundColor = "#0b0618", sunColors = ["#fde047", "#f43f5e"], skyColors = ["#1e0b3a", "#05020d"], speed = 1.2, hills = 1.4, showSun = true, height = "100%", children, className = "" }) => {
64
+ const mount = useRef(null);
65
+ const opts = useRef({ lineColor, groundColor, sunColors, speed, hills });
66
+ opts.current = { lineColor, groundColor, sunColors, speed, hills };
67
+
68
+ useEffect(() => {
69
+ const el = mount.current;
70
+ const renderer = new WebGLRenderer({ antialias: true, alpha: true, powerPreference: "high-performance" });
71
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 1.75));
72
+ el.appendChild(renderer.domElement);
73
+ Object.assign(renderer.domElement.style, { display: "block", position: "absolute", inset: "0" });
74
+ const scene = new Scene();
75
+ const camera = new PerspectiveCamera(60, 1, 0.1, 80);
76
+ camera.position.set(0, 1.25, 5);
77
+ camera.lookAt(0, 1.0, -10);
78
+
79
+ const gUniforms = { uTime: { value: 0 }, uSpeed: { value: speed }, uHeight: { value: hills }, uLine: { value: new Color(lineColor) }, uGround: { value: new Color(groundColor) }, uFog: { value: 34 } };
80
+ const groundGeo = new PlaneGeometry(40, 60, 80, 120);
81
+ const groundMat = new ShaderMaterial({ vertexShader: GROUND_VERT, fragmentShader: GROUND_FRAG, uniforms: gUniforms, transparent: true });
82
+ const ground = new Mesh(groundGeo, groundMat);
83
+ ground.rotation.x = -Math.PI / 2;
84
+ ground.position.z = -22;
85
+ scene.add(ground);
86
+
87
+ const sUniforms = { uTop: { value: new Color(sunColors[0]) }, uBottom: { value: new Color(sunColors[1] ?? sunColors[0]) }, uTime: { value: 0 } };
88
+ const sunGeo = new PlaneGeometry(14, 14);
89
+ const sunMat = new ShaderMaterial({ vertexShader: SUN_VERT, fragmentShader: SUN_FRAG, uniforms: sUniforms, transparent: true, depthWrite: false, side: DoubleSide });
90
+ const sun = new Mesh(sunGeo, sunMat);
91
+ sun.position.set(0, 4.2, -48);
92
+ sun.visible = showSun;
93
+ scene.add(sun);
94
+
95
+ const resize = () => {
96
+ const { width, height: h } = el.getBoundingClientRect();
97
+ if (!width || !h) return;
98
+ renderer.setSize(width, h, false);
99
+ renderer.domElement.style.width = "100%";
100
+ renderer.domElement.style.height = "100%";
101
+ camera.aspect = width / h;
102
+ camera.fov = width / h < 0.8 ? 75 : 60;
103
+ camera.updateProjectionMatrix();
104
+ };
105
+ const ro = new ResizeObserver(resize);
106
+ ro.observe(el);
107
+ resize();
108
+
109
+ const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
110
+ let visible = true;
111
+ const io = new IntersectionObserver(([entry]) => (visible = entry.isIntersecting));
112
+ io.observe(el);
113
+
114
+ let raf = 0;
115
+ let last = performance.now();
116
+ const frame = (now) => {
117
+ raf = requestAnimationFrame(frame);
118
+ const dt = Math.min(0.05, (now - last) / 1000);
119
+ last = now;
120
+ if (!visible) return;
121
+ const o = opts.current;
122
+ gUniforms.uTime.value += dt * (reduced ? 0.1 : 1);
123
+ sUniforms.uTime.value = gUniforms.uTime.value;
124
+ gUniforms.uSpeed.value = o.speed;
125
+ gUniforms.uHeight.value = o.hills;
126
+ gUniforms.uLine.value.set(o.lineColor);
127
+ gUniforms.uGround.value.set(o.groundColor);
128
+ sUniforms.uTop.value.set(o.sunColors[0]);
129
+ sUniforms.uBottom.value.set(o.sunColors[1] ?? o.sunColors[0]);
130
+ renderer.render(scene, camera);
131
+ };
132
+ raf = requestAnimationFrame(frame);
133
+
134
+ return () => {
135
+ cancelAnimationFrame(raf);
136
+ ro.disconnect();
137
+ io.disconnect();
138
+ groundGeo.dispose();
139
+ groundMat.dispose();
140
+ sunGeo.dispose();
141
+ sunMat.dispose();
142
+ renderer.dispose();
143
+ renderer.forceContextLoss();
144
+ renderer.domElement.remove();
145
+ };
146
+ // Colors, speed and hills are read live from opts.
147
+ // eslint-disable-next-line react-hooks/exhaustive-deps
148
+ }, [showSun]);
149
+
150
+ return (
151
+ <div className={`relative isolate w-full overflow-hidden ${className}`} style={{ height, background: `linear-gradient(to bottom, ${skyColors[1] ?? skyColors[0]} 0%, ${skyColors[0]} 55%, ${groundColor} 100%)` }}>
152
+ <div ref={mount} aria-hidden="true" className="absolute inset-0 -z-10" />
153
+ {children && <div className="relative flex h-full w-full items-center justify-center">{children}</div>}
154
+ </div>
155
+ );
156
+ };
157
+
158
+ export default RetroGrid3D;
@@ -17,6 +17,8 @@ const ShinyText = ({ text = "Shipping soon, join the waitlist", as = "span", spe
17
17
  const band = Math.min(16, Math.max(4, spread));
18
18
  return (
19
19
  <Tag
20
+ // A new key restarts the loop, so speed / pause changes apply right away.
21
+ key={`${speed}-${pause}-${still}`}
20
22
  className={`inline-block bg-clip-text text-transparent ${className}`}
21
23
  style={{
22
24
  backgroundImage: `linear-gradient(100deg, ${color} ${50 - band}%, ${shineColor} 50%, ${color} ${50 + band}%)`,
@@ -16,13 +16,14 @@ const StarBorder = ({ as = "button", color = "#a3e635", speed = 5, thickness = 1
16
16
  animate: reduced ? undefined : { x: dir > 0 ? ["-35%", "35%"] : ["35%", "-35%"], opacity: [0, 1, 1, 0] },
17
17
  transition: { duration: speed, ease: "linear", repeat: Infinity, times: [0, 0.15, 0.85, 1] },
18
18
  });
19
+ // Keyed on speed below: a looping animation keeps its old duration otherwise.
19
20
  const top = star(-1);
20
21
  const bottom = star(1);
21
22
 
22
23
  return (
23
24
  <Tag className={`group relative inline-block overflow-hidden rounded-[1.1rem] ${className}`} style={{ padding: `${thickness}px 0` }} {...(as === "button" ? { type: "button" } : {})} {...rest}>
24
- <motion.span aria-hidden="true" className="pointer-events-none absolute -bottom-[11px] left-[-100%] h-1/2 w-[300%] opacity-70" style={{ background: bottom.background }} animate={bottom.animate} transition={bottom.transition} />
25
- <motion.span aria-hidden="true" className="pointer-events-none absolute -top-[10px] left-[-100%] h-1/2 w-[300%] opacity-70" style={{ background: top.background }} animate={top.animate} transition={top.transition} />
25
+ <motion.span key={`b-${speed}`} aria-hidden="true" className="pointer-events-none absolute -bottom-[11px] left-[-100%] h-1/2 w-[300%] opacity-70" style={{ background: bottom.background }} animate={bottom.animate} transition={bottom.transition} />
26
+ <motion.span key={`t-${speed}`} aria-hidden="true" className="pointer-events-none absolute -top-[10px] left-[-100%] h-1/2 w-[300%] opacity-70" style={{ background: top.background }} animate={top.animate} transition={top.transition} />
26
27
  <span className={`relative z-[1] block rounded-[1.1rem] border px-7 py-3.5 text-center text-[0.95rem] font-semibold transition-colors ${dark ? "border-white/10 bg-gradient-to-b from-neutral-900 to-black text-white group-hover:border-white/20" : "border-black/10 bg-gradient-to-b from-white to-neutral-100 text-neutral-900 group-hover:border-black/20"}`}>
27
28
  {children}
28
29
  </span>
package/dist/cli/index.js CHANGED
@@ -114,6 +114,9 @@ const componentMap = {
114
114
  "bounce-cards": "BounceCards",
115
115
  "card-swap-3d": "CardSwap3D",
116
116
  "gooey-nav": "GooeyNav",
117
+ "liquid-blob-3d": "LiquidBlob3D",
118
+ "particle-morph": "ParticleMorph",
119
+ "retro-grid-3d": "RetroGrid3D",
117
120
  "water-drop-reveal": "MaskView",
118
121
  };
119
122
  // Packages only some components need, installed on top of the shared ones.
package/package.json CHANGED
@@ -1,24 +1,32 @@
1
1
  {
2
2
  "name": "apex-ui-kit",
3
- "version": "2.3.1",
3
+ "version": "2.4.0",
4
4
  "license": "MIT",
5
- "description": "Apex UI Kit - A React component library for building modern user interfaces",
5
+ "description": "93 free, animated UI components for React, styled with Tailwind CSS. Add any of them with one CLI command.",
6
+ "homepage": "https://apex-ui.in",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Mohan-Kumar-Dalei/ApexUI.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/Mohan-Kumar-Dalei/ApexUI/issues"
13
+ },
6
14
  "keywords": [
7
15
  "react",
8
- "ui",
9
- "components",
10
- "library",
11
- "design",
12
- "kit",
13
- "modern",
14
- "user interface",
15
- "frontend",
16
- "web development",
17
16
  "react components",
17
+ "tailwind",
18
+ "tailwindcss",
19
+ "framer-motion",
20
+ "animation",
21
+ "animated components",
22
+ "ui",
23
+ "ui library",
18
24
  "ui components",
19
- "react ui library",
20
- "apex ui kit",
21
- "ui components library"
25
+ "cli",
26
+ "webgl",
27
+ "ai ui",
28
+ "apexui",
29
+ "apex ui kit"
22
30
  ],
23
31
  "type": "module",
24
32
  "main": "dist/cli/index.js",