@smicolon/fasla-ui 0.3.0 → 0.4.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.
Files changed (35) hide show
  1. package/README.md +52 -20
  2. package/dist/index.cjs +319 -0
  3. package/dist/index.d.cts +5 -0
  4. package/dist/index.js +1 -1
  5. package/dist/lib/utils.cjs +11 -0
  6. package/dist/lib/utils.d.cts +9 -0
  7. package/dist/motion/index.cjs +61 -0
  8. package/dist/motion/index.d.cts +33 -0
  9. package/dist/registry/index.cjs +179 -0
  10. package/dist/registry/index.d.cts +17 -0
  11. package/dist/registry/index.js +1 -1
  12. package/dist/tokens/index.cjs +77 -0
  13. package/dist/tokens/index.d.cts +124 -0
  14. package/package.json +14 -7
  15. package/registry/blocks/empty-state/EmptyState.test.tsx +11 -0
  16. package/registry/blocks/empty-state/EmptyState.tsx +10 -2
  17. package/registry/blocks/sidebar/Sidebar.test.tsx +39 -0
  18. package/registry/blocks/sidebar/Sidebar.tsx +28 -11
  19. package/registry/blocks/stats-card/StatsCard.tsx +3 -2
  20. package/registry/effects/animated-gradient/animated-gradient.tsx +3 -7
  21. package/registry/effects/effects.test.tsx +61 -0
  22. package/registry/effects/glow-card/glow-card.tsx +18 -12
  23. package/registry/effects/text-reveal/text-reveal.tsx +3 -1
  24. package/registry/effects/typewriter-text/typewriter-text.tsx +49 -44
  25. package/registry/next14-tsconfig.test.ts +43 -0
  26. package/registry/ui/checkbox/checkbox.test.tsx +23 -0
  27. package/registry/ui/checkbox/checkbox.tsx +3 -1
  28. package/registry/ui/combobox/combobox.test.tsx +30 -1
  29. package/registry/ui/combobox/combobox.tsx +7 -4
  30. package/registry/ui/input/input.test.tsx +1 -1
  31. package/registry/ui/input/input.tsx +1 -1
  32. package/registry/ui/skeleton/index.ts +1 -0
  33. package/registry/ui/skeleton/skeleton.test.tsx +113 -29
  34. package/registry/ui/skeleton/skeleton.tsx +101 -41
  35. package/registry.json +1 -1
package/README.md CHANGED
@@ -19,12 +19,25 @@ A professional React UI component library built on **shadcn/ui** primitives. Pro
19
19
 
20
20
  ## Installation
21
21
 
22
+ Components are copied into your project with the CLI, as with shadcn/ui:
23
+
24
+ ```bash
25
+ npx @smicolon/cli init # once per project: writes components.json and lib/utils.ts,
26
+ # and installs clsx and tailwind-merge, which lib/utils.ts imports
27
+ npx @smicolon/cli add button card shimmer-button
28
+ ```
29
+
30
+ `add` ends by printing the install command for the packages the components
31
+ need, in your project's package manager. For `button card shimmer-button` with npm it is:
32
+
22
33
  ```bash
23
- # Using the CLI (recommended)
24
- npx @smicolon/cli init
25
- npx @smicolon/cli add button
34
+ npm install @radix-ui/react-slot class-variance-authority lucide-react framer-motion
35
+ ```
26
36
 
27
- # Or install the package directly
37
+ This package holds the design tokens, the Tailwind preset and the motion
38
+ presets, not the components. Install it when you want those:
39
+
40
+ ```bash
28
41
  npm install @smicolon/fasla-ui
29
42
  # or
30
43
  yarn add @smicolon/fasla-ui
@@ -83,9 +96,9 @@ Animated components for visual flair (powered by Framer Motion):
83
96
  ## Quick Start
84
97
 
85
98
  ```tsx
86
- import { Button } from "@smicolon/fasla-ui/registry/ui/button"
87
- import { Card, CardHeader, CardTitle, CardContent } from "@smicolon/fasla-ui/registry/ui/card"
88
- import { ShimmerButton } from "@smicolon/fasla-ui/registry/effects/shimmer-button"
99
+ import { Button } from "@/components/ui/button"
100
+ import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"
101
+ import { ShimmerButton } from "@/components/ui/shimmer-button"
89
102
 
90
103
  export function MyComponent() {
91
104
  return (
@@ -106,26 +119,45 @@ export function MyComponent() {
106
119
 
107
120
  ```json
108
121
  {
109
- "react": "^18.0.0",
110
- "react-dom": "^18.0.0",
111
- "tailwindcss": "^3.4.0",
112
- "framer-motion": "^11.0.0" // optional, required for effects
122
+ "react": "^18.0.0 || ^19.0.0",
123
+ "react-dom": "^18.0.0 || ^19.0.0",
124
+ "tailwindcss": "^3.4.0 || ^4.1.4",
125
+ "framer-motion": "^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0" // optional, required for effects
113
126
  }
114
127
  ```
115
128
 
116
- ## Tailwind Configuration
129
+ ## Theme
117
130
 
118
- Add fasla-ui to your `tailwind.config.js`:
131
+ Components take every colour from your theme. `init` asks which of two layers
132
+ you want and installs it; both include the base layer. The shadcn commands below
133
+ install one too, or switch a project to the other one later.
119
134
 
120
- ```js
121
- module.exports = {
122
- content: [
123
- // ... your content paths
124
- "./node_modules/@smicolon/fasla-ui/**/*.{js,ts,jsx,tsx}",
125
- ],
126
- }
135
+ They need a project that is already set up, with a `components.json`: in a new
136
+ project, run `npx @smicolon/cli init` first, or `npx shadcn@latest init`. Without
137
+ one, the shadcn CLI sets the project up itself, and its preset's colours replace
138
+ Fasla's. On Next.js 14, use `npx @smicolon/cli init`: `shadcn init` writes
139
+ Tailwind 4 styles and loads Geist from `next/font`, which Next.js 14 has neither of.
140
+
141
+ ```bash
142
+ # Starting from scratch: use Fasla's colours (palette, radius, Geist).
143
+ npx shadcn@latest add https://ui.smicolon.com/r/theme.json https://ui.smicolon.com/r/font-geist.json
144
+
145
+ # The same on Next.js 14, without Geist: its template already loads it.
146
+ npx shadcn@latest add https://ui.smicolon.com/r/theme.json
147
+
148
+ # I have a brand: keep my colours (base layer only)
149
+ npx shadcn@latest add https://ui.smicolon.com/r/theme-base.json
127
150
  ```
128
151
 
152
+ The base layer adds only what shadcn doesn't have: the success, warning and
153
+ info tokens and the soft tints, the type scale with Arabic line heights, the
154
+ Arabic setting, and the Cairo font. It never changes a colour you already
155
+ have. Arabic text uses `--font-arabic`; point it at another font to swap Cairo.
156
+
157
+ Fasla's base components replace shadcn's components of the same name in
158
+ `components/ui`; `add` asks before replacing a file that isn't Fasla's. Blocks
159
+ install to `components/blocks`.
160
+
129
161
  ## Documentation
130
162
 
131
163
  Visit [ui.smicolon.com](https://ui.smicolon.com) for full documentation, examples, and API reference.
package/dist/index.cjs ADDED
@@ -0,0 +1,319 @@
1
+ 'use strict';
2
+
3
+ var clsx = require('clsx');
4
+ var tailwindMerge = require('tailwind-merge');
5
+ var framerMotion = require('framer-motion');
6
+
7
+ // src/lib/utils.ts
8
+ function cn(...inputs) {
9
+ return tailwindMerge.twMerge(clsx.clsx(inputs));
10
+ }
11
+
12
+ // src/tokens/model.ts
13
+ var baseTokens = {
14
+ radius: { sm: "0.5rem", md: "0.75rem", lg: "1rem" },
15
+ spacing: { densityMultiplier: { compact: 0.9, comfortable: 1, spacious: 1.1 } },
16
+ // Brand V2.5 §14 — Geist is the LTR face. Arabic is set in Cairo, which the
17
+ // Tailwind preset selects per direction; this scaffold carries only the base.
18
+ // 16/24 is the `Base` rung, identical in both Figma ramps.
19
+ typography: { fontFamily: "Geist", baseFontSizePx: 16, lineHeight: 1.5 },
20
+ motion: { durationsMs: { fast: 120, base: 180, slow: 260 } }
21
+ };
22
+
23
+ // src/tokens/tailwind.ts
24
+ var semanticColor = (name) => `color-mix(in oklch, var(--${name}) calc(<alpha-value> * 100%), transparent)`;
25
+ var c = semanticColor;
26
+ var tailwindSemanticColors = {
27
+ // Surfaces
28
+ background: c("background"),
29
+ foreground: c("foreground"),
30
+ card: { DEFAULT: c("card"), foreground: c("card-foreground") },
31
+ popover: { DEFAULT: c("popover"), foreground: c("popover-foreground") },
32
+ // Actions. `primary` is Figma's strong neutral (#0a0a0a light, #fafafa dark),
33
+ // identical to `foreground` in both modes. It is NOT a brand accent — an app
34
+ // that wants its brand colour should define its own token for it.
35
+ primary: { DEFAULT: c("primary"), foreground: c("primary-foreground") },
36
+ secondary: { DEFAULT: c("secondary"), foreground: c("secondary-foreground") },
37
+ muted: { DEFAULT: c("muted"), foreground: c("muted-foreground") },
38
+ accent: { DEFAULT: c("accent"), foreground: c("accent-foreground") },
39
+ destructive: {
40
+ DEFAULT: c("destructive"),
41
+ foreground: c("destructive-foreground")
42
+ },
43
+ // Lines and focus
44
+ border: c("border"),
45
+ input: c("input"),
46
+ ring: c("ring"),
47
+ // Status
48
+ success: { DEFAULT: c("success"), foreground: c("success-foreground") },
49
+ warning: { DEFAULT: c("warning"), foreground: c("warning-foreground") },
50
+ info: { DEFAULT: c("info"), foreground: c("info-foreground") },
51
+ // Soft tints — Figma's `opacity/*` variables, already translucent. The Badge
52
+ // Soft type lays one over `card`, exactly as Figma stacks the two fills, so
53
+ // the chip reads the same on any surface. Only `primary` changes in Dark.
54
+ soft: {
55
+ primary: c("soft-primary"),
56
+ secondary: c("soft-secondary"),
57
+ info: c("soft-info"),
58
+ success: c("soft-success"),
59
+ warning: c("soft-warning"),
60
+ destructive: c("soft-destructive")
61
+ },
62
+ // Inverse — a fixed contrast band, not a mode-reactive surface.
63
+ "background-inverse": c("background-inverse"),
64
+ "foreground-inverse": c("foreground-inverse"),
65
+ "muted-foreground-inverse": c("muted-foreground-inverse"),
66
+ // Charts
67
+ "chart-1": c("chart-1"),
68
+ "chart-2": c("chart-2"),
69
+ "chart-3": c("chart-3"),
70
+ "chart-4": c("chart-4"),
71
+ "chart-5": c("chart-5"),
72
+ sidebar: {
73
+ DEFAULT: c("sidebar"),
74
+ foreground: c("sidebar-foreground"),
75
+ primary: c("sidebar-primary"),
76
+ "primary-foreground": c("sidebar-primary-foreground"),
77
+ accent: c("sidebar-accent"),
78
+ "accent-foreground": c("sidebar-accent-foreground"),
79
+ border: c("sidebar-border"),
80
+ ring: c("sidebar-ring")
81
+ }
82
+ };
83
+ function useMotionPresets() {
84
+ const prefersReduced = framerMotion.useReducedMotion();
85
+ const durations = {
86
+ fast: 0.12,
87
+ base: 0.18,
88
+ slow: 0.26
89
+ };
90
+ const easing = {
91
+ standard: [0.2, 0, 0, 1],
92
+ emphasized: [0.2, 0.8, 0.2, 1]
93
+ };
94
+ const safe = (preset) => {
95
+ if (!prefersReduced) return preset;
96
+ return {
97
+ initial: { opacity: 0 },
98
+ animate: { opacity: 1 },
99
+ exit: { opacity: 0 },
100
+ transition: { duration: durations.fast }
101
+ };
102
+ };
103
+ return {
104
+ fadeIn: safe({
105
+ initial: { opacity: 0 },
106
+ animate: { opacity: 1 },
107
+ exit: { opacity: 0 },
108
+ transition: { duration: durations.base, ease: easing.standard }
109
+ }),
110
+ slideUp: safe({
111
+ initial: { opacity: 0, y: 10 },
112
+ animate: { opacity: 1, y: 0 },
113
+ exit: { opacity: 0, y: 6 },
114
+ transition: { duration: durations.base, ease: easing.standard }
115
+ }),
116
+ modalScale: safe({
117
+ initial: { opacity: 0, scale: 0.98 },
118
+ animate: { opacity: 1, scale: 1 },
119
+ exit: { opacity: 0, scale: 0.98 },
120
+ transition: { duration: durations.base, ease: easing.emphasized }
121
+ }),
122
+ stagger: {
123
+ container: safe({
124
+ initial: { opacity: 0 },
125
+ animate: { opacity: 1 },
126
+ transition: { staggerChildren: 0.06, delayChildren: 0.02 }
127
+ }),
128
+ item: safe({
129
+ initial: { opacity: 0, y: 8 },
130
+ animate: { opacity: 1, y: 0 },
131
+ transition: { duration: durations.base, ease: easing.standard }
132
+ })
133
+ },
134
+ meta: { prefersReduced, durations, easing }
135
+ };
136
+ }
137
+
138
+ // src/registry/index.ts
139
+ var registry = [
140
+ // UI Primitives
141
+ {
142
+ name: "button",
143
+ type: "ui",
144
+ title: "Button",
145
+ description: "A button component with multiple variants, sizes, and loading state.",
146
+ sourcePath: "registry/ui/button",
147
+ dependencies: ["@radix-ui/react-slot", "class-variance-authority", "lucide-react"],
148
+ categories: ["primitives", "forms"]
149
+ },
150
+ {
151
+ name: "input",
152
+ type: "ui",
153
+ title: "Input",
154
+ description: "An input component with variants for validation states and icon support.",
155
+ sourcePath: "registry/ui/input",
156
+ dependencies: ["class-variance-authority"],
157
+ categories: ["primitives", "forms"]
158
+ },
159
+ {
160
+ name: "card",
161
+ type: "ui",
162
+ title: "Card",
163
+ description: "A card component with header, content, and footer sections.",
164
+ sourcePath: "registry/ui/card",
165
+ categories: ["primitives", "layout"]
166
+ },
167
+ {
168
+ name: "badge",
169
+ type: "ui",
170
+ title: "Badge",
171
+ description: "A compact inline label for status, category or metadata: solid, soft and outline types in six semantic tones, with an optional icon, avatar and close button.",
172
+ sourcePath: "registry/ui/badge",
173
+ dependencies: ["class-variance-authority", "lucide-react"],
174
+ categories: ["primitives", "data-display"]
175
+ },
176
+ {
177
+ name: "avatar",
178
+ type: "ui",
179
+ title: "Avatar",
180
+ description: "A user's photo, initials or icon, in three sizes and two radii, with an optional border and presence dot. Falls back from photo to initials to icon on its own.",
181
+ sourcePath: "registry/ui/avatar",
182
+ dependencies: ["class-variance-authority", "lucide-react"],
183
+ registryDependencies: ["status-indicator"],
184
+ categories: ["primitives", "data-display"]
185
+ },
186
+ {
187
+ name: "status-indicator",
188
+ type: "ui",
189
+ title: "Status Indicator",
190
+ description: "A presence dot \u2014 online, away, busy or offline \u2014 with an accessible name in the page's language, so colour never carries the status alone.",
191
+ sourcePath: "registry/ui/status-indicator",
192
+ dependencies: ["class-variance-authority"],
193
+ categories: ["primitives", "data-display"]
194
+ },
195
+ {
196
+ name: "skeleton",
197
+ type: "ui",
198
+ title: "Skeleton",
199
+ description: "Loading placeholders that replace shadcn's Skeleton: a plain block plus text, avatar, list item and card shapes that stay still under reduced motion.",
200
+ sourcePath: "registry/ui/skeleton",
201
+ categories: ["primitives", "feedback"]
202
+ },
203
+ {
204
+ name: "radio",
205
+ type: "ui",
206
+ title: "Radio",
207
+ description: "An accessible radio for choosing one option from a set, with a plain and a bordered layout variant.",
208
+ sourcePath: "registry/ui/radio",
209
+ dependencies: ["class-variance-authority"],
210
+ categories: ["primitives", "forms"]
211
+ },
212
+ // Blocks
213
+ {
214
+ name: "app-shell",
215
+ type: "block",
216
+ title: "App Shell",
217
+ description: "Application shell with sidebar, header, and content area.",
218
+ sourcePath: "registry/blocks/app-shell",
219
+ categories: ["layout", "application"]
220
+ },
221
+ {
222
+ name: "page-header",
223
+ type: "block",
224
+ title: "Page Header",
225
+ description: "Page header with title, breadcrumb, description, and actions.",
226
+ sourcePath: "registry/blocks/page-header",
227
+ categories: ["layout", "navigation"]
228
+ },
229
+ {
230
+ name: "empty-state",
231
+ type: "block",
232
+ title: "Empty State",
233
+ description: "Empty state component with icon, title, description, and actions.",
234
+ sourcePath: "registry/blocks/empty-state",
235
+ dependencies: ["lucide-react"],
236
+ categories: ["feedback", "data-display"]
237
+ },
238
+ {
239
+ name: "form-section",
240
+ type: "block",
241
+ title: "Form Section",
242
+ description: "Form section with title, description, fields, and actions.",
243
+ sourcePath: "registry/blocks/form-section",
244
+ categories: ["forms", "layout"]
245
+ },
246
+ {
247
+ name: "data-table",
248
+ type: "block",
249
+ title: "Data Table",
250
+ description: "Data table with sorting, pagination, and custom cell rendering.",
251
+ sourcePath: "registry/blocks/data-table",
252
+ categories: ["data-display", "tables"]
253
+ },
254
+ // Effects
255
+ {
256
+ name: "shimmer-button",
257
+ type: "effect",
258
+ title: "Shimmer Button",
259
+ description: "A button with animated shimmer effect.",
260
+ sourcePath: "registry/effects/shimmer-button",
261
+ dependencies: ["framer-motion"],
262
+ categories: ["effects", "buttons"]
263
+ },
264
+ {
265
+ name: "animated-gradient",
266
+ type: "effect",
267
+ title: "Animated Gradient",
268
+ description: "Animated gradient background and text effects.",
269
+ sourcePath: "registry/effects/animated-gradient",
270
+ dependencies: ["framer-motion"],
271
+ categories: ["effects", "backgrounds"]
272
+ },
273
+ {
274
+ name: "text-reveal",
275
+ type: "effect",
276
+ title: "Text Reveal",
277
+ description: "Character and word reveal animations.",
278
+ sourcePath: "registry/effects/text-reveal",
279
+ dependencies: ["framer-motion"],
280
+ categories: ["effects", "text"]
281
+ },
282
+ {
283
+ name: "border-beam",
284
+ type: "effect",
285
+ title: "Border Beam",
286
+ description: "Animated border beam and glowing border effects.",
287
+ sourcePath: "registry/effects/border-beam",
288
+ dependencies: ["framer-motion"],
289
+ categories: ["effects", "borders"]
290
+ },
291
+ {
292
+ name: "spotlight",
293
+ type: "effect",
294
+ title: "Spotlight",
295
+ description: "Cursor-following spotlight effect.",
296
+ sourcePath: "registry/effects/spotlight",
297
+ dependencies: ["framer-motion"],
298
+ categories: ["effects", "interactive"]
299
+ }
300
+ ];
301
+ function getRegistryItem(name) {
302
+ return registry.find((item) => item.name === name);
303
+ }
304
+ function getRegistryItemsByType(type) {
305
+ return registry.filter((item) => item.type === type);
306
+ }
307
+ function getRegistryItemsByCategory(category) {
308
+ return registry.filter((item) => item.categories?.includes(category));
309
+ }
310
+
311
+ exports.baseTokens = baseTokens;
312
+ exports.cn = cn;
313
+ exports.getRegistryItem = getRegistryItem;
314
+ exports.getRegistryItemsByCategory = getRegistryItemsByCategory;
315
+ exports.getRegistryItemsByType = getRegistryItemsByType;
316
+ exports.registry = registry;
317
+ exports.semanticColor = semanticColor;
318
+ exports.tailwindSemanticColors = tailwindSemanticColors;
319
+ exports.useMotionPresets = useMotionPresets;
@@ -0,0 +1,5 @@
1
+ export { cn } from './lib/utils.cjs';
2
+ export { Density, TokenSet, baseTokens, semanticColor, tailwindSemanticColors } from './tokens/index.cjs';
3
+ export { MotionPreset, useMotionPresets } from './motion/index.cjs';
4
+ export { RegistryItem, RegistryItemType, getRegistryItem, getRegistryItemsByCategory, getRegistryItemsByType, registry } from './registry/index.cjs';
5
+ import 'clsx';
package/dist/index.js CHANGED
@@ -194,7 +194,7 @@ var registry = [
194
194
  name: "skeleton",
195
195
  type: "ui",
196
196
  title: "Skeleton",
197
- description: "Loading skeleton placeholders with pre-built variants for text, avatars, and cards.",
197
+ description: "Loading placeholders that replace shadcn's Skeleton: a plain block plus text, avatar, list item and card shapes that stay still under reduced motion.",
198
198
  sourcePath: "registry/ui/skeleton",
199
199
  categories: ["primitives", "feedback"]
200
200
  },
@@ -0,0 +1,11 @@
1
+ 'use strict';
2
+
3
+ var clsx = require('clsx');
4
+ var tailwindMerge = require('tailwind-merge');
5
+
6
+ // src/lib/utils.ts
7
+ function cn(...inputs) {
8
+ return tailwindMerge.twMerge(clsx.clsx(inputs));
9
+ }
10
+
11
+ exports.cn = cn;
@@ -0,0 +1,9 @@
1
+ import { ClassValue } from 'clsx';
2
+
3
+ /**
4
+ * Merge Tailwind CSS classes with clsx and tailwind-merge.
5
+ * This ensures proper class merging and deduplication.
6
+ */
7
+ declare function cn(...inputs: ClassValue[]): string;
8
+
9
+ export { cn };
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+
3
+ var framerMotion = require('framer-motion');
4
+
5
+ // src/motion/presets.ts
6
+ function useMotionPresets() {
7
+ const prefersReduced = framerMotion.useReducedMotion();
8
+ const durations = {
9
+ fast: 0.12,
10
+ base: 0.18,
11
+ slow: 0.26
12
+ };
13
+ const easing = {
14
+ standard: [0.2, 0, 0, 1],
15
+ emphasized: [0.2, 0.8, 0.2, 1]
16
+ };
17
+ const safe = (preset) => {
18
+ if (!prefersReduced) return preset;
19
+ return {
20
+ initial: { opacity: 0 },
21
+ animate: { opacity: 1 },
22
+ exit: { opacity: 0 },
23
+ transition: { duration: durations.fast }
24
+ };
25
+ };
26
+ return {
27
+ fadeIn: safe({
28
+ initial: { opacity: 0 },
29
+ animate: { opacity: 1 },
30
+ exit: { opacity: 0 },
31
+ transition: { duration: durations.base, ease: easing.standard }
32
+ }),
33
+ slideUp: safe({
34
+ initial: { opacity: 0, y: 10 },
35
+ animate: { opacity: 1, y: 0 },
36
+ exit: { opacity: 0, y: 6 },
37
+ transition: { duration: durations.base, ease: easing.standard }
38
+ }),
39
+ modalScale: safe({
40
+ initial: { opacity: 0, scale: 0.98 },
41
+ animate: { opacity: 1, scale: 1 },
42
+ exit: { opacity: 0, scale: 0.98 },
43
+ transition: { duration: durations.base, ease: easing.emphasized }
44
+ }),
45
+ stagger: {
46
+ container: safe({
47
+ initial: { opacity: 0 },
48
+ animate: { opacity: 1 },
49
+ transition: { staggerChildren: 0.06, delayChildren: 0.02 }
50
+ }),
51
+ item: safe({
52
+ initial: { opacity: 0, y: 8 },
53
+ animate: { opacity: 1, y: 0 },
54
+ transition: { duration: durations.base, ease: easing.standard }
55
+ })
56
+ },
57
+ meta: { prefersReduced, durations, easing }
58
+ };
59
+ }
60
+
61
+ exports.useMotionPresets = useMotionPresets;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Motion presets: use these across the app to keep motion consistent.
3
+ * Rule: do not hardcode durations/easing elsewhere.
4
+ */
5
+ type MotionPreset = {
6
+ initial?: Record<string, unknown>;
7
+ animate?: Record<string, unknown>;
8
+ exit?: Record<string, unknown>;
9
+ transition?: Record<string, unknown>;
10
+ };
11
+ declare function useMotionPresets(): {
12
+ fadeIn: MotionPreset;
13
+ slideUp: MotionPreset;
14
+ modalScale: MotionPreset;
15
+ stagger: {
16
+ container: MotionPreset;
17
+ item: MotionPreset;
18
+ };
19
+ meta: {
20
+ prefersReduced: boolean | null;
21
+ durations: {
22
+ fast: number;
23
+ base: number;
24
+ slow: number;
25
+ };
26
+ easing: {
27
+ standard: number[];
28
+ emphasized: number[];
29
+ };
30
+ };
31
+ };
32
+
33
+ export { type MotionPreset, useMotionPresets };