@stackvibe-studio/motion 0.2.0 → 0.3.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,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,7 @@
1
+ import { type ReactNode } from 'react';
2
+ export interface MagneticProps {
3
+ children: ReactNode;
4
+ strength?: number;
5
+ className?: string;
6
+ }
7
+ export declare function Magnetic({ children, strength, 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,8 @@
1
+ import { type ReactNode } from 'react';
2
+ export interface TiltCardProps {
3
+ children: ReactNode;
4
+ maxTilt?: number;
5
+ glare?: boolean;
6
+ className?: string;
7
+ }
8
+ export declare function TiltCard({ children, maxTilt, glare, 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,128 @@ 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({ children, strength = 0.35, className = "" }) {
63
+ const ref = (0, import_react3.useRef)(null);
64
+ const x = (0, import_react2.useMotionValue)(0);
65
+ const y = (0, import_react2.useMotionValue)(0);
66
+ const springConfig = { damping: 15, stiffness: 150, mass: 0.1 };
67
+ const springX = (0, import_react2.useSpring)(x, springConfig);
68
+ const springY = (0, import_react2.useSpring)(y, springConfig);
69
+ const handleMouseMove = (e) => {
70
+ if (!ref.current) return;
71
+ const { clientX, clientY } = e;
72
+ const { left, top, width, height } = ref.current.getBoundingClientRect();
73
+ const middleX = clientX - (left + width / 2);
74
+ const middleY = clientY - (top + height / 2);
75
+ x.set(middleX * strength);
76
+ y.set(middleY * strength);
77
+ };
78
+ const handleMouseLeave = () => {
79
+ x.set(0);
80
+ y.set(0);
81
+ };
82
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
83
+ import_react2.motion.div,
84
+ {
85
+ ref,
86
+ onMouseMove: handleMouseMove,
87
+ onMouseLeave: handleMouseLeave,
88
+ style: { x: springX, y: springY },
89
+ className: ["inline-block cursor-pointer", className].join(" "),
90
+ children
91
+ }
92
+ );
93
+ }
94
+
95
+ // src/components/marquee.tsx
96
+ var import_jsx_runtime3 = require("react/jsx-runtime");
97
+ function Marquee({
98
+ children,
99
+ speed = 25,
100
+ direction = "left",
101
+ pauseOnHover = true,
102
+ className = ""
103
+ }) {
104
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
105
+ "div",
106
+ {
107
+ className: [
108
+ "group flex overflow-hidden select-none [mask-image:linear-gradient(to_right,transparent,white_10%,white_90%,transparent)]",
109
+ className
110
+ ].join(" "),
111
+ children: [
112
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
113
+ "div",
114
+ {
115
+ className: [
116
+ "flex min-w-full shrink-0 items-center justify-around gap-8",
117
+ direction === "left" ? "animate-marquee" : "animate-marquee-reverse",
118
+ pauseOnHover ? "group-hover:[animation-play-state:paused]" : ""
119
+ ].join(" "),
120
+ style: { animationDuration: `${speed}s` },
121
+ children
122
+ }
123
+ ),
124
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
125
+ "div",
126
+ {
127
+ "aria-hidden": "true",
128
+ className: [
129
+ "flex min-w-full shrink-0 items-center justify-around gap-8",
130
+ direction === "left" ? "animate-marquee" : "animate-marquee-reverse",
131
+ pauseOnHover ? "group-hover:[animation-play-state:paused]" : ""
132
+ ].join(" "),
133
+ style: { animationDuration: `${speed}s` },
134
+ children
135
+ }
136
+ )
137
+ ]
138
+ }
139
+ );
140
+ }
141
+
142
+ // src/components/number-ticker.tsx
143
+ var import_react4 = require("react");
144
+ var import_jsx_runtime4 = require("react/jsx-runtime");
145
+ function NumberTicker({
146
+ value,
147
+ duration = 1500,
148
+ className = "",
149
+ prefix = "",
150
+ suffix = ""
151
+ }) {
152
+ const [current, setCurrent] = (0, import_react4.useState)(0);
153
+ const startTimeRef = (0, import_react4.useRef)(null);
154
+ (0, import_react4.useEffect)(() => {
155
+ let animationFrame;
156
+ const animate = (timestamp) => {
157
+ if (!startTimeRef.current) startTimeRef.current = timestamp;
158
+ const progress = Math.min((timestamp - startTimeRef.current) / duration, 1);
159
+ const easeOut = 1 - Math.pow(1 - progress, 3);
160
+ setCurrent(Math.floor(easeOut * value));
161
+ if (progress < 1) {
162
+ animationFrame = requestAnimationFrame(animate);
163
+ } else {
164
+ setCurrent(value);
165
+ }
166
+ };
167
+ animationFrame = requestAnimationFrame(animate);
168
+ return () => cancelAnimationFrame(animationFrame);
169
+ }, [value, duration]);
170
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: ["tabular-nums font-mono", className].join(" "), children: [
171
+ prefix,
172
+ current.toLocaleString(),
173
+ suffix
174
+ ] });
175
+ }
176
+
177
+ // src/components/spotlight-card.tsx
178
+ var import_react5 = require("motion/react");
179
+ var import_jsx_runtime5 = require("react/jsx-runtime");
57
180
  function SpotlightCard({
58
181
  children,
59
182
  glowColor = "#8b5cf6",
@@ -62,8 +185,8 @@ function SpotlightCard({
62
185
  className = "",
63
186
  ...props
64
187
  }) {
65
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
66
- import_react2.motion.div,
188
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
189
+ import_react5.motion.div,
67
190
  {
68
191
  ...props,
69
192
  whileHover: { scale: 1.01, y: -4 },
@@ -74,8 +197,8 @@ function SpotlightCard({
74
197
  background: "linear-gradient(135deg, rgba(15,23,42,0.78), rgba(15,23,42,0.42))"
75
198
  },
76
199
  children: [
77
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
78
- import_react2.motion.div,
200
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
201
+ import_react5.motion.div,
79
202
  {
80
203
  "aria-hidden": "true",
81
204
  className: "pointer-events-none absolute inset-0 opacity-90",
@@ -86,34 +209,114 @@ function SpotlightCard({
86
209
  }
87
210
  }
88
211
  ),
89
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "relative z-10", children })
212
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "relative z-10", children })
90
213
  ]
91
214
  }
92
215
  );
93
216
  }
94
217
 
95
218
  // src/components/text-reveal.tsx
96
- var import_react3 = require("motion/react");
97
- var import_jsx_runtime3 = require("react/jsx-runtime");
219
+ var import_react6 = require("motion/react");
220
+ var import_jsx_runtime6 = require("react/jsx-runtime");
98
221
  function TextReveal({
99
222
  as: Component = "h1",
100
223
  children,
101
224
  className = "",
102
225
  ...props
103
226
  }) {
104
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
105
- import_react3.motion.div,
227
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
228
+ import_react6.motion.div,
106
229
  {
107
230
  initial: { opacity: 0, y: 18 },
108
231
  animate: { opacity: 1, y: 0 },
109
232
  transition: { duration: 0.5, ease: "easeOut" },
110
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Component, { ...props, className, children })
233
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Component, { ...props, className, children })
234
+ }
235
+ );
236
+ }
237
+
238
+ // src/components/tilt-card.tsx
239
+ var import_react7 = require("react");
240
+ var import_jsx_runtime7 = require("react/jsx-runtime");
241
+ function TiltCard({
242
+ children,
243
+ maxTilt = 15,
244
+ glare = true,
245
+ className = ""
246
+ }) {
247
+ const cardRef = (0, import_react7.useRef)(null);
248
+ const [rotateX, setRotateX] = (0, import_react7.useState)(0);
249
+ const [rotateY, setRotateY] = (0, import_react7.useState)(0);
250
+ const [glarePos, setGlarePos] = (0, import_react7.useState)({ x: 50, y: 50, opacity: 0 });
251
+ const handleMouseMove = (e) => {
252
+ if (!cardRef.current) return;
253
+ const rect = cardRef.current.getBoundingClientRect();
254
+ const x = e.clientX - rect.left;
255
+ const y = e.clientY - rect.top;
256
+ const centerX = rect.width / 2;
257
+ const centerY = rect.height / 2;
258
+ const rotX = (y - centerY) / centerY * -maxTilt;
259
+ const rotY = (x - centerX) / centerX * maxTilt;
260
+ setRotateX(rotX);
261
+ setRotateY(rotY);
262
+ if (glare) {
263
+ setGlarePos({
264
+ x: x / rect.width * 100,
265
+ y: y / rect.height * 100,
266
+ opacity: 0.25
267
+ });
268
+ }
269
+ };
270
+ const handleMouseLeave = () => {
271
+ setRotateX(0);
272
+ setRotateY(0);
273
+ setGlarePos((prev) => ({ ...prev, opacity: 0 }));
274
+ };
275
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
276
+ "div",
277
+ {
278
+ style: { perspective: "1000px" },
279
+ className: "inline-block w-full",
280
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
281
+ "div",
282
+ {
283
+ ref: cardRef,
284
+ onMouseMove: handleMouseMove,
285
+ onMouseLeave: handleMouseLeave,
286
+ style: {
287
+ transform: `rotateX(${rotateX}deg) rotateY(${rotateY}deg)`,
288
+ transition: "transform 0.15s ease-out",
289
+ transformStyle: "preserve-3d"
290
+ },
291
+ className: [
292
+ "relative overflow-hidden rounded-3xl border border-white/10 bg-slate-900/80 p-6 backdrop-blur-xl",
293
+ className
294
+ ].join(" "),
295
+ children: [
296
+ glare && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
297
+ "div",
298
+ {
299
+ className: "pointer-events-none absolute -inset-px transition-opacity duration-300",
300
+ style: {
301
+ opacity: glarePos.opacity,
302
+ background: `radial-gradient(circle at ${glarePos.x}% ${glarePos.y}%, rgba(255,255,255,0.4), transparent 60%)`
303
+ }
304
+ }
305
+ ),
306
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { transform: "translateZ(20px)" }, children })
307
+ ]
308
+ }
309
+ )
111
310
  }
112
311
  );
113
312
  }
114
313
  // Annotate the CommonJS export names for ESM import in node:
115
314
  0 && (module.exports = {
116
315
  FadeInSection,
316
+ Magnetic,
317
+ Marquee,
318
+ NumberTicker,
117
319
  SpotlightCard,
118
- TextReveal
320
+ TextReveal,
321
+ TiltCard
119
322
  });
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,128 @@ 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({ children, strength = 0.35, className = "" }) {
32
+ const ref = useRef(null);
33
+ const x = useMotionValue(0);
34
+ const y = useMotionValue(0);
35
+ const springConfig = { damping: 15, stiffness: 150, mass: 0.1 };
36
+ const springX = useSpring(x, springConfig);
37
+ const springY = useSpring(y, springConfig);
38
+ const handleMouseMove = (e) => {
39
+ if (!ref.current) return;
40
+ const { clientX, clientY } = e;
41
+ const { left, top, width, height } = ref.current.getBoundingClientRect();
42
+ const middleX = clientX - (left + width / 2);
43
+ const middleY = clientY - (top + height / 2);
44
+ x.set(middleX * strength);
45
+ y.set(middleY * strength);
46
+ };
47
+ const handleMouseLeave = () => {
48
+ x.set(0);
49
+ y.set(0);
50
+ };
51
+ return /* @__PURE__ */ jsx2(
52
+ motion2.div,
53
+ {
54
+ ref,
55
+ onMouseMove: handleMouseMove,
56
+ onMouseLeave: handleMouseLeave,
57
+ style: { x: springX, y: springY },
58
+ className: ["inline-block cursor-pointer", className].join(" "),
59
+ children
60
+ }
61
+ );
62
+ }
63
+
64
+ // src/components/marquee.tsx
65
+ import { jsx as jsx3, jsxs } from "react/jsx-runtime";
66
+ function Marquee({
67
+ children,
68
+ speed = 25,
69
+ direction = "left",
70
+ pauseOnHover = true,
71
+ className = ""
72
+ }) {
73
+ return /* @__PURE__ */ jsxs(
74
+ "div",
75
+ {
76
+ className: [
77
+ "group flex overflow-hidden select-none [mask-image:linear-gradient(to_right,transparent,white_10%,white_90%,transparent)]",
78
+ className
79
+ ].join(" "),
80
+ children: [
81
+ /* @__PURE__ */ jsx3(
82
+ "div",
83
+ {
84
+ className: [
85
+ "flex min-w-full shrink-0 items-center justify-around gap-8",
86
+ direction === "left" ? "animate-marquee" : "animate-marquee-reverse",
87
+ pauseOnHover ? "group-hover:[animation-play-state:paused]" : ""
88
+ ].join(" "),
89
+ style: { animationDuration: `${speed}s` },
90
+ children
91
+ }
92
+ ),
93
+ /* @__PURE__ */ jsx3(
94
+ "div",
95
+ {
96
+ "aria-hidden": "true",
97
+ className: [
98
+ "flex min-w-full shrink-0 items-center justify-around gap-8",
99
+ direction === "left" ? "animate-marquee" : "animate-marquee-reverse",
100
+ pauseOnHover ? "group-hover:[animation-play-state:paused]" : ""
101
+ ].join(" "),
102
+ style: { animationDuration: `${speed}s` },
103
+ children
104
+ }
105
+ )
106
+ ]
107
+ }
108
+ );
109
+ }
110
+
111
+ // src/components/number-ticker.tsx
112
+ import { useEffect, useRef as useRef2, useState } from "react";
113
+ import { jsxs as jsxs2 } from "react/jsx-runtime";
114
+ function NumberTicker({
115
+ value,
116
+ duration = 1500,
117
+ className = "",
118
+ prefix = "",
119
+ suffix = ""
120
+ }) {
121
+ const [current, setCurrent] = useState(0);
122
+ const startTimeRef = useRef2(null);
123
+ useEffect(() => {
124
+ let animationFrame;
125
+ const animate = (timestamp) => {
126
+ if (!startTimeRef.current) startTimeRef.current = timestamp;
127
+ const progress = Math.min((timestamp - startTimeRef.current) / duration, 1);
128
+ const easeOut = 1 - Math.pow(1 - progress, 3);
129
+ setCurrent(Math.floor(easeOut * value));
130
+ if (progress < 1) {
131
+ animationFrame = requestAnimationFrame(animate);
132
+ } else {
133
+ setCurrent(value);
134
+ }
135
+ };
136
+ animationFrame = requestAnimationFrame(animate);
137
+ return () => cancelAnimationFrame(animationFrame);
138
+ }, [value, duration]);
139
+ return /* @__PURE__ */ jsxs2("span", { className: ["tabular-nums font-mono", className].join(" "), children: [
140
+ prefix,
141
+ current.toLocaleString(),
142
+ suffix
143
+ ] });
144
+ }
145
+
27
146
  // src/components/spotlight-card.tsx
28
- import { motion as motion2 } from "motion/react";
29
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
147
+ import { motion as motion3 } from "motion/react";
148
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
30
149
  function SpotlightCard({
31
150
  children,
32
151
  glowColor = "#8b5cf6",
@@ -35,8 +154,8 @@ function SpotlightCard({
35
154
  className = "",
36
155
  ...props
37
156
  }) {
38
- return /* @__PURE__ */ jsxs(
39
- motion2.div,
157
+ return /* @__PURE__ */ jsxs3(
158
+ motion3.div,
40
159
  {
41
160
  ...props,
42
161
  whileHover: { scale: 1.01, y: -4 },
@@ -47,8 +166,8 @@ function SpotlightCard({
47
166
  background: "linear-gradient(135deg, rgba(15,23,42,0.78), rgba(15,23,42,0.42))"
48
167
  },
49
168
  children: [
50
- /* @__PURE__ */ jsx2(
51
- motion2.div,
169
+ /* @__PURE__ */ jsx4(
170
+ motion3.div,
52
171
  {
53
172
  "aria-hidden": "true",
54
173
  className: "pointer-events-none absolute inset-0 opacity-90",
@@ -59,33 +178,113 @@ function SpotlightCard({
59
178
  }
60
179
  }
61
180
  ),
62
- /* @__PURE__ */ jsx2("div", { className: "relative z-10", children })
181
+ /* @__PURE__ */ jsx4("div", { className: "relative z-10", children })
63
182
  ]
64
183
  }
65
184
  );
66
185
  }
67
186
 
68
187
  // src/components/text-reveal.tsx
69
- import { motion as motion3 } from "motion/react";
70
- import { jsx as jsx3 } from "react/jsx-runtime";
188
+ import { motion as motion4 } from "motion/react";
189
+ import { jsx as jsx5 } from "react/jsx-runtime";
71
190
  function TextReveal({
72
191
  as: Component = "h1",
73
192
  children,
74
193
  className = "",
75
194
  ...props
76
195
  }) {
77
- return /* @__PURE__ */ jsx3(
78
- motion3.div,
196
+ return /* @__PURE__ */ jsx5(
197
+ motion4.div,
79
198
  {
80
199
  initial: { opacity: 0, y: 18 },
81
200
  animate: { opacity: 1, y: 0 },
82
201
  transition: { duration: 0.5, ease: "easeOut" },
83
- children: /* @__PURE__ */ jsx3(Component, { ...props, className, children })
202
+ children: /* @__PURE__ */ jsx5(Component, { ...props, className, children })
203
+ }
204
+ );
205
+ }
206
+
207
+ // src/components/tilt-card.tsx
208
+ import { useRef as useRef3, useState as useState2 } from "react";
209
+ import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
210
+ function TiltCard({
211
+ children,
212
+ maxTilt = 15,
213
+ glare = true,
214
+ className = ""
215
+ }) {
216
+ const cardRef = useRef3(null);
217
+ const [rotateX, setRotateX] = useState2(0);
218
+ const [rotateY, setRotateY] = useState2(0);
219
+ const [glarePos, setGlarePos] = useState2({ x: 50, y: 50, opacity: 0 });
220
+ const handleMouseMove = (e) => {
221
+ if (!cardRef.current) return;
222
+ const rect = cardRef.current.getBoundingClientRect();
223
+ const x = e.clientX - rect.left;
224
+ const y = e.clientY - rect.top;
225
+ const centerX = rect.width / 2;
226
+ const centerY = rect.height / 2;
227
+ const rotX = (y - centerY) / centerY * -maxTilt;
228
+ const rotY = (x - centerX) / centerX * maxTilt;
229
+ setRotateX(rotX);
230
+ setRotateY(rotY);
231
+ if (glare) {
232
+ setGlarePos({
233
+ x: x / rect.width * 100,
234
+ y: y / rect.height * 100,
235
+ opacity: 0.25
236
+ });
237
+ }
238
+ };
239
+ const handleMouseLeave = () => {
240
+ setRotateX(0);
241
+ setRotateY(0);
242
+ setGlarePos((prev) => ({ ...prev, opacity: 0 }));
243
+ };
244
+ return /* @__PURE__ */ jsx6(
245
+ "div",
246
+ {
247
+ style: { perspective: "1000px" },
248
+ className: "inline-block w-full",
249
+ children: /* @__PURE__ */ jsxs4(
250
+ "div",
251
+ {
252
+ ref: cardRef,
253
+ onMouseMove: handleMouseMove,
254
+ onMouseLeave: handleMouseLeave,
255
+ style: {
256
+ transform: `rotateX(${rotateX}deg) rotateY(${rotateY}deg)`,
257
+ transition: "transform 0.15s ease-out",
258
+ transformStyle: "preserve-3d"
259
+ },
260
+ className: [
261
+ "relative overflow-hidden rounded-3xl border border-white/10 bg-slate-900/80 p-6 backdrop-blur-xl",
262
+ className
263
+ ].join(" "),
264
+ children: [
265
+ glare && /* @__PURE__ */ jsx6(
266
+ "div",
267
+ {
268
+ className: "pointer-events-none absolute -inset-px transition-opacity duration-300",
269
+ style: {
270
+ opacity: glarePos.opacity,
271
+ background: `radial-gradient(circle at ${glarePos.x}% ${glarePos.y}%, rgba(255,255,255,0.4), transparent 60%)`
272
+ }
273
+ }
274
+ ),
275
+ /* @__PURE__ */ jsx6("div", { style: { transform: "translateZ(20px)" }, children })
276
+ ]
277
+ }
278
+ )
84
279
  }
85
280
  );
86
281
  }
87
282
  export {
88
283
  FadeInSection,
284
+ Magnetic,
285
+ Marquee,
286
+ NumberTicker,
89
287
  SpotlightCard,
90
- TextReveal
288
+ TextReveal,
289
+ TiltCard
91
290
  };
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.0",
4
4
  "type": "module",
5
5
  "description": "Motion primitives for StackVibe UI.",
6
6
  "repository": {