chati-dev 4.2.2 → 4.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.
Files changed (220) hide show
  1. package/README.md +80 -53
  2. package/bin/chati.js +150 -5
  3. package/framework/agents/build/dev.md +509 -76
  4. package/framework/agents/deploy/devops.md +40 -25
  5. package/framework/agents/discover/brief.md +156 -22
  6. package/framework/agents/discover/brownfield-wu.md +24 -14
  7. package/framework/agents/discover/greenfield-wu.md +100 -14
  8. package/framework/agents/plan/architect-data-engineer.md +6 -6
  9. package/framework/agents/plan/architect-system.md +46 -12
  10. package/framework/agents/plan/architect.md +40 -20
  11. package/framework/agents/plan/detail.md +36 -24
  12. package/framework/agents/plan/phases.md +36 -26
  13. package/framework/agents/plan/tasks.md +114 -33
  14. package/framework/agents/plan/ux-brand-architect.md +240 -8
  15. package/framework/agents/plan/ux-component-engineer.md +28 -13
  16. package/framework/agents/plan/ux-researcher.md +7 -6
  17. package/framework/agents/plan/ux.md +55 -22
  18. package/framework/agents/quality/qa-implementation.md +143 -74
  19. package/framework/agents/quality/qa-planning.md +115 -42
  20. package/framework/agents/quality/qa-visual.md +439 -0
  21. package/framework/agents/shared/visualizer.md +128 -0
  22. package/framework/config.yaml +7 -6
  23. package/framework/constitution.md +127 -44
  24. package/framework/context/governance.md +12 -7
  25. package/framework/context/quality.md +6 -5
  26. package/framework/context/root.md +6 -6
  27. package/framework/data/entity-registry.yaml +377 -4
  28. package/framework/data/model-limits.json +19 -0
  29. package/framework/domains/agents/qa-visual.yaml +74 -0
  30. package/framework/domains/constitution.yaml +46 -2
  31. package/framework/domains/workflows/greenfield-fullstack.yaml +2 -2
  32. package/framework/hooks/advance-trigger.js +131 -0
  33. package/framework/hooks/brief-validator.js +83 -0
  34. package/framework/hooks/constitution-guard.js +24 -5
  35. package/framework/hooks/license-guard.js +62 -27
  36. package/framework/hooks/mode-governance.js +13 -2
  37. package/framework/hooks/model-governance.js +1 -0
  38. package/framework/hooks/post-dev.js +207 -0
  39. package/framework/hooks/prism-engine.js +274 -105
  40. package/framework/hooks/reasoning-escalator.js +371 -0
  41. package/framework/hooks/reference-trigger.js +117 -0
  42. package/framework/hooks/session-digest.js +50 -1
  43. package/framework/hooks/settings.json +32 -1
  44. package/framework/hooks/style-guard.js +25 -6
  45. package/framework/hooks/team-quality-gate.js +19 -12
  46. package/framework/hooks/undercover-guard.js +4 -2
  47. package/framework/i18n/en.yaml +3 -3
  48. package/framework/i18n/es.yaml +3 -3
  49. package/framework/i18n/fr.yaml +3 -3
  50. package/framework/i18n/pt.yaml +3 -3
  51. package/framework/intelligence/context-engine.md +4 -5
  52. package/framework/intelligence/decision-engine.md +1 -1
  53. package/framework/intelligence/hooks-performance.md +3 -3
  54. package/framework/migrations/v1.0-to-v1.1.yaml +1 -1
  55. package/framework/migrations/v1.4-to-v2.0.yaml +11 -11
  56. package/framework/migrations/v4.0-to-v4.1.yaml +2 -2
  57. package/framework/migrations/v4.2-to-v4.3.yaml +29 -0
  58. package/framework/orchestrator/chati-router.js +387 -0
  59. package/framework/orchestrator/chati-update.md +40 -40
  60. package/framework/orchestrator/chati.md +294 -94
  61. package/framework/scaffold/motion-premium/README.md +89 -0
  62. package/framework/scaffold/motion-premium/app/globals.css.template +400 -0
  63. package/framework/scaffold/motion-premium/app/layout.tsx.template +110 -0
  64. package/framework/scaffold/motion-premium/components/animation/BackgroundCrossfadeProvider.tsx.template +170 -0
  65. package/framework/scaffold/motion-premium/components/animation/LenisProvider.tsx.template +49 -0
  66. package/framework/scaffold/motion-premium/components/animation/PageTransitionWrapper.tsx.template +83 -0
  67. package/framework/scaffold/motion-premium/components/animation/Preloader.tsx.template +171 -0
  68. package/framework/scaffold/motion-premium/components/ui/Container.tsx.template +69 -0
  69. package/framework/scaffold/motion-premium/components/ui/PageSection.tsx.template +74 -0
  70. package/framework/scaffold/motion-premium/lib/animations/gsap.ts.template +112 -0
  71. package/framework/scaffold/motion-premium/lib/animations/refreshCoordinator.ts.template +75 -0
  72. package/framework/scaffold/motion-premium/lib/animations/tokens.ts.template +119 -0
  73. package/framework/scaffold/motion-premium/lib/animations/useGsapContext.ts.template +92 -0
  74. package/framework/scaffold/motion-premium/lib/animations/useScrollSnapStepper.ts.template +265 -0
  75. package/framework/scaffold/motion-premium/lib/animations/useSmoothScroll.ts.template +67 -0
  76. package/framework/scaffold/motion-premium/lib/brand.ts.template +43 -0
  77. package/framework/scaffold/motion-premium/scaffold.yaml +174 -0
  78. package/framework/scaffold/motion-premium-3d/README.md +80 -0
  79. package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoCanvas.tsx.template +81 -0
  80. package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoClient.tsx.template +75 -0
  81. package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/page.tsx.template +26 -0
  82. package/framework/scaffold/motion-premium-3d/components/3d/CameraRig.tsx.template +100 -0
  83. package/framework/scaffold/motion-premium-3d/components/3d/CanvasProvider.tsx.template +85 -0
  84. package/framework/scaffold/motion-premium-3d/components/3d/InvalidateOnScroll.tsx.template +51 -0
  85. package/framework/scaffold/motion-premium-3d/components/3d/MeshCrossfade.tsx.template +79 -0
  86. package/framework/scaffold/motion-premium-3d/components/3d/ScrollCrossfade.tsx.template +88 -0
  87. package/framework/scaffold/motion-premium-3d/components/3d/ScrollScene.tsx.template +121 -0
  88. package/framework/scaffold/motion-premium-3d/components/webgl/SceneFallback.tsx.template +65 -0
  89. package/framework/scaffold/motion-premium-3d/components/webgl/WebGLContext.tsx.template +68 -0
  90. package/framework/scaffold/motion-premium-3d/lib/webgl/detect.ts.template +69 -0
  91. package/framework/scaffold/motion-premium-3d/scaffold.yaml +133 -0
  92. package/framework/schemas/session.schema.json +109 -21
  93. package/framework/scripts/reference-capture.js +430 -0
  94. package/framework/scripts/visual-qa.js +674 -0
  95. package/framework/tasks/orchestrator-handoff.md +1 -1
  96. package/framework/tasks/orchestrator-resume.md +1 -1
  97. package/framework/tasks/orchestrator-route.md +1 -1
  98. package/framework/tasks/orchestrator-status.md +3 -3
  99. package/framework/tasks/qa-planning-gate-define.md +1 -1
  100. package/framework/templates/brandbook-html-tmpl.md +1 -1
  101. package/framework/templates/brandbook-tmpl.yaml +1 -1
  102. package/framework/templates/component-spec-tmpl.yaml +1 -1
  103. package/framework/templates/design-token-tmpl.yaml +1 -1
  104. package/framework/templates/icon-system-tmpl.yaml +1 -1
  105. package/framework/templates/team-planning-tasks.yaml +6 -5
  106. package/framework/workflows/brownfield-discovery.yaml +2 -2
  107. package/framework/workflows/brownfield-fullstack.yaml +15 -11
  108. package/framework/workflows/brownfield-service.yaml +14 -10
  109. package/framework/workflows/brownfield-ui.yaml +15 -11
  110. package/framework/workflows/greenfield-fullstack.yaml +16 -13
  111. package/framework/workflows/quick-flow.yaml +3 -3
  112. package/framework/workflows/standard-flow.yaml +12 -9
  113. package/package.json +10 -5
  114. package/src/autonomy/autonomous-gate.js +1 -0
  115. package/src/autonomy/build-state.js +1 -2
  116. package/src/autonomy/progress-reporter.js +1 -1
  117. package/src/config/agent-customizer.js +11 -3
  118. package/src/config/claude-settings-generator.js +27 -7
  119. package/src/config/context-file-generator.js +41 -21
  120. package/src/config/framework-adapter.js +1 -0
  121. package/src/config/gemini-hooks-generator.js +19 -7
  122. package/src/config/mcp-configs.js +1 -0
  123. package/src/context/layers/l1-global.js +2 -1
  124. package/src/dashboard/data-reader.js +4 -3
  125. package/src/dashboard/layout.js +2 -1
  126. package/src/decision/analyzer.js +6 -30
  127. package/src/decision/engine.js +4 -28
  128. package/src/decision/registry-healer.js +3 -2
  129. package/src/decision/registry-updater.js +23 -14
  130. package/src/extensions/loader.js +2 -8
  131. package/src/gates/g1-planning-complete.js +2 -1
  132. package/src/gates/g2-qa-planning.js +2 -1
  133. package/src/gates/g3-implementation.js +2 -1
  134. package/src/gates/g4-qa-implementation.js +3 -2
  135. package/src/gates/g5-deploy-ready.js +2 -1
  136. package/src/health/engine.js +4 -3
  137. package/src/installer/core.js +422 -81
  138. package/src/installer/preflight.js +131 -0
  139. package/src/installer/provider-overlay.js +3 -3
  140. package/src/installer/scaffold-applier.js +358 -0
  141. package/src/installer/templates.js +46 -29
  142. package/src/installer/validator.js +17 -12
  143. package/src/intelligence/registry-manager.js +22 -29
  144. package/src/intelligence/timeline.js +11 -6
  145. package/src/license/commands.js +1 -1
  146. package/src/license/wait.js +102 -0
  147. package/src/memory/agent-memory.js +81 -0
  148. package/src/memory/dream.js +32 -1
  149. package/src/merger/replace-merger.js +28 -15
  150. package/src/orchestrator/agent-selector.js +2 -1
  151. package/src/orchestrator/cli.js +1869 -71
  152. package/src/orchestrator/doctor.js +270 -0
  153. package/src/orchestrator/handoff-engine.js +4 -3
  154. package/src/orchestrator/index.js +2 -0
  155. package/src/orchestrator/pipeline-manager.js +306 -15
  156. package/src/orchestrator/session-manager.js +331 -6
  157. package/src/tasks/handoff.js +3 -2
  158. package/src/telemetry/config.js +4 -3
  159. package/src/telemetry/schema.js +1 -0
  160. package/src/terminal/collector.js +3 -2
  161. package/src/terminal/index.js +1 -2
  162. package/src/terminal/isolation.js +52 -18
  163. package/src/terminal/prompt-builder.js +42 -25
  164. package/src/terminal/run-parallel.js +1 -1
  165. package/src/terminal/run-team.js +3 -3
  166. package/src/terminal/team-task-list.js +43 -4
  167. package/src/upgrade/backup.js +3 -2
  168. package/src/upgrade/checker.js +3 -2
  169. package/src/upgrade/migrator.js +65 -7
  170. package/src/upgrade/tracked-files-detector.js +86 -0
  171. package/src/upgrade/user-messages.js +94 -0
  172. package/src/utils/config-parser.js +2 -1
  173. package/src/utils/feature-flags.js +2 -1
  174. package/src/utils/flatten-entities.js +69 -0
  175. package/src/utils/framework-dir.js +16 -0
  176. package/src/utils/model-id.js +85 -0
  177. package/src/utils/provider-limits.js +84 -23
  178. package/src/utils/schema-validator.js +1 -1
  179. package/src/wizard/i18n.js +5 -4
  180. package/src/wizard/index.js +14 -0
  181. package/assets/logo - c/303/263pia.png +0 -0
  182. package/assets/logo.svg +0 -42
  183. package/assets/logo2.png +0 -0
  184. package/assets/social-preview.png +0 -0
  185. package/scripts/bundle-framework.js +0 -69
  186. package/scripts/changelog-generator.js +0 -222
  187. package/scripts/codebase-mapper.js +0 -728
  188. package/scripts/commit-message-generator.js +0 -167
  189. package/scripts/coverage-analyzer.js +0 -260
  190. package/scripts/dependency-analyzer.js +0 -280
  191. package/scripts/doctor/checks/agents.js +0 -77
  192. package/scripts/doctor/checks/constitution.js +0 -41
  193. package/scripts/doctor/checks/domain-alignment.js +0 -58
  194. package/scripts/doctor/checks/prism-layers.js +0 -84
  195. package/scripts/doctor/checks/registry.js +0 -55
  196. package/scripts/doctor/checks/schemas.js +0 -61
  197. package/scripts/doctor/fixes/reference-fix.js +0 -100
  198. package/scripts/doctor/fixes/registry-fix.js +0 -56
  199. package/scripts/doctor/index.js +0 -212
  200. package/scripts/framework-analyzer.js +0 -308
  201. package/scripts/generate-constitution-domain.js +0 -253
  202. package/scripts/generate-signing-key.js +0 -33
  203. package/scripts/health-check.js +0 -481
  204. package/scripts/ide-sync.js +0 -326
  205. package/scripts/performance-analyzer.js +0 -325
  206. package/scripts/plan-tracker.js +0 -278
  207. package/scripts/populate-entity-registry.js +0 -481
  208. package/scripts/pr-review.js +0 -317
  209. package/scripts/rollback-manager.js +0 -310
  210. package/scripts/semantic-lint.js +0 -328
  211. package/scripts/sign-manifest.js +0 -53
  212. package/scripts/stuck-detector.js +0 -343
  213. package/scripts/test-quality-assessment.js +0 -257
  214. package/scripts/validate-agents.js +0 -368
  215. package/scripts/validate-package.js +0 -505
  216. package/scripts/validate-tasks.js +0 -465
  217. package/src/autonomy/worktree-manager.js +0 -250
  218. package/src/intelligence/decision-engine.js +0 -256
  219. package/src/intelligence/document-sharder.js +0 -221
  220. package/src/intelligence/elicitation.js +0 -265
@@ -0,0 +1,112 @@
1
+ /**
2
+ * lib/animations/gsap.ts — scaffold motion-premium v1.0.0
3
+ *
4
+ * Module-level GSAP setup: plugin registration, Lenis integration, reduced-motion
5
+ * detection. The non-obvious choices here are the difference between "premium"
6
+ * and "broken under real-world conditions" — do not remove without reading why.
7
+ *
8
+ * Non-obvious choices:
9
+ *
10
+ * - `gsap.registerPlugin(ScrollTrigger)` at MODULE level (not inside a hook or
11
+ * component) — this is called exactly once per app load, regardless of HMR
12
+ * reloads or strict-mode double-mounts. Registering inside a component
13
+ * causes a silent warning and then silent failure as GSAP rejects the second
14
+ * registration.
15
+ *
16
+ * - `gsap.ticker.lagSmoothing(0)` — SAUCE PATTERN #1. By default, when the tab
17
+ * loses focus (or the browser throttles a background tab), `requestAnimationFrame`
18
+ * stops firing. GSAP compensates by "smoothing" the accumulated lag the next
19
+ * time RAF fires, time-scaling animations to catch up. For scroll-driven
20
+ * animations this presents as a snap: the page jerks forward as GSAP races to
21
+ * "catch up" scroll-linked progress values. Disabling smoothing keeps
22
+ * progress tied strictly to real scroll position, no catch-up.
23
+ *
24
+ * - Single RAF loop — SAUCE PATTERN #5. Lenis has its own RAF and GSAP has its
25
+ * own ticker; running both causes subtle drift (their clocks diverge over
26
+ * minutes). Hooking Lenis into `gsap.ticker.add` makes GSAP the authoritative
27
+ * clock, Lenis a consumer. Zero drift, one RAF, one heartbeat.
28
+ *
29
+ * - `lenis.raf(time * 1000)` — GSAP ticker passes time in SECONDS, Lenis.raf
30
+ * expects MILLISECONDS. Forgetting this multiplication produces smooth-scroll
31
+ * that runs 1000x slower than expected.
32
+ *
33
+ * - `window.scrollTo(0, 0)` + `lenis.scrollTo(0, immediate: true)` on init —
34
+ * defends against browser's default scrollRestoration on back/forward
35
+ * navigation. Combined with `history.scrollRestoration = 'manual'` in
36
+ * layout.tsx (phase 3), guarantees the page opens at top.
37
+ *
38
+ * - `ScrollTrigger.refresh()` AFTER Lenis init — required because ScrollTrigger
39
+ * measures scroll positions using the native scrollbar by default; once Lenis
40
+ * takes over, all positions must be re-measured against the virtualized
41
+ * scroll surface.
42
+ *
43
+ * - `(window as unknown as {...}).lenis = lenis` — exposes the Lenis/GSAP
44
+ * instances on the global window. Used by the chati-dev visual-qa.js script
45
+ * to call `window.lenis.scrollTo()` during capture runs. Also useful for
46
+ * in-browser debugging. DO NOT remove — regression surface.
47
+ */
48
+
49
+ 'use client';
50
+
51
+ import { gsap } from 'gsap';
52
+ import { ScrollTrigger } from 'gsap/ScrollTrigger';
53
+
54
+ // Register plugins once at module level (see header: registration reason).
55
+ gsap.registerPlugin(ScrollTrigger);
56
+
57
+ export { gsap, ScrollTrigger };
58
+
59
+ /**
60
+ * Initialize Lenis smooth scroll and connect it to GSAP + ScrollTrigger.
61
+ * Call this ONCE per app lifecycle from `useSmoothScroll` (LenisProvider).
62
+ * Returns the lenis instance for explicit teardown on unmount.
63
+ */
64
+ export async function initLenis() {
65
+ const { default: Lenis } = await import('lenis');
66
+
67
+ const lenis = new Lenis({
68
+ duration: 1.2,
69
+ easing: (t: number) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
70
+ smoothWheel: true,
71
+ });
72
+
73
+ if (typeof window !== 'undefined') {
74
+ window.scrollTo(0, 0);
75
+ lenis.scrollTo(0, { immediate: true });
76
+ }
77
+
78
+ lenis.on('scroll', ScrollTrigger.update);
79
+
80
+ // Single RAF loop — GSAP is the authoritative clock; Lenis consumes it.
81
+ gsap.ticker.add((time) => {
82
+ lenis.raf(time * 1000);
83
+ });
84
+
85
+ // Sauce pattern #1: eliminates tab-restore snap for scroll-driven animations.
86
+ gsap.ticker.lagSmoothing(0);
87
+
88
+ // Refresh ScrollTrigger AFTER Lenis is live so scroll positions are measured
89
+ // against the virtualized surface, not the native scrollbar.
90
+ ScrollTrigger.refresh();
91
+
92
+ // Expose globally for QA scripts (visual-qa.js reads window.lenis for capture).
93
+ if (typeof window !== 'undefined') {
94
+ (window as unknown as { lenis: typeof lenis }).lenis = lenis;
95
+ (window as unknown as { gsap: typeof gsap }).gsap = gsap;
96
+ (window as unknown as { ScrollTrigger: typeof ScrollTrigger }).ScrollTrigger = ScrollTrigger;
97
+ }
98
+
99
+ return lenis;
100
+ }
101
+
102
+ /**
103
+ * Check if the user has requested reduced motion.
104
+ * SSR-safe: returns false during server render (no `window`).
105
+ *
106
+ * Consumers: useGsapContext (early-return), every animated component that
107
+ * needs a reduced-motion branch.
108
+ */
109
+ export function prefersReducedMotion(): boolean {
110
+ if (typeof window === 'undefined') return false;
111
+ return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
112
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * lib/animations/refreshCoordinator.ts — scaffold motion-premium v1.0.0
3
+ *
4
+ * Debounced singleton for ScrollTrigger.refresh(). Coalesces N simultaneous
5
+ * refresh requests from N components mounting together into ONE refresh at
6
+ * the end of the debounce window.
7
+ *
8
+ * Non-obvious choices:
9
+ *
10
+ * - SAUCE PATTERN #9. Multiple components requesting refresh on mount is
11
+ * common: image loaded → want refresh, font loaded → want refresh, video
12
+ * ready → want refresh. Each raw `ScrollTrigger.refresh()` call iterates
13
+ * all registered triggers and recomputes their start/end positions —
14
+ * expensive, and visible as a layout jitter. Debouncing means the browser
15
+ * sees ONE recompute for the burst instead of N.
16
+ *
17
+ * - `DEBOUNCE_MS = 200` — long enough to coalesce typical "everything mounted
18
+ * this frame" bursts (which settle in under 100ms on fast hardware, up to
19
+ * 200ms on slow), short enough that a user will not perceive the delay
20
+ * before layout correctness kicks in.
21
+ *
22
+ * - Module-level `_timer` singleton — the whole point is cross-component
23
+ * coordination. A hook-level timer wouldn't coalesce across siblings.
24
+ *
25
+ * - Dynamic `import('gsap/ScrollTrigger')` inside the debounced fn —
26
+ * keeps this module importable from SSR paths without pulling GSAP into
27
+ * the server bundle. The debounced fn only ever runs client-side (after
28
+ * the `typeof window === 'undefined'` guard in `requestRefresh`).
29
+ *
30
+ * - `cancelPendingRefresh` helper — exposed so page-transition handlers can
31
+ * abort a pending refresh that would otherwise fire during an unmount
32
+ * animation (the refresh rips triggers away that the exit animation is
33
+ * still using). Most callers don't need this; it's a surgical tool.
34
+ */
35
+
36
+ export const DEBOUNCE_MS = 200;
37
+
38
+ let _timer: ReturnType<typeof setTimeout> | null = null;
39
+
40
+ /**
41
+ * Request a ScrollTrigger.refresh(). Multiple calls within DEBOUNCE_MS
42
+ * coalesce into a single refresh at the end of the debounce window.
43
+ *
44
+ * Safe to call from SSR context: no-ops when window is undefined.
45
+ */
46
+ export function requestRefresh(): void {
47
+ if (typeof window === 'undefined') return;
48
+
49
+ // Cancel any pending refresh
50
+ if (_timer !== null) {
51
+ clearTimeout(_timer);
52
+ _timer = null;
53
+ }
54
+
55
+ // Schedule a new refresh
56
+ _timer = setTimeout(async () => {
57
+ _timer = null;
58
+ const { ScrollTrigger } = await import('gsap/ScrollTrigger');
59
+ ScrollTrigger.refresh();
60
+ }, DEBOUNCE_MS);
61
+ }
62
+
63
+ /**
64
+ * Cancel any pending refresh. Use before a page transition / unmount that
65
+ * should not be perturbed by a trailing refresh.
66
+ *
67
+ * Usually not needed — the refresh is harmless after unmount. Reach for
68
+ * this when you see a visible jump during a page transition.
69
+ */
70
+ export function cancelPendingRefresh(): void {
71
+ if (_timer !== null) {
72
+ clearTimeout(_timer);
73
+ _timer = null;
74
+ }
75
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * lib/animations/tokens.ts — scaffold motion-premium v1.0.0
3
+ *
4
+ * Animation constants: easings, durations, staggers, ScrollTrigger defaults.
5
+ * Mirrors the CSS custom properties declared in app/globals.css.
6
+ *
7
+ * Why this file exists separately from globals.css:
8
+ * - GSAP accepts easing as a `cubic-bezier(...)` string OR a function;
9
+ * Framer Motion expects a 4-number array. Having both shapes here prevents
10
+ * every component from re-deriving the same curve math.
11
+ * - GSAP uses seconds for duration; CSS uses milliseconds. Converting at
12
+ * the token boundary is cleaner than at every call site.
13
+ * - Staggers have no CSS equivalent — they live purely in the JS animation
14
+ * API, so the TS file is their canonical home.
15
+ *
16
+ * Override classification: `partial` — adjust values (e.g. change `cinematic`
17
+ * from 1.4s to 1.2s), but keep the exported object shape so downstream
18
+ * templates (useGsapContext consumers, providers) remain compatible.
19
+ */
20
+
21
+ // -----------------------------------------------------------------------
22
+ // EASINGS — string form for GSAP + CSS
23
+ // Mirror of --ease-* custom properties declared in app/globals.css.
24
+ // When values change in globals.css, mirror here to avoid drift.
25
+ // -----------------------------------------------------------------------
26
+ export const EASINGS = {
27
+ /** --ease-default: cubic-bezier(0.25, 0.1, 0.25, 1) */
28
+ default: "cubic-bezier(0.25, 0.1, 0.25, 1)",
29
+ /** --ease-in: cubic-bezier(0.42, 0, 1, 1) */
30
+ in: "cubic-bezier(0.42, 0, 1, 1)",
31
+ /** --ease-out: cubic-bezier(0, 0, 0.58, 1) */
32
+ out: "cubic-bezier(0, 0, 0.58, 1)",
33
+ /** --ease-in-out: cubic-bezier(0.42, 0, 0.58, 1) */
34
+ inOut: "cubic-bezier(0.42, 0, 0.58, 1)",
35
+ /** --ease-spring: cubic-bezier(0.175, 0.885, 0.32, 1.275) */
36
+ spring: "cubic-bezier(0.175, 0.885, 0.32, 1.275)",
37
+ /** --ease-cinematic: cubic-bezier(0.16, 1, 0.3, 1) */
38
+ cinematic: "cubic-bezier(0.16, 1, 0.3, 1)",
39
+ /** --ease-gentle: cubic-bezier(0.4, 0, 0.2, 1) */
40
+ gentle: "cubic-bezier(0.4, 0, 0.2, 1)",
41
+ } as const;
42
+
43
+ export type EasingKey = keyof typeof EASINGS;
44
+
45
+ // -----------------------------------------------------------------------
46
+ // EASING_CURVES — array form for Framer Motion
47
+ // Same curves as EASINGS but expressed as 4-number tuples. Framer Motion's
48
+ // `ease` prop rejects `cubic-bezier(...)` strings, so this parallel shape
49
+ // is required — do not try to collapse into EASINGS.
50
+ // -----------------------------------------------------------------------
51
+ export const EASING_CURVES: Record<EasingKey, [number, number, number, number]> = {
52
+ default: [0.25, 0.1, 0.25, 1],
53
+ in: [0.42, 0, 1, 1],
54
+ out: [0, 0, 0.58, 1],
55
+ inOut: [0.42, 0, 0.58, 1],
56
+ spring: [0.175, 0.885, 0.32, 1.275],
57
+ cinematic: [0.16, 1, 0.3, 1],
58
+ gentle: [0.4, 0, 0.2, 1],
59
+ };
60
+
61
+ // -----------------------------------------------------------------------
62
+ // DURATIONS — seconds (GSAP convention)
63
+ // The CSS side is in milliseconds; the values here are the same intervals
64
+ // converted at the token boundary. Reference mapping in doc comments.
65
+ // -----------------------------------------------------------------------
66
+ export const DURATIONS = {
67
+ /** --duration-instant: 0ms -> 0s (no-op / CSS-only transitions) */
68
+ instant: 0,
69
+ /** --duration-fast: 150ms -> 0.15s (hover states, microinteractions) */
70
+ fast: 0.15,
71
+ /** --duration-normal: 300ms -> 0.3s (default UI transitions) */
72
+ normal: 0.3,
73
+ /** --duration-slow: 500ms -> 0.5s (panel reveals, accordion) */
74
+ slow: 0.5,
75
+ /** --duration-dramatic: 900ms -> 0.9s (hero entrance, section reveals) */
76
+ dramatic: 0.9,
77
+ /** --duration-cinematic: 1400ms -> 1.4s (long pin scrubs, crossfades) */
78
+ cinematic: 1.4,
79
+ } as const;
80
+
81
+ export type DurationKey = keyof typeof DURATIONS;
82
+
83
+ // -----------------------------------------------------------------------
84
+ // STAGGERS — inter-element delays (seconds)
85
+ // No CSS equivalent — staggers are an animation-API concept. These are the
86
+ // TypeScript source of truth; there is no globals.css mirror.
87
+ // -----------------------------------------------------------------------
88
+ export const STAGGERS = {
89
+ /** Fine-grained character or item stagger (split-text reveals) */
90
+ tight: 0.03,
91
+ /** Default stagger for lists, cards, grids */
92
+ default: 0.05,
93
+ /** Relaxed stagger for section-level reveals */
94
+ relaxed: 0.1,
95
+ /** Dramatic entrance stagger for hero copy */
96
+ hero: 0.15,
97
+ } as const;
98
+
99
+ export type StaggerKey = keyof typeof STAGGERS;
100
+
101
+ // -----------------------------------------------------------------------
102
+ // SCROLL_TRIGGER_DEFAULTS — common trigger start/end strings
103
+ // The GSAP "top 80%" syntax means: "fire when element's top is at 80% down
104
+ // the viewport". Defaults here encode the weight/timing recommendations
105
+ // from the reference implementation (focus-ai-website) — they produce the
106
+ // signature feel of the premium pattern set.
107
+ // -----------------------------------------------------------------------
108
+ export const SCROLL_TRIGGER_DEFAULTS = {
109
+ /** Element enters viewport at 80% from top — standard reveal threshold */
110
+ standardReveal: "top 80%",
111
+ /** Element enters viewport at 90% from top — early reveal for large blocks */
112
+ earlyReveal: "top 90%",
113
+ /** Element enters viewport at 70% from top — late reveal for emphasis */
114
+ lateReveal: "top 70%",
115
+ /** Scrub scene start position (pinned panels) */
116
+ sceneStart: "top top",
117
+ /** Scrub scene end (500% viewport height travel — typical pin duration) */
118
+ sceneEnd: "+=500%",
119
+ } as const;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * lib/animations/useGsapContext.ts — scaffold motion-premium v1.0.0
3
+ *
4
+ * Scoped GSAP animations with automatic cleanup and reduced-motion safety.
5
+ * This is the primary hook every animated component should use.
6
+ *
7
+ * Non-obvious choices:
8
+ *
9
+ * - `gsap.context(fn, containerRef)` — SAUCE PATTERN #3. The second argument
10
+ * SCOPES all selector strings inside `fn` to descendants of containerRef.
11
+ * Calling `gsap.fromTo('.title', ...)` inside the context ONLY matches
12
+ * `.title` elements within this component, not globally. Without scope, a
13
+ * second mount of the same component (or any sibling with .title) would
14
+ * double-animate. Forget this and the bug appears as "animations sometimes
15
+ * jitter or fire twice on some pages" — hell to debug.
16
+ *
17
+ * - `ctx.revert()` on unmount — kills every tween, timeline, and ScrollTrigger
18
+ * created inside the context. Without this, ScrollTriggers leak: they stay
19
+ * registered against the scroll position, firing on pages that don't contain
20
+ * the originating component. Memory cost grows with navigation; worse, the
21
+ * leaked triggers intercept wheel events and break Lenis.
22
+ *
23
+ * - Reduced-motion early-return with `clearProps` — SAUCE PATTERN #11 tie-in.
24
+ * When prefers-reduced-motion is on, we DO NOT run the setup fn (would
25
+ * leave initial state like `opacity: 0` stuck). Instead we forcibly show
26
+ * all [data-gsap-target] descendants at their final state. `clearProps:
27
+ * 'transform,opacity'` strips any prior GSAP overrides so CSS defaults take
28
+ * effect. Components must tag their animated elements with
29
+ * `data-gsap-target` for this fallback to find them.
30
+ *
31
+ * - `deps` param — passed to useEffect. Default empty array = run once,
32
+ * consistent with how most component animations want it. Pass a non-empty
33
+ * array only if the animation genuinely depends on state that can change.
34
+ *
35
+ * - `RefObject<HTMLElement | null>` — the `| null` matches React 19's
36
+ * stricter ref typing where `useRef<HTMLDivElement>(null)` returns a
37
+ * nullable ref. Removing `| null` forces every caller to cast.
38
+ */
39
+
40
+ 'use client';
41
+
42
+ import { useEffect, useRef } from 'react';
43
+ import { gsap, prefersReducedMotion } from './gsap';
44
+
45
+ /**
46
+ * Run a GSAP setup inside a scoped context tied to a container ref.
47
+ * Automatically calls `ctx.revert()` on unmount for leak-free teardown.
48
+ *
49
+ * Usage:
50
+ * const containerRef = useRef<HTMLDivElement>(null);
51
+ * useGsapContext(containerRef, (ctx) => {
52
+ * gsap.fromTo('.title', { opacity: 0 }, { opacity: 1 });
53
+ * // Register ScrollTriggers, timelines, etc. They are all scoped.
54
+ * });
55
+ *
56
+ * If reduced-motion is active, `setupFn` is NOT called. Instead, all
57
+ * `[data-gsap-target]` descendants are set to the visible final state.
58
+ * Mark every animated element with `data-gsap-target` so this fallback
59
+ * can find it.
60
+ */
61
+ export function useGsapContext(
62
+ containerRef: React.RefObject<HTMLElement | null>,
63
+ setupFn: (context: gsap.Context) => void,
64
+ deps: React.DependencyList = []
65
+ ) {
66
+ const ctxRef = useRef<gsap.Context | null>(null);
67
+
68
+ useEffect(() => {
69
+ if (!containerRef.current) return;
70
+
71
+ // Reduced-motion safeguard: skip animations, show targets at final state.
72
+ if (prefersReducedMotion()) {
73
+ const targets = containerRef.current.querySelectorAll('[data-gsap-target]');
74
+ if (targets.length > 0) {
75
+ gsap.set(targets, { opacity: 1, y: 0, clearProps: 'transform,opacity' });
76
+ }
77
+ return;
78
+ }
79
+
80
+ // Scoped context — all selectors inside setupFn match only this subtree.
81
+ ctxRef.current = gsap.context((ctx) => {
82
+ setupFn(ctx);
83
+ }, containerRef);
84
+
85
+ // Cleanup: kill every tween / timeline / ScrollTrigger created above.
86
+ return () => {
87
+ ctxRef.current?.revert();
88
+ ctxRef.current = null;
89
+ };
90
+ // eslint-disable-next-line react-hooks/exhaustive-deps
91
+ }, deps);
92
+ }
@@ -0,0 +1,265 @@
1
+ /**
2
+ * lib/animations/useScrollSnapStepper.ts — scaffold motion-premium v1.0.0
3
+ *
4
+ * Step-snap navigation for horizontally-pinned ScrollTrigger sequences.
5
+ * One wheel tick = one panel advance. Used by multi-stage pins (method
6
+ * sections, editorial lists) where free scrub would miss panels entirely.
7
+ *
8
+ * The function is named `setupScrollSnapStepper` (not a hook) so callers
9
+ * can invoke it from inside `useGsapContext` setup fns without nested hooks.
10
+ *
11
+ * Non-obvious choices:
12
+ *
13
+ * - `normalizeWheelDelta` — WheelEvent.deltaY values differ by deltaMode:
14
+ * 0 = pixels (trackpad, default), 1 = lines (mouse wheel, multiply by 16),
15
+ * 2 = pages (rare, multiply by 800). Without normalization, a mouse wheel
16
+ * tick (deltaY=1 in mode 1) looks smaller than a trackpad flick (deltaY=50
17
+ * in mode 0) and the threshold logic misbehaves.
18
+ *
19
+ * - `wheelAccumulator` with 500ms reset — SAUCE PATTERN #13. A trackpad
20
+ * momentum scroll fires dozens of small deltaY events. Each alone is
21
+ * sub-threshold, but their sum represents clear intent. The accumulator
22
+ * sums deltas; the 500ms reset clears stale totals so a slow continuous
23
+ * scroll doesn't eventually cross the threshold by itself.
24
+ *
25
+ * - `WHEEL_THRESHOLD = 50` — chosen empirically. Low enough that a single
26
+ * deliberate swipe crosses it; high enough that drift/noise doesn't.
27
+ *
28
+ * - `entryCooldownUntil` — SAUCE PATTERN #12. 600ms cooldown after the pin
29
+ * section scrolls INTO view. Without it, the momentum of the entry scroll
30
+ * triggers an immediate first-panel snap — user perceives "I just got here,
31
+ * why is it already moving?" Cooldown owner (caller) sets this via
32
+ * stepState.entryCooldownUntil = performance.now() + 600 on pin start.
33
+ *
34
+ * - `lenis.stop()` / `lenis.start()` around programmatic scroll — Lenis's
35
+ * smoothWheel listener fights user-initiated scrolls that are actually
36
+ * our own programmatic scrollTo. Stopping Lenis prevents the fight; the
37
+ * 150ms post-complete delay before `start()` absorbs any residual input.
38
+ *
39
+ * - `lock: true` on lenis.scrollTo — forbids other scroll inputs until our
40
+ * programmatic motion completes. Without it, a rapid second wheel tick
41
+ * during a snap causes a compound motion that overshoots by 2 panels.
42
+ *
43
+ * - `capture: true` listeners — React's synthetic event system sits downstream
44
+ * of native capture-phase listeners. By listening in capture we intercept
45
+ * wheel/touch BEFORE React handlers (which may preventDefault on us or
46
+ * trigger layout scroll that makes Lenis fight).
47
+ *
48
+ * - `passive: false` on wheel + touchmove — required for preventDefault to
49
+ * work. Chrome warns (correctly) that passive listeners are faster, but we
50
+ * need the ability to cancel the scroll during isBlocked() windows.
51
+ */
52
+
53
+ 'use client';
54
+
55
+ import { ScrollTrigger } from './gsap';
56
+
57
+ /**
58
+ * Minimal Lenis surface we depend on. Kept narrow so the stepper can work
59
+ * with any Lenis-like scroll driver (or a shim in tests).
60
+ */
61
+ export interface LenisLike {
62
+ stop: () => void;
63
+ start: () => void;
64
+ scrollTo: (
65
+ v: number,
66
+ o?: {
67
+ duration?: number;
68
+ easing?: (t: number) => number;
69
+ force?: boolean;
70
+ onComplete?: () => void;
71
+ lock?: boolean;
72
+ },
73
+ ) => void;
74
+ }
75
+
76
+ /**
77
+ * Retrieve the globally-exposed Lenis instance (see gsap.ts initLenis).
78
+ * Returns undefined during SSR or before LenisProvider has mounted.
79
+ */
80
+ export function getLenis(): LenisLike | undefined {
81
+ if (typeof window === 'undefined') return undefined;
82
+ const w = window as unknown as { lenis?: LenisLike };
83
+ return w.lenis;
84
+ }
85
+
86
+ /**
87
+ * Mutable state the stepper threads between invocations. Owners typically
88
+ * allocate one per pinned ScrollTrigger instance.
89
+ */
90
+ export interface StepState {
91
+ isSnapping: boolean;
92
+ lastScrollTs: number;
93
+ entryCooldownUntil: number;
94
+ }
95
+
96
+ export interface StepByStepConfig {
97
+ /** ScrollTrigger id that this stepper should advance. */
98
+ triggerId: string;
99
+ /** Number of discrete stages (panels) within the scrubbed range. */
100
+ stageCount: number;
101
+ /** Mutable step state (snap in progress, cooldowns, last wheel ts). */
102
+ stepState: StepState;
103
+ }
104
+
105
+ /**
106
+ * Attach wheel + touch listeners that advance a scrubbed ScrollTrigger one
107
+ * panel at a time. Returns a cleanup function that removes the listeners;
108
+ * always pair with useEffect or gsap.context cleanup.
109
+ *
110
+ * Typical call site:
111
+ * useGsapContext(sectionRef, () => {
112
+ * const stepState = { isSnapping: false, lastScrollTs: 0, entryCooldownUntil: 0 };
113
+ * ScrollTrigger.create({ id: 'method', trigger: ..., pin: true, ... });
114
+ * const cleanup = setupScrollSnapStepper({
115
+ * triggerId: 'method', stageCount: 5, stepState,
116
+ * });
117
+ * return cleanup; // gsap.context.revert() calls this for free if returned
118
+ * });
119
+ */
120
+ export function setupScrollSnapStepper({
121
+ triggerId,
122
+ stageCount,
123
+ stepState,
124
+ }: StepByStepConfig): () => void {
125
+ const stageStep = 1 / (stageCount - 1);
126
+ let touchStartY = 0;
127
+
128
+ const advance = (direction: number): boolean => {
129
+ if (stepState.isSnapping) return true;
130
+ const st = ScrollTrigger.getById(triggerId);
131
+ if (!st || !st.isActive) return false;
132
+
133
+ const currentStep = Math.round(st.progress / stageStep);
134
+ const nextStep = currentStep + direction;
135
+
136
+ if (nextStep < 0 || nextStep > stageCount - 1) return false;
137
+
138
+ stepState.isSnapping = true;
139
+ const lenis = getLenis();
140
+ if (lenis) lenis.stop();
141
+
142
+ const targetProgress = nextStep * stageStep;
143
+ const targetScroll = st.start + (st.end - st.start) * targetProgress;
144
+
145
+ const finish = () => {
146
+ window.setTimeout(() => {
147
+ stepState.isSnapping = false;
148
+ stepState.lastScrollTs = performance.now();
149
+ if (lenis) lenis.start();
150
+ }, 150);
151
+ };
152
+
153
+ if (lenis) {
154
+ lenis.scrollTo(targetScroll, {
155
+ duration: 0.9,
156
+ easing: (t: number) => 1 - Math.pow(1 - t, 3),
157
+ force: true,
158
+ lock: true,
159
+ onComplete: finish,
160
+ });
161
+ window.setTimeout(finish, 1100);
162
+ } else {
163
+ window.scrollTo({ top: targetScroll, behavior: 'smooth' });
164
+ window.setTimeout(finish, 900);
165
+ }
166
+ return true;
167
+ };
168
+
169
+ const isBlocked = () => {
170
+ const now = performance.now();
171
+ return (
172
+ stepState.isSnapping ||
173
+ now < stepState.entryCooldownUntil ||
174
+ now - stepState.lastScrollTs < 200
175
+ );
176
+ };
177
+
178
+ // Normalize WheelEvent deltaY across deltaMode values:
179
+ // 0 = pixels (pass-through), 1 = lines (*16), 2 = pages (*800).
180
+ const normalizeWheelDelta = (e: WheelEvent): number => {
181
+ if (e.deltaMode === 1) return e.deltaY * 16;
182
+ if (e.deltaMode === 2) return e.deltaY * 800;
183
+ return e.deltaY;
184
+ };
185
+
186
+ let wheelAccumulator = 0;
187
+ let wheelResetTimer: ReturnType<typeof setTimeout> | null = null;
188
+ const WHEEL_THRESHOLD = 50;
189
+
190
+ const onWheel = (e: WheelEvent) => {
191
+ const st = ScrollTrigger.getById(triggerId);
192
+ if (!st || !st.isActive) return;
193
+
194
+ const normalized = normalizeWheelDelta(e);
195
+ if (Math.abs(normalized) < 1) return;
196
+
197
+ if (isBlocked()) {
198
+ e.preventDefault();
199
+ e.stopPropagation();
200
+ return;
201
+ }
202
+
203
+ // Accumulate normalized delta; reset after 500ms of no events.
204
+ wheelAccumulator += normalized;
205
+ if (wheelResetTimer !== null) clearTimeout(wheelResetTimer);
206
+ wheelResetTimer = setTimeout(() => {
207
+ wheelAccumulator = 0;
208
+ wheelResetTimer = null;
209
+ }, 500);
210
+
211
+ if (Math.abs(wheelAccumulator) < WHEEL_THRESHOLD) {
212
+ e.preventDefault();
213
+ e.stopPropagation();
214
+ return;
215
+ }
216
+
217
+ // Threshold exceeded: fire step-snap and reset accumulator.
218
+ const direction = wheelAccumulator > 0 ? 1 : -1;
219
+ wheelAccumulator = 0;
220
+ if (wheelResetTimer !== null) {
221
+ clearTimeout(wheelResetTimer);
222
+ wheelResetTimer = null;
223
+ }
224
+
225
+ const handled = advance(direction);
226
+ if (handled) {
227
+ e.preventDefault();
228
+ e.stopPropagation();
229
+ }
230
+ };
231
+
232
+ const onTouchStart = (e: TouchEvent) => {
233
+ touchStartY = e.touches[0].clientY;
234
+ };
235
+
236
+ const onTouchMove = (e: TouchEvent) => {
237
+ const st = ScrollTrigger.getById(triggerId);
238
+ if (!st || !st.isActive) return;
239
+ if (isBlocked()) {
240
+ e.preventDefault();
241
+ e.stopPropagation();
242
+ return;
243
+ }
244
+ // Screen-origin inverted: finger moves DOWN (deltaY > 0 here) = forward scroll.
245
+ const deltaY = touchStartY - e.touches[0].clientY;
246
+ if (Math.abs(deltaY) < 25) return;
247
+ const direction = deltaY > 0 ? 1 : -1;
248
+ const handled = advance(direction);
249
+ if (handled) {
250
+ e.preventDefault();
251
+ e.stopPropagation();
252
+ touchStartY = e.touches[0].clientY;
253
+ }
254
+ };
255
+
256
+ window.addEventListener('wheel', onWheel, { passive: false, capture: true });
257
+ window.addEventListener('touchstart', onTouchStart, { passive: true, capture: true });
258
+ window.addEventListener('touchmove', onTouchMove, { passive: false, capture: true });
259
+
260
+ return () => {
261
+ window.removeEventListener('wheel', onWheel, { capture: true } as EventListenerOptions);
262
+ window.removeEventListener('touchstart', onTouchStart, { capture: true } as EventListenerOptions);
263
+ window.removeEventListener('touchmove', onTouchMove, { capture: true } as EventListenerOptions);
264
+ };
265
+ }