@bearmenu/ui 0.6.0 → 0.7.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.
Files changed (80) hide show
  1. package/dist/chunk-A5ONAJCE.js +102 -0
  2. package/dist/chunk-AV777R4G.js +43 -0
  3. package/dist/{chunk-WKMLZDUT.js → chunk-FRPPIXMY.js} +1 -1
  4. package/dist/{chunk-PSR7N36A.js → chunk-HCM6LKM5.js} +1 -1
  5. package/dist/{chunk-YJ6OBKZJ.js → chunk-TL4YU2O5.js} +10 -3
  6. package/dist/chunk-UX6SWAKH.js +320 -0
  7. package/dist/{chunk-7PPCH42H.js → chunk-VQNDZVI7.js} +1 -1
  8. package/dist/chunk-WSRWFA6K.js +26 -0
  9. package/dist/chunk-YLNYXPIH.js +61 -0
  10. package/dist/components/accordion.js +1 -1
  11. package/dist/components/alert.d.ts +2 -2
  12. package/dist/components/badge.d.ts +3 -2
  13. package/dist/components/badge.js +1 -1
  14. package/dist/components/button.d.ts +1 -1
  15. package/dist/components/card.d.ts +2 -2
  16. package/dist/components/card.js +127 -4
  17. package/dist/components/combobox.js +2 -2
  18. package/dist/components/command.d.ts +15 -15
  19. package/dist/components/command.js +2 -2
  20. package/dist/components/currency-input.js +1 -1
  21. package/dist/components/date-input.d.ts +1 -1
  22. package/dist/components/date-input.js +1 -1
  23. package/dist/components/dialog.js +1 -1
  24. package/dist/components/filter-category-row.d.ts +13 -0
  25. package/dist/components/filter-category-row.js +44 -0
  26. package/dist/components/input-group.d.ts +2 -0
  27. package/dist/components/input-group.js +10 -5
  28. package/dist/components/input.d.ts +1 -0
  29. package/dist/components/input.js +1 -1
  30. package/dist/components/item.d.ts +2 -2
  31. package/dist/components/multi-select-combobox.js +3 -3
  32. package/dist/components/phone-frame.js +1 -1
  33. package/dist/components/place-about-tab.d.ts +2 -2
  34. package/dist/components/place-about-tab.js +4 -3
  35. package/dist/components/place-details-mobile.js +5 -3
  36. package/dist/components/place-schedule-week.d.ts +19 -0
  37. package/dist/components/place-schedule-week.js +4 -0
  38. package/dist/components/place-types.d.ts +18 -9
  39. package/dist/components/scroll-fade.d.ts +15 -0
  40. package/dist/components/scroll-fade.js +121 -0
  41. package/dist/components/searchable-select.js +2 -2
  42. package/dist/components/separator.js +3 -26
  43. package/dist/components/sheet.js +1 -1
  44. package/dist/components/sliding-panels.d.ts +1 -0
  45. package/dist/components/sliding-panels.js +22 -6
  46. package/dist/components/sort-option-list.d.ts +16 -0
  47. package/dist/components/sort-option-list.js +46 -0
  48. package/dist/components/spinner.d.ts +2 -2
  49. package/dist/components/tabs.js +98 -17
  50. package/dist/components/tag.d.ts +2 -2
  51. package/dist/components/tag.js +3 -61
  52. package/dist/components/text.d.ts +3 -3
  53. package/dist/components/toast.js +1 -1
  54. package/dist/components/toaster.js +1 -1
  55. package/package.json +23 -112
  56. package/src/components/accordion.tsx +1 -1
  57. package/src/components/badge.tsx +9 -2
  58. package/src/components/card.tsx +22 -18
  59. package/src/components/dialog.tsx +1 -1
  60. package/src/components/filter-category-row.tsx +52 -0
  61. package/src/components/input-group.tsx +9 -4
  62. package/src/components/input.tsx +8 -3
  63. package/src/components/phone-frame.tsx +1 -1
  64. package/src/components/place-about-tab.stories.tsx +24 -13
  65. package/src/components/place-about-tab.test.tsx +92 -38
  66. package/src/components/place-about-tab.tsx +310 -344
  67. package/src/components/place-schedule-week.tsx +128 -0
  68. package/src/components/place-types.ts +27 -8
  69. package/src/components/scroll-fade.stories.tsx +98 -0
  70. package/src/components/scroll-fade.tsx +132 -0
  71. package/src/components/sheet.tsx +1 -1
  72. package/src/components/sliding-panels.tsx +33 -6
  73. package/src/components/sort-option-list.tsx +57 -0
  74. package/src/components/tabs.tsx +76 -11
  75. package/src/components/toast.tsx +1 -1
  76. package/src/globals.css +77 -19
  77. package/src/lib/motion.ts +137 -0
  78. package/dist/chunk-BAZFVSSG.js +0 -279
  79. package/dist/chunk-RIDXFY6M.js +0 -38
  80. package/dist/chunk-XK2ZDQVE.js +0 -122
@@ -2,6 +2,8 @@
2
2
 
3
3
  import * as React from "react";
4
4
  import * as TabsPrimitive from "@radix-ui/react-tabs";
5
+ import { motion } from "framer-motion";
6
+ import { archetype } from "../lib/motion";
5
7
  import { cn } from "../lib/utils";
6
8
 
7
9
  type TabsVariant = "pill" | "underline";
@@ -10,11 +12,15 @@ type TabsOrientation = "horizontal" | "vertical";
10
12
  type TabsContextValue = {
11
13
  variant: TabsVariant;
12
14
  orientation: TabsOrientation;
15
+ /** Unique id used by Framer Motion `layoutId` to morph the active indicator
16
+ * between triggers. Generated once per <TabsList>. */
17
+ indicatorId: string;
13
18
  };
14
19
 
15
20
  const TabsContext = React.createContext<TabsContextValue>({
16
21
  variant: "pill",
17
22
  orientation: "horizontal",
23
+ indicatorId: "tabs-indicator",
18
24
  });
19
25
 
20
26
  const Tabs = React.forwardRef<
@@ -45,9 +51,10 @@ const TabsList = React.forwardRef<
45
51
  >(({ className, variant = "pill", scrollable, children, ...props }, ref) => {
46
52
  const orientation: TabsOrientation = props["aria-orientation"] === "vertical" ? "vertical" : "horizontal";
47
53
  const allowScroll = scrollable ?? orientation === "horizontal";
54
+ const indicatorId = React.useId();
48
55
 
49
56
  return (
50
- <TabsContext.Provider value={{ variant, orientation }}>
57
+ <TabsContext.Provider value={{ variant, orientation, indicatorId }}>
51
58
  <TabsPrimitive.List
52
59
  ref={ref}
53
60
  data-slot="tabs-list"
@@ -75,27 +82,80 @@ TabsList.displayName = TabsPrimitive.List.displayName;
75
82
  const TabsTrigger = React.forwardRef<
76
83
  React.ElementRef<typeof TabsPrimitive.Trigger>,
77
84
  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
78
- >(({ className, ...props }, ref) => {
79
- const { variant, orientation } = React.useContext(TabsContext);
85
+ >(({ className, children, ...props }, ref) => {
86
+ const { variant, orientation, indicatorId } = React.useContext(TabsContext);
87
+ // Inspect data-state at render to know whether to render the indicator.
88
+ // Radix sets data-state="active" | "inactive" on the trigger via its primitive.
89
+ const triggerRef = React.useRef<HTMLButtonElement | null>(null);
90
+ const [isActive, setIsActive] = React.useState(false);
91
+
92
+ // Sync isActive from Radix's data-state. Tracking via mutation observer
93
+ // keeps the indicator in sync without needing controlled state from consumers.
94
+ React.useEffect(() => {
95
+ const node = triggerRef.current;
96
+ if (!node) return;
97
+ const sync = () => setIsActive(node.getAttribute("data-state") === "active");
98
+ sync();
99
+ const observer = new MutationObserver(sync);
100
+ observer.observe(node, { attributes: true, attributeFilter: ["data-state"] });
101
+ return () => observer.disconnect();
102
+ }, []);
103
+
104
+ const composedRef = React.useCallback(
105
+ (node: HTMLButtonElement | null) => {
106
+ triggerRef.current = node;
107
+ if (typeof ref === "function") ref(node);
108
+ else if (ref) (ref as React.MutableRefObject<HTMLButtonElement | null>).current = node;
109
+ },
110
+ [ref]
111
+ );
80
112
 
81
113
  return (
82
114
  <TabsPrimitive.Trigger
83
- ref={ref}
115
+ ref={composedRef}
84
116
  data-slot="tabs-trigger"
85
117
  className={cn(
86
- "inline-flex items-center justify-center whitespace-nowrap text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
118
+ "relative inline-flex items-center justify-center whitespace-nowrap text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
87
119
  variant === "pill" &&
88
- "rounded-xl px-4 py-1.5 text-muted-foreground hover:text-foreground data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
120
+ "rounded-xl px-4 py-1.5 text-muted-foreground hover:text-foreground data-[state=active]:text-foreground",
89
121
  variant === "underline" &&
90
122
  orientation === "horizontal" &&
91
- "relative h-11 rounded-none border-b-2 border-transparent px-4 text-muted-foreground hover:text-foreground data-[state=active]:border-primary data-[state=active]:text-foreground",
123
+ "h-11 rounded-none px-4 text-muted-foreground hover:text-foreground data-[state=active]:text-foreground",
92
124
  variant === "underline" &&
93
125
  orientation === "vertical" &&
94
- "relative rounded-none border-r-2 border-transparent px-4 py-2 -mr-px text-muted-foreground hover:text-foreground data-[state=active]:border-primary data-[state=active]:text-foreground justify-start",
126
+ "rounded-none px-4 py-2 -mr-px text-muted-foreground hover:text-foreground data-[state=active]:text-foreground justify-start",
95
127
  className
96
128
  )}
97
129
  {...props}
98
- />
130
+ >
131
+ {/* Active indicator: shared-layout element morphs between triggers */}
132
+ {isActive && (
133
+ <motion.span
134
+ aria-hidden
135
+ layoutId={indicatorId}
136
+ transition={archetype.indicatorMorph}
137
+ className={cn(
138
+ "absolute inset-0 -z-0 motion-reduce:hidden",
139
+ variant === "pill" && "rounded-xl bg-background shadow-sm",
140
+ variant === "underline" && orientation === "horizontal" && "border-b-2 border-primary",
141
+ variant === "underline" && orientation === "vertical" && "border-r-2 border-primary"
142
+ )}
143
+ />
144
+ )}
145
+ {/* Static fallback for reduced-motion: keep the legacy active styling */}
146
+ {isActive && (
147
+ <span
148
+ aria-hidden
149
+ className={cn(
150
+ "absolute inset-0 -z-0 hidden motion-reduce:block",
151
+ variant === "pill" && "rounded-xl bg-background shadow-sm",
152
+ variant === "underline" && orientation === "horizontal" && "border-b-2 border-primary",
153
+ variant === "underline" && orientation === "vertical" && "border-r-2 border-primary"
154
+ )}
155
+ />
156
+ )}
157
+ <span className="relative z-10 flex items-center gap-2">{children}</span>
158
+ </TabsPrimitive.Trigger>
99
159
  );
100
160
  });
101
161
  TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
@@ -103,16 +163,21 @@ TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
103
163
  const TabsContent = React.forwardRef<
104
164
  React.ElementRef<typeof TabsPrimitive.Content>,
105
165
  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
106
- >(({ className, ...props }, ref) => (
166
+ >(({ className, children, ...props }, ref) => (
107
167
  <TabsPrimitive.Content
108
168
  ref={ref}
109
169
  data-slot="tabs-content"
110
170
  className={cn(
111
171
  "flex-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-md",
172
+ // v1.0 motion: content-swap on activation. CSS animation so this works
173
+ // even outside a Framer-Motion-wrapped layout. Disabled for reduced-motion.
174
+ "data-[state=active]:animate-in data-[state=active]:fade-in-0 data-[state=active]:slide-in-from-bottom-1 data-[state=active]:duration-[var(--duration-quick)] data-[state=active]:ease-[var(--ease-standard)] motion-reduce:data-[state=active]:animate-none",
112
175
  className
113
176
  )}
114
177
  {...props}
115
- />
178
+ >
179
+ {children}
180
+ </TabsPrimitive.Content>
116
181
  ));
117
182
  TabsContent.displayName = TabsPrimitive.Content.displayName;
118
183
 
@@ -24,7 +24,7 @@ const ToastViewport = React.forwardRef<
24
24
  ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
25
25
 
26
26
  const toastVariants = cva(
27
- "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-xl p-6 pr-8 shadow-2xl transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
27
+ "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-xl p-6 pr-8 shadow-2xl transition-[transform,opacity] duration-[var(--duration-standard)] ease-[var(--ease-standard)] data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=open]:duration-[var(--duration-standard)] data-[state=closed]:duration-[var(--duration-exit-standard)] data-[state=open]:ease-[var(--ease-enter)] data-[state=closed]:ease-[var(--ease-exit)] data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
28
28
  {
29
29
  variants: {
30
30
  variant: {
package/src/globals.css CHANGED
@@ -89,18 +89,30 @@ h6 {
89
89
  --shadow-alpha-xl: 0.18;
90
90
  --shadow-alpha-2xl: 0.25;
91
91
 
92
- /* Motion */
93
- --duration-instant: 0ms;
94
- --duration-fast: 150ms;
95
- --duration-base: 200ms;
96
- --duration-slow: 300ms;
97
- --duration-slower: 500ms;
98
- --duration-drawer: 350ms;
99
- --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
100
- --ease-in: cubic-bezier(0.7, 0, 0.84, 0);
101
- --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
102
- --ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);
103
- --ease-spring: cubic-bezier(0.5, 1.5, 0.5, 1);
92
+ /* ─── Motion System v1.0 ─────────────────────────────────────────────
93
+ Three layers of tokens. Feature code reads only `--duration-*` and
94
+ `--ease-*` semantic names; raw values live here.
95
+
96
+ JS twin: @bearmenu/ui/lib/motion (duration / ease / archetype). */
97
+
98
+ /* Durations */
99
+ --duration-none: 0ms; /* literal "no animation" */
100
+ --duration-instant: 120ms; /* tap feedback, icon swap */
101
+ --duration-quick: 180ms; /* chip toggle, tab indicator */
102
+ --duration-standard: 260ms; /* component entrance — workhorse */
103
+ --duration-deliberate: 360ms; /* drawer / sheet open */
104
+ --duration-narrative: 500ms; /* editorial hero (sparingly) */
105
+ --duration-exit-quick: 110ms; /* content exit inside a surface */
106
+ --duration-exit-standard: 210ms; /* surface / overlay exit */
107
+
108
+ /* Easings */
109
+ --ease-enter: cubic-bezier(0, 0, 0.2, 1);
110
+ --ease-standard: cubic-bezier(0.4, 0, 0.2, 1);
111
+ --ease-exit: cubic-bezier(0.55, 0, 1, 0.45);
112
+ /* CSS equivalents for the JS spring presets — for keyframe / transition use */
113
+ --ease-spring-snappy: cubic-bezier(0.34, 1.56, 0.64, 1);
114
+ --ease-spring-smooth: cubic-bezier(0.22, 1, 0.36, 1);
115
+ --ease-spring-soft: cubic-bezier(0.16, 1, 0.3, 1);
104
116
 
105
117
  /* Z-index scale */
106
118
  --z-base: 0;
@@ -246,8 +258,8 @@ h6 {
246
258
  --shadow-2xl: 0 25px 50px -12px hsl(var(--shadow-color) / var(--shadow-alpha-2xl));
247
259
 
248
260
  /* Default transition tokens — consumed by Tailwind's `transition-*` utilities */
249
- --default-transition-duration: var(--duration-base);
250
- --default-transition-timing-function: var(--ease-out);
261
+ --default-transition-duration: var(--duration-standard);
262
+ --default-transition-timing-function: var(--ease-standard);
251
263
 
252
264
  --animate-highlight-ring: highlight-ring 5s ease-out forwards;
253
265
  }
@@ -296,8 +308,8 @@ h6 {
296
308
 
297
309
  .glass-hover {
298
310
  transition:
299
- background-color var(--duration-base) var(--ease-out),
300
- border-color var(--duration-base) var(--ease-out);
311
+ background-color var(--duration-standard) var(--ease-standard),
312
+ border-color var(--duration-standard) var(--ease-standard);
301
313
  }
302
314
 
303
315
  .glass-hover:hover {
@@ -407,11 +419,11 @@ h6 {
407
419
  }
408
420
 
409
421
  .animate-collapsible-down {
410
- animation: collapsible-down var(--duration-slow) var(--ease-out);
422
+ animation: collapsible-down var(--duration-deliberate) var(--ease-enter);
411
423
  }
412
424
 
413
425
  .animate-collapsible-up {
414
- animation: collapsible-up var(--duration-base) var(--ease-in);
426
+ animation: collapsible-up var(--duration-exit-standard) var(--ease-exit);
415
427
  }
416
428
 
417
429
  /* Shimmer — referenced by Skeleton (was undefined → animation was dead) */
@@ -421,13 +433,59 @@ h6 {
421
433
  }
422
434
  }
423
435
 
436
+ /* Live-pulse — referenced by Badge `live` variant, EventBanner live indicator,
437
+ and any "this is happening right now" surface. Subtle to avoid distraction. */
438
+ @keyframes live-pulse {
439
+ 0%, 100% {
440
+ transform: scale(1);
441
+ opacity: 1;
442
+ }
443
+ 50% {
444
+ transform: scale(1.08);
445
+ opacity: 0.7;
446
+ }
447
+ }
448
+
449
+ .motion-live-pulse {
450
+ animation: live-pulse 1.8s ease-in-out infinite;
451
+ transform-origin: center;
452
+ }
453
+
454
+ /* Image-enter — gallery / hero photographs. Slight scale-down + fade so images
455
+ read as "arriving" rather than popping. Standalone CSS so consumers don't
456
+ need framer-motion on every photo. */
457
+ @keyframes image-enter {
458
+ from {
459
+ opacity: 0;
460
+ transform: scale(1.04);
461
+ }
462
+ to {
463
+ opacity: 1;
464
+ transform: scale(1);
465
+ }
466
+ }
467
+
468
+ .motion-image-enter {
469
+ animation: image-enter var(--duration-standard) var(--ease-standard) both;
470
+ }
471
+
472
+ /* Reduced-motion: instant — no scale, no fade. */
473
+ @media (prefers-reduced-motion: reduce) {
474
+ .motion-image-enter {
475
+ animation: none;
476
+ }
477
+ .motion-live-pulse {
478
+ animation: none;
479
+ }
480
+ }
481
+
424
482
  /* Swiper pagination styling for experience cards */
425
483
  .swiper-pagination-bullet {
426
484
  background: color-mix(in oklch, var(--foreground) 30%, transparent) !important;
427
485
  opacity: 1 !important;
428
486
  width: 6px !important;
429
487
  height: 6px !important;
430
- transition: all var(--duration-slow) var(--ease-out) !important;
488
+ transition: width var(--duration-standard) var(--ease-standard), background var(--duration-standard) var(--ease-standard) !important;
431
489
  }
432
490
 
433
491
  .swiper-pagination-bullet-active {
@@ -0,0 +1,137 @@
1
+ import type { Transition, Variants } from "framer-motion";
2
+
3
+ /**
4
+ * BearMenu Motion System — v1.0
5
+ *
6
+ * Three layers:
7
+ * 1. Primitives — durations + easing curves (raw values)
8
+ * 2. Springs — Framer Motion spring configs (named)
9
+ * 3. Archetypes — compound presets that compose primitives + springs
10
+ *
11
+ * Feature code reaches for archetypes, never primitives.
12
+ * Adding a new archetype is a DS-maintainer task.
13
+ *
14
+ * Companion CSS custom properties live in `./globals.css`:
15
+ * --duration-instant / --duration-quick / --duration-standard /
16
+ * --duration-deliberate / --duration-narrative /
17
+ * --duration-exit-quick / --duration-exit-standard
18
+ * --ease-enter / --ease-standard / --ease-exit
19
+ */
20
+
21
+ // ─── Layer 1: Primitives ─────────────────────────────────────────────────
22
+
23
+ export const duration = {
24
+ instant: 0.12,
25
+ quick: 0.18,
26
+ standard: 0.26,
27
+ deliberate: 0.36,
28
+ narrative: 0.5,
29
+ exitQuick: 0.11,
30
+ exitStandard: 0.21,
31
+ } as const;
32
+
33
+ export const ease = {
34
+ enter: [0.0, 0.0, 0.2, 1.0] as const,
35
+ standard: [0.4, 0.0, 0.2, 1.0] as const,
36
+ exit: [0.55, 0.0, 1.0, 0.45] as const,
37
+ } as const;
38
+
39
+ // ─── Layer 2: Springs ────────────────────────────────────────────────────
40
+
41
+ export const springSnappy: Transition = {
42
+ type: "spring",
43
+ damping: 30,
44
+ stiffness: 380,
45
+ };
46
+
47
+ export const springSmooth: Transition = {
48
+ type: "spring",
49
+ damping: 30,
50
+ stiffness: 220,
51
+ };
52
+
53
+ export const springSoft: Transition = {
54
+ type: "spring",
55
+ damping: 35,
56
+ stiffness: 140,
57
+ };
58
+
59
+ /** @deprecated use `springSoft`. Kept as alias during migration. */
60
+ export const springGentle = springSoft;
61
+
62
+ // ─── Layer 3: Archetypes (composed presets) ──────────────────────────────
63
+
64
+ export const archetype = {
65
+ /** Drawer / Sheet / Dialog entrance. Surface arrives first; content layers after. */
66
+ surfaceOpen: { ...springSmooth } as Transition,
67
+ surfaceClose: { duration: duration.exitStandard, ease: ease.exit } as Transition,
68
+
69
+ /** First-reveal stagger for card grids. Per-item spring. Cap stagger at 8 items. */
70
+ listReveal: { ...springSoft } as Transition,
71
+ listRevealStagger: 0.04,
72
+ listRevealMaxItems: 8,
73
+
74
+ /** Tab content swap, skeleton → data swap. Opacity-only — no transform. */
75
+ contentSwap: { duration: duration.quick, ease: ease.standard } as Transition,
76
+ contentSwapExit: { duration: duration.exitQuick, ease: ease.exit } as Transition,
77
+
78
+ /** Bottom-sheet / side-drawer entry. */
79
+ edgeEnter: { ...springSmooth } as Transition,
80
+ edgeExit: { duration: duration.exitStandard, ease: ease.exit } as Transition,
81
+
82
+ /** Card / button tap feedback. */
83
+ pressResponse: { duration: duration.instant, ease: ease.enter } as Transition,
84
+
85
+ /** Active tab pill, active filter chip — use with Framer Motion `layoutId`. */
86
+ indicatorMorph: { ...springSnappy } as Transition,
87
+
88
+ /** Image enter — slight scale on load. */
89
+ imageEnter: { duration: 0.3, ease: ease.standard } as Transition,
90
+ } as const;
91
+
92
+ // ─── Stagger gaps (seconds, for `staggerChildren`) ───────────────────────
93
+
94
+ export const stagger = {
95
+ tight: 0.03,
96
+ standard: 0.04,
97
+ loose: 0.06,
98
+ } as const;
99
+
100
+ // ─── Variant builders (common patterns) ──────────────────────────────────
101
+
102
+ /**
103
+ * List-reveal container + item variants. Pair with Framer Motion `motion.ul`/`motion.div`.
104
+ * Container drives stagger; item handles the actual fade + Y-shift.
105
+ */
106
+ export const listRevealVariants: { container: Variants; item: Variants } = {
107
+ container: {
108
+ hidden: { opacity: 1 },
109
+ show: {
110
+ opacity: 1,
111
+ transition: { staggerChildren: stagger.standard },
112
+ },
113
+ },
114
+ item: {
115
+ hidden: { opacity: 0, y: 8 },
116
+ show: { opacity: 1, y: 0, transition: archetype.listReveal },
117
+ },
118
+ };
119
+
120
+ /**
121
+ * Content-swap variants for `AnimatePresence mode="wait"`. Opacity + tiny Y shift.
122
+ * Use for tab panels.
123
+ */
124
+ export const contentSwapVariants: Variants = {
125
+ hidden: { opacity: 0, y: 6 },
126
+ show: { opacity: 1, y: 0, transition: archetype.contentSwap },
127
+ exit: { opacity: 0, y: -4, transition: archetype.contentSwapExit },
128
+ };
129
+
130
+ /**
131
+ * Skeleton → data swap. Opacity-only to prevent layout shift on slow networks.
132
+ */
133
+ export const skeletonSwapVariants: Variants = {
134
+ hidden: { opacity: 0 },
135
+ show: { opacity: 1, transition: archetype.contentSwap },
136
+ exit: { opacity: 0, transition: archetype.contentSwapExit },
137
+ };