@stackvibe-studio/motion 0.2.0 → 0.3.1

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,35 +1,64 @@
1
1
  # @stackvibe-studio/motion
2
2
 
3
- Motion primitives and reveal patterns for premium product experiences.
3
+ > Physics-based motion and animation primitives for StackVibe UI, powered by Framer Motion.
4
4
 
5
- ## Highlights
5
+ [![npm version](https://img.shields.io/npm/v/@stackvibe-studio/motion.svg)](https://www.npmjs.com/package/@stackvibe-studio/motion)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
7
 
7
- - Scroll-aware reveal sections
8
- - Spotlight cards with soft glow lighting
9
- - Clean text reveal utilities
10
- - New in v0.2.0: FadeInSection for storytelling and landing pages
11
-
12
- ## Install
8
+ ## Installation
13
9
 
14
10
  ```bash
15
- npm install @stackvibe-studio/motion
11
+ npm install @stackvibe-studio/motion framer-motion
12
+ # or
13
+ pnpm add @stackvibe-studio/motion framer-motion
16
14
  ```
17
15
 
18
- ## Example
16
+ ## Primitives Included
17
+
18
+ - **`Magnetic`**: Physics-based cursor attraction that pulls buttons or badges toward the pointer.
19
+ - **`TiltCard`**: 3D perspective mouse-tracking tilt card with realistic specular glare reflection.
20
+ - **`NumberTicker`**: Smooth animated counter from 0 to N with cubic easing for metric showcases.
21
+ - **`Marquee`**: Infinite looping ticker for partners, logos, or news with pause-on-hover.
22
+ - **`SpotlightCard`**: Interactive card with dynamic radial cursor spotlight illumination.
23
+ - **`FadeIn`**: Scroll-aware reveal wrapper for onboarding sections and storytelling layouts.
24
+ - **`TextReveal`**: Typography entry animation for titles and headings.
25
+
26
+ ## Usage
19
27
 
20
28
  ```tsx
21
- import { FadeInSection, SpotlightCard } from '@stackvibe-studio/motion';
29
+ import { Magnetic, TiltCard, NumberTicker, Marquee } from '@stackvibe-studio/motion';
30
+ import { Button } from '@stackvibe-studio/ui';
22
31
 
23
- export function Example() {
32
+ export function MotionDemo() {
24
33
  return (
25
- <FadeInSection>
26
- <SpotlightCard className="p-6">Premium motion without the bloat.</SpotlightCard>
27
- </FadeInSection>
34
+ <div className="space-y-6">
35
+ <Magnetic strength={0.4}>
36
+ <Button size="lg">Magnetic Button</Button>
37
+ </Magnetic>
38
+
39
+ <TiltCard maxTilt={15} glare>
40
+ <h3>3D Tilt Surface</h3>
41
+ <p>Interactive mouse perspective.</p>
42
+ </TiltCard>
43
+
44
+ <div className="text-3xl font-bold">
45
+ <NumberTicker value={25000} prefix="$" suffix=" ARR" />
46
+ </div>
47
+
48
+ <Marquee speed={20}>
49
+ <span>Item 1</span>
50
+ <span>Item 2</span>
51
+ <span>Item 3</span>
52
+ </Marquee>
53
+ </div>
28
54
  );
29
55
  }
30
56
  ```
31
57
 
32
- ## Related packages
58
+ ## Documentation
59
+
60
+ [https://stackvibe-studio.github.io/stackvibe-ui/docs](https://stackvibe-studio.github.io/stackvibe-ui/docs)
61
+
62
+ ## License
33
63
 
34
- - @stackvibe-studio/ui
35
- - @stackvibe-studio/three
64
+ MIT © [StackVibe Studio](https://github.com/StackVibe-Studio)
@@ -0,0 +1,11 @@
1
+ import { type ReactNode } from 'react';
2
+ export interface MagneticProps {
3
+ children: ReactNode;
4
+ strength?: number;
5
+ damping?: number;
6
+ stiffness?: number;
7
+ mass?: number;
8
+ disabled?: boolean;
9
+ className?: string;
10
+ }
11
+ export declare function Magnetic({ children, strength, damping, stiffness, mass, disabled, className, }: MagneticProps): import("react").JSX.Element;
@@ -0,0 +1,9 @@
1
+ import { type ReactNode } from 'react';
2
+ export interface MarqueeProps {
3
+ children: ReactNode;
4
+ speed?: number;
5
+ direction?: 'left' | 'right';
6
+ pauseOnHover?: boolean;
7
+ className?: string;
8
+ }
9
+ export declare function Marquee({ children, speed, direction, pauseOnHover, className, }: MarqueeProps): import("react").JSX.Element;
@@ -0,0 +1,8 @@
1
+ export interface NumberTickerProps {
2
+ value: number;
3
+ duration?: number;
4
+ className?: string;
5
+ prefix?: string;
6
+ suffix?: string;
7
+ }
8
+ export declare function NumberTicker({ value, duration, className, prefix, suffix, }: NumberTickerProps): import("react").JSX.Element;
@@ -0,0 +1,13 @@
1
+ import { type ReactNode } from 'react';
2
+ export interface TiltCardProps {
3
+ children: ReactNode;
4
+ maxTilt?: number;
5
+ perspective?: number;
6
+ scale?: number;
7
+ glare?: boolean;
8
+ glareOpacity?: number;
9
+ glareColor?: string;
10
+ speed?: number;
11
+ className?: string;
12
+ }
13
+ export declare function TiltCard({ children, maxTilt, perspective, scale, glare, glareOpacity, glareColor, speed, className, }: TiltCardProps): import("react").JSX.Element;
package/dist/index.cjs CHANGED
@@ -22,8 +22,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
22
22
  var index_exports = {};
23
23
  __export(index_exports, {
24
24
  FadeInSection: () => FadeInSection,
25
+ Magnetic: () => Magnetic,
26
+ Marquee: () => Marquee,
27
+ NumberTicker: () => NumberTicker,
25
28
  SpotlightCard: () => SpotlightCard,
26
- TextReveal: () => TextReveal
29
+ TextReveal: () => TextReveal,
30
+ TiltCard: () => TiltCard
27
31
  });
28
32
  module.exports = __toCommonJS(index_exports);
29
33
 
@@ -51,9 +55,136 @@ function FadeInSection({
51
55
  );
52
56
  }
53
57
 
54
- // src/components/spotlight-card.tsx
58
+ // src/components/magnetic.tsx
55
59
  var import_react2 = require("motion/react");
60
+ var import_react3 = require("react");
56
61
  var import_jsx_runtime2 = require("react/jsx-runtime");
62
+ function Magnetic({
63
+ children,
64
+ strength = 0.35,
65
+ damping = 15,
66
+ stiffness = 150,
67
+ mass = 0.1,
68
+ disabled = false,
69
+ className = ""
70
+ }) {
71
+ const ref = (0, import_react3.useRef)(null);
72
+ const x = (0, import_react2.useMotionValue)(0);
73
+ const y = (0, import_react2.useMotionValue)(0);
74
+ const springConfig = { damping, stiffness, mass };
75
+ const springX = (0, import_react2.useSpring)(x, springConfig);
76
+ const springY = (0, import_react2.useSpring)(y, springConfig);
77
+ const handleMouseMove = (e) => {
78
+ if (disabled || !ref.current) return;
79
+ const { clientX, clientY } = e;
80
+ const { left, top, width, height } = ref.current.getBoundingClientRect();
81
+ const middleX = clientX - (left + width / 2);
82
+ const middleY = clientY - (top + height / 2);
83
+ x.set(middleX * strength);
84
+ y.set(middleY * strength);
85
+ };
86
+ const handleMouseLeave = () => {
87
+ x.set(0);
88
+ y.set(0);
89
+ };
90
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
91
+ import_react2.motion.div,
92
+ {
93
+ ref,
94
+ onMouseMove: handleMouseMove,
95
+ onMouseLeave: handleMouseLeave,
96
+ style: { x: springX, y: springY },
97
+ className: ["inline-block", className].join(" "),
98
+ children
99
+ }
100
+ );
101
+ }
102
+
103
+ // src/components/marquee.tsx
104
+ var import_jsx_runtime3 = require("react/jsx-runtime");
105
+ function Marquee({
106
+ children,
107
+ speed = 25,
108
+ direction = "left",
109
+ pauseOnHover = true,
110
+ className = ""
111
+ }) {
112
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
113
+ "div",
114
+ {
115
+ className: [
116
+ "group flex overflow-hidden select-none [mask-image:linear-gradient(to_right,transparent,white_10%,white_90%,transparent)]",
117
+ className
118
+ ].join(" "),
119
+ children: [
120
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
121
+ "div",
122
+ {
123
+ className: [
124
+ "flex min-w-full shrink-0 items-center justify-around gap-8",
125
+ direction === "left" ? "animate-marquee" : "animate-marquee-reverse",
126
+ pauseOnHover ? "group-hover:[animation-play-state:paused]" : ""
127
+ ].join(" "),
128
+ style: { animationDuration: `${speed}s` },
129
+ children
130
+ }
131
+ ),
132
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
133
+ "div",
134
+ {
135
+ "aria-hidden": "true",
136
+ className: [
137
+ "flex min-w-full shrink-0 items-center justify-around gap-8",
138
+ direction === "left" ? "animate-marquee" : "animate-marquee-reverse",
139
+ pauseOnHover ? "group-hover:[animation-play-state:paused]" : ""
140
+ ].join(" "),
141
+ style: { animationDuration: `${speed}s` },
142
+ children
143
+ }
144
+ )
145
+ ]
146
+ }
147
+ );
148
+ }
149
+
150
+ // src/components/number-ticker.tsx
151
+ var import_react4 = require("react");
152
+ var import_jsx_runtime4 = require("react/jsx-runtime");
153
+ function NumberTicker({
154
+ value,
155
+ duration = 1500,
156
+ className = "",
157
+ prefix = "",
158
+ suffix = ""
159
+ }) {
160
+ const [current, setCurrent] = (0, import_react4.useState)(0);
161
+ const startTimeRef = (0, import_react4.useRef)(null);
162
+ (0, import_react4.useEffect)(() => {
163
+ let animationFrame;
164
+ const animate = (timestamp) => {
165
+ if (!startTimeRef.current) startTimeRef.current = timestamp;
166
+ const progress = Math.min((timestamp - startTimeRef.current) / duration, 1);
167
+ const easeOut = 1 - Math.pow(1 - progress, 3);
168
+ setCurrent(Math.floor(easeOut * value));
169
+ if (progress < 1) {
170
+ animationFrame = requestAnimationFrame(animate);
171
+ } else {
172
+ setCurrent(value);
173
+ }
174
+ };
175
+ animationFrame = requestAnimationFrame(animate);
176
+ return () => cancelAnimationFrame(animationFrame);
177
+ }, [value, duration]);
178
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: ["tabular-nums font-mono", className].join(" "), children: [
179
+ prefix,
180
+ current.toLocaleString(),
181
+ suffix
182
+ ] });
183
+ }
184
+
185
+ // src/components/spotlight-card.tsx
186
+ var import_react5 = require("motion/react");
187
+ var import_jsx_runtime5 = require("react/jsx-runtime");
57
188
  function SpotlightCard({
58
189
  children,
59
190
  glowColor = "#8b5cf6",
@@ -62,8 +193,8 @@ function SpotlightCard({
62
193
  className = "",
63
194
  ...props
64
195
  }) {
65
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
66
- import_react2.motion.div,
196
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
197
+ import_react5.motion.div,
67
198
  {
68
199
  ...props,
69
200
  whileHover: { scale: 1.01, y: -4 },
@@ -74,8 +205,8 @@ function SpotlightCard({
74
205
  background: "linear-gradient(135deg, rgba(15,23,42,0.78), rgba(15,23,42,0.42))"
75
206
  },
76
207
  children: [
77
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
78
- import_react2.motion.div,
208
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
209
+ import_react5.motion.div,
79
210
  {
80
211
  "aria-hidden": "true",
81
212
  className: "pointer-events-none absolute inset-0 opacity-90",
@@ -86,34 +217,122 @@ function SpotlightCard({
86
217
  }
87
218
  }
88
219
  ),
89
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "relative z-10", children })
220
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "relative z-10", children })
90
221
  ]
91
222
  }
92
223
  );
93
224
  }
94
225
 
95
226
  // src/components/text-reveal.tsx
96
- var import_react3 = require("motion/react");
97
- var import_jsx_runtime3 = require("react/jsx-runtime");
227
+ var import_react6 = require("motion/react");
228
+ var import_jsx_runtime6 = require("react/jsx-runtime");
98
229
  function TextReveal({
99
230
  as: Component = "h1",
100
231
  children,
101
232
  className = "",
102
233
  ...props
103
234
  }) {
104
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
105
- import_react3.motion.div,
235
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
236
+ import_react6.motion.div,
106
237
  {
107
238
  initial: { opacity: 0, y: 18 },
108
239
  animate: { opacity: 1, y: 0 },
109
240
  transition: { duration: 0.5, ease: "easeOut" },
110
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Component, { ...props, className, children })
241
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Component, { ...props, className, children })
242
+ }
243
+ );
244
+ }
245
+
246
+ // src/components/tilt-card.tsx
247
+ var import_react7 = require("react");
248
+ var import_jsx_runtime7 = require("react/jsx-runtime");
249
+ function TiltCard({
250
+ children,
251
+ maxTilt = 15,
252
+ perspective = 1e3,
253
+ scale = 1.02,
254
+ glare = true,
255
+ glareOpacity = 0.25,
256
+ glareColor = "rgba(255, 255, 255, 0.4)",
257
+ speed = 300,
258
+ className = ""
259
+ }) {
260
+ const cardRef = (0, import_react7.useRef)(null);
261
+ const [rotateX, setRotateX] = (0, import_react7.useState)(0);
262
+ const [rotateY, setRotateY] = (0, import_react7.useState)(0);
263
+ const [currentScale, setCurrentScale] = (0, import_react7.useState)(1);
264
+ const [glarePos, setGlarePos] = (0, import_react7.useState)({ x: 50, y: 50, opacity: 0 });
265
+ const handleMouseMove = (e) => {
266
+ if (!cardRef.current) return;
267
+ const rect = cardRef.current.getBoundingClientRect();
268
+ const x = e.clientX - rect.left;
269
+ const y = e.clientY - rect.top;
270
+ const centerX = rect.width / 2;
271
+ const centerY = rect.height / 2;
272
+ const rotX = (y - centerY) / centerY * -maxTilt;
273
+ const rotY = (x - centerX) / centerX * maxTilt;
274
+ setRotateX(rotX);
275
+ setRotateY(rotY);
276
+ setCurrentScale(scale);
277
+ if (glare) {
278
+ setGlarePos({
279
+ x: x / rect.width * 100,
280
+ y: y / rect.height * 100,
281
+ opacity: glareOpacity
282
+ });
283
+ }
284
+ };
285
+ const handleMouseLeave = () => {
286
+ setRotateX(0);
287
+ setRotateY(0);
288
+ setCurrentScale(1);
289
+ setGlarePos((prev) => ({ ...prev, opacity: 0 }));
290
+ };
291
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
292
+ "div",
293
+ {
294
+ style: { perspective: `${perspective}px` },
295
+ className: "inline-block w-full",
296
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
297
+ "div",
298
+ {
299
+ ref: cardRef,
300
+ onMouseMove: handleMouseMove,
301
+ onMouseLeave: handleMouseLeave,
302
+ style: {
303
+ transform: `rotateX(${rotateX}deg) rotateY(${rotateY}deg) scale3d(${currentScale}, ${currentScale}, ${currentScale})`,
304
+ transition: `transform ${speed}ms cubic-bezier(0.03, 0.98, 0.52, 0.99)`,
305
+ transformStyle: "preserve-3d"
306
+ },
307
+ className: [
308
+ "relative overflow-hidden rounded-3xl border border-white/10 bg-slate-900/80 p-6 shadow-2xl backdrop-blur-md",
309
+ className
310
+ ].join(" "),
311
+ children: [
312
+ glare && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
313
+ "div",
314
+ {
315
+ className: "pointer-events-none absolute inset-0 transition-opacity duration-300",
316
+ style: {
317
+ opacity: glarePos.opacity,
318
+ background: `radial-gradient(circle at ${glarePos.x}% ${glarePos.y}%, ${glareColor}, transparent 60%)`
319
+ }
320
+ }
321
+ ),
322
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { transform: "translateZ(30px)" }, children })
323
+ ]
324
+ }
325
+ )
111
326
  }
112
327
  );
113
328
  }
114
329
  // Annotate the CommonJS export names for ESM import in node:
115
330
  0 && (module.exports = {
116
331
  FadeInSection,
332
+ Magnetic,
333
+ Marquee,
334
+ NumberTicker,
117
335
  SpotlightCard,
118
- TextReveal
336
+ TextReveal,
337
+ TiltCard
119
338
  });
package/dist/index.d.ts CHANGED
@@ -1,3 +1,7 @@
1
1
  export * from './components/fade-in';
2
+ export * from './components/magnetic';
3
+ export * from './components/marquee';
4
+ export * from './components/number-ticker';
2
5
  export * from './components/spotlight-card';
3
6
  export * from './components/text-reveal';
7
+ export * from './components/tilt-card';
package/dist/index.js CHANGED
@@ -24,9 +24,136 @@ function FadeInSection({
24
24
  );
25
25
  }
26
26
 
27
+ // src/components/magnetic.tsx
28
+ import { motion as motion2, useMotionValue, useSpring } from "motion/react";
29
+ import { useRef } from "react";
30
+ import { jsx as jsx2 } from "react/jsx-runtime";
31
+ function Magnetic({
32
+ children,
33
+ strength = 0.35,
34
+ damping = 15,
35
+ stiffness = 150,
36
+ mass = 0.1,
37
+ disabled = false,
38
+ className = ""
39
+ }) {
40
+ const ref = useRef(null);
41
+ const x = useMotionValue(0);
42
+ const y = useMotionValue(0);
43
+ const springConfig = { damping, stiffness, mass };
44
+ const springX = useSpring(x, springConfig);
45
+ const springY = useSpring(y, springConfig);
46
+ const handleMouseMove = (e) => {
47
+ if (disabled || !ref.current) return;
48
+ const { clientX, clientY } = e;
49
+ const { left, top, width, height } = ref.current.getBoundingClientRect();
50
+ const middleX = clientX - (left + width / 2);
51
+ const middleY = clientY - (top + height / 2);
52
+ x.set(middleX * strength);
53
+ y.set(middleY * strength);
54
+ };
55
+ const handleMouseLeave = () => {
56
+ x.set(0);
57
+ y.set(0);
58
+ };
59
+ return /* @__PURE__ */ jsx2(
60
+ motion2.div,
61
+ {
62
+ ref,
63
+ onMouseMove: handleMouseMove,
64
+ onMouseLeave: handleMouseLeave,
65
+ style: { x: springX, y: springY },
66
+ className: ["inline-block", className].join(" "),
67
+ children
68
+ }
69
+ );
70
+ }
71
+
72
+ // src/components/marquee.tsx
73
+ import { jsx as jsx3, jsxs } from "react/jsx-runtime";
74
+ function Marquee({
75
+ children,
76
+ speed = 25,
77
+ direction = "left",
78
+ pauseOnHover = true,
79
+ className = ""
80
+ }) {
81
+ return /* @__PURE__ */ jsxs(
82
+ "div",
83
+ {
84
+ className: [
85
+ "group flex overflow-hidden select-none [mask-image:linear-gradient(to_right,transparent,white_10%,white_90%,transparent)]",
86
+ className
87
+ ].join(" "),
88
+ children: [
89
+ /* @__PURE__ */ jsx3(
90
+ "div",
91
+ {
92
+ className: [
93
+ "flex min-w-full shrink-0 items-center justify-around gap-8",
94
+ direction === "left" ? "animate-marquee" : "animate-marquee-reverse",
95
+ pauseOnHover ? "group-hover:[animation-play-state:paused]" : ""
96
+ ].join(" "),
97
+ style: { animationDuration: `${speed}s` },
98
+ children
99
+ }
100
+ ),
101
+ /* @__PURE__ */ jsx3(
102
+ "div",
103
+ {
104
+ "aria-hidden": "true",
105
+ className: [
106
+ "flex min-w-full shrink-0 items-center justify-around gap-8",
107
+ direction === "left" ? "animate-marquee" : "animate-marquee-reverse",
108
+ pauseOnHover ? "group-hover:[animation-play-state:paused]" : ""
109
+ ].join(" "),
110
+ style: { animationDuration: `${speed}s` },
111
+ children
112
+ }
113
+ )
114
+ ]
115
+ }
116
+ );
117
+ }
118
+
119
+ // src/components/number-ticker.tsx
120
+ import { useEffect, useRef as useRef2, useState } from "react";
121
+ import { jsxs as jsxs2 } from "react/jsx-runtime";
122
+ function NumberTicker({
123
+ value,
124
+ duration = 1500,
125
+ className = "",
126
+ prefix = "",
127
+ suffix = ""
128
+ }) {
129
+ const [current, setCurrent] = useState(0);
130
+ const startTimeRef = useRef2(null);
131
+ useEffect(() => {
132
+ let animationFrame;
133
+ const animate = (timestamp) => {
134
+ if (!startTimeRef.current) startTimeRef.current = timestamp;
135
+ const progress = Math.min((timestamp - startTimeRef.current) / duration, 1);
136
+ const easeOut = 1 - Math.pow(1 - progress, 3);
137
+ setCurrent(Math.floor(easeOut * value));
138
+ if (progress < 1) {
139
+ animationFrame = requestAnimationFrame(animate);
140
+ } else {
141
+ setCurrent(value);
142
+ }
143
+ };
144
+ animationFrame = requestAnimationFrame(animate);
145
+ return () => cancelAnimationFrame(animationFrame);
146
+ }, [value, duration]);
147
+ return /* @__PURE__ */ jsxs2("span", { className: ["tabular-nums font-mono", className].join(" "), children: [
148
+ prefix,
149
+ current.toLocaleString(),
150
+ suffix
151
+ ] });
152
+ }
153
+
27
154
  // src/components/spotlight-card.tsx
28
- import { motion as motion2 } from "motion/react";
29
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
155
+ import { motion as motion3 } from "motion/react";
156
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
30
157
  function SpotlightCard({
31
158
  children,
32
159
  glowColor = "#8b5cf6",
@@ -35,8 +162,8 @@ function SpotlightCard({
35
162
  className = "",
36
163
  ...props
37
164
  }) {
38
- return /* @__PURE__ */ jsxs(
39
- motion2.div,
165
+ return /* @__PURE__ */ jsxs3(
166
+ motion3.div,
40
167
  {
41
168
  ...props,
42
169
  whileHover: { scale: 1.01, y: -4 },
@@ -47,8 +174,8 @@ function SpotlightCard({
47
174
  background: "linear-gradient(135deg, rgba(15,23,42,0.78), rgba(15,23,42,0.42))"
48
175
  },
49
176
  children: [
50
- /* @__PURE__ */ jsx2(
51
- motion2.div,
177
+ /* @__PURE__ */ jsx4(
178
+ motion3.div,
52
179
  {
53
180
  "aria-hidden": "true",
54
181
  className: "pointer-events-none absolute inset-0 opacity-90",
@@ -59,33 +186,121 @@ function SpotlightCard({
59
186
  }
60
187
  }
61
188
  ),
62
- /* @__PURE__ */ jsx2("div", { className: "relative z-10", children })
189
+ /* @__PURE__ */ jsx4("div", { className: "relative z-10", children })
63
190
  ]
64
191
  }
65
192
  );
66
193
  }
67
194
 
68
195
  // src/components/text-reveal.tsx
69
- import { motion as motion3 } from "motion/react";
70
- import { jsx as jsx3 } from "react/jsx-runtime";
196
+ import { motion as motion4 } from "motion/react";
197
+ import { jsx as jsx5 } from "react/jsx-runtime";
71
198
  function TextReveal({
72
199
  as: Component = "h1",
73
200
  children,
74
201
  className = "",
75
202
  ...props
76
203
  }) {
77
- return /* @__PURE__ */ jsx3(
78
- motion3.div,
204
+ return /* @__PURE__ */ jsx5(
205
+ motion4.div,
79
206
  {
80
207
  initial: { opacity: 0, y: 18 },
81
208
  animate: { opacity: 1, y: 0 },
82
209
  transition: { duration: 0.5, ease: "easeOut" },
83
- children: /* @__PURE__ */ jsx3(Component, { ...props, className, children })
210
+ children: /* @__PURE__ */ jsx5(Component, { ...props, className, children })
211
+ }
212
+ );
213
+ }
214
+
215
+ // src/components/tilt-card.tsx
216
+ import { useRef as useRef3, useState as useState2 } from "react";
217
+ import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
218
+ function TiltCard({
219
+ children,
220
+ maxTilt = 15,
221
+ perspective = 1e3,
222
+ scale = 1.02,
223
+ glare = true,
224
+ glareOpacity = 0.25,
225
+ glareColor = "rgba(255, 255, 255, 0.4)",
226
+ speed = 300,
227
+ className = ""
228
+ }) {
229
+ const cardRef = useRef3(null);
230
+ const [rotateX, setRotateX] = useState2(0);
231
+ const [rotateY, setRotateY] = useState2(0);
232
+ const [currentScale, setCurrentScale] = useState2(1);
233
+ const [glarePos, setGlarePos] = useState2({ x: 50, y: 50, opacity: 0 });
234
+ const handleMouseMove = (e) => {
235
+ if (!cardRef.current) return;
236
+ const rect = cardRef.current.getBoundingClientRect();
237
+ const x = e.clientX - rect.left;
238
+ const y = e.clientY - rect.top;
239
+ const centerX = rect.width / 2;
240
+ const centerY = rect.height / 2;
241
+ const rotX = (y - centerY) / centerY * -maxTilt;
242
+ const rotY = (x - centerX) / centerX * maxTilt;
243
+ setRotateX(rotX);
244
+ setRotateY(rotY);
245
+ setCurrentScale(scale);
246
+ if (glare) {
247
+ setGlarePos({
248
+ x: x / rect.width * 100,
249
+ y: y / rect.height * 100,
250
+ opacity: glareOpacity
251
+ });
252
+ }
253
+ };
254
+ const handleMouseLeave = () => {
255
+ setRotateX(0);
256
+ setRotateY(0);
257
+ setCurrentScale(1);
258
+ setGlarePos((prev) => ({ ...prev, opacity: 0 }));
259
+ };
260
+ return /* @__PURE__ */ jsx6(
261
+ "div",
262
+ {
263
+ style: { perspective: `${perspective}px` },
264
+ className: "inline-block w-full",
265
+ children: /* @__PURE__ */ jsxs4(
266
+ "div",
267
+ {
268
+ ref: cardRef,
269
+ onMouseMove: handleMouseMove,
270
+ onMouseLeave: handleMouseLeave,
271
+ style: {
272
+ transform: `rotateX(${rotateX}deg) rotateY(${rotateY}deg) scale3d(${currentScale}, ${currentScale}, ${currentScale})`,
273
+ transition: `transform ${speed}ms cubic-bezier(0.03, 0.98, 0.52, 0.99)`,
274
+ transformStyle: "preserve-3d"
275
+ },
276
+ className: [
277
+ "relative overflow-hidden rounded-3xl border border-white/10 bg-slate-900/80 p-6 shadow-2xl backdrop-blur-md",
278
+ className
279
+ ].join(" "),
280
+ children: [
281
+ glare && /* @__PURE__ */ jsx6(
282
+ "div",
283
+ {
284
+ className: "pointer-events-none absolute inset-0 transition-opacity duration-300",
285
+ style: {
286
+ opacity: glarePos.opacity,
287
+ background: `radial-gradient(circle at ${glarePos.x}% ${glarePos.y}%, ${glareColor}, transparent 60%)`
288
+ }
289
+ }
290
+ ),
291
+ /* @__PURE__ */ jsx6("div", { style: { transform: "translateZ(30px)" }, children })
292
+ ]
293
+ }
294
+ )
84
295
  }
85
296
  );
86
297
  }
87
298
  export {
88
299
  FadeInSection,
300
+ Magnetic,
301
+ Marquee,
302
+ NumberTicker,
89
303
  SpotlightCard,
90
- TextReveal
304
+ TextReveal,
305
+ TiltCard
91
306
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackvibe-studio/motion",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "description": "Motion primitives for StackVibe UI.",
6
6
  "repository": {
@@ -12,6 +12,24 @@
12
12
  "url": "https://github.com/StackVibe-Studio/stackvibe-ui/issues"
13
13
  },
14
14
  "license": "MIT",
15
+ "keywords": [
16
+ "react",
17
+ "react-19",
18
+ "motion",
19
+ "framer-motion",
20
+ "spring-physics",
21
+ "magnetic",
22
+ "tilt",
23
+ "3d-tilt",
24
+ "ticker",
25
+ "number-ticker",
26
+ "marquee",
27
+ "spotlight",
28
+ "text-reveal",
29
+ "animation",
30
+ "micro-interactions",
31
+ "stackvibe"
32
+ ],
15
33
  "main": "./dist/index.js",
16
34
  "types": "./dist/index.d.ts",
17
35
  "exports": {