vectorvesper 1.2.0 → 2.0.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.
@@ -0,0 +1,886 @@
1
+ {
2
+ "version": "1.0.0",
3
+ "engineVersion": "0.3.0",
4
+ "generatedAt": "2026-07-26T08:44:11.000Z",
5
+ "contractLevel": "lean",
6
+ "hooks": [
7
+ {
8
+ "contractLevel": "lean",
9
+ "name": "useAdaptiveQuality",
10
+ "importFrom": "@vectorvesper/motion/react",
11
+ "packageName": "@vectorvesper/motion",
12
+ "since": "0.1.0",
13
+ "category": "governor",
14
+ "tier": "free",
15
+ "tagline": "Device capability fused with live frame health — the complete quality signal.",
16
+ "problem": "Measured frame health is the truth but is blind for the first seconds; device signals guess instantly but never learn. Using either alone gets one of those wrong.",
17
+ "summary": "Probes the device once (WebGL2, GPU renderer string, memory, cores, reduced-motion) and fuses that with the live AnimationBudget as `tier = max(deviceTier, budgetTier)`. The device tier is a FLOOR, and that is the whole point: without it, weak devices oscillate forever — degrade, frames recover because you degraded, upgrade, jank, degrade. A software renderer stays a software renderer. `reasons` explains the device verdict.",
18
+ "signature": "useAdaptiveQuality(): AdaptiveState",
19
+ "options": [],
20
+ "returns": [
21
+ {
22
+ "name": "tier",
23
+ "type": "0 | 1 | 2",
24
+ "description": "Effective tier — max(deviceTier, budgetTier). Consume THIS one."
25
+ },
26
+ {
27
+ "name": "label",
28
+ "type": "\"high\" | \"medium\" | \"low\"",
29
+ "description": "Human-readable form of `tier`."
30
+ },
31
+ {
32
+ "name": "deviceTier",
33
+ "type": "0 | 1 | 2",
34
+ "description": "Static floor from the one-time hardware probe."
35
+ },
36
+ {
37
+ "name": "budgetTier",
38
+ "type": "0 | 1 | 2",
39
+ "description": "Live measured tier from AnimationBudget."
40
+ },
41
+ {
42
+ "name": "reasons",
43
+ "type": "string[]",
44
+ "description": "Why the device tier is what it is, e.g. [\"mobile-class GPU\"]. For HUDs and support."
45
+ },
46
+ {
47
+ "name": "reducedMotion",
48
+ "type": "boolean",
49
+ "description": "Whether the user asked for reduced motion."
50
+ }
51
+ ],
52
+ "quickStart": "\"use client\";\nimport { useAdaptiveQuality } from \"@vectorvesper/motion/react\";\n\nexport function Scene() {\n const { tier, reducedMotion } = useAdaptiveQuality();\n if (reducedMotion) return <Poster />;\n return tier === 0 ? <FullScene /> : <LightScene />;\n}",
53
+ "whenNotToUse": [
54
+ {
55
+ "when": "You have no WebGL, canvas or heavy effects on the page.",
56
+ "instead": "Skip it. The device probe creates a throwaway WebGL2 context to read the renderer string, which is not worth paying for on a page of text and CSS transitions."
57
+ },
58
+ {
59
+ "when": "You only need to respect prefers-reduced-motion.",
60
+ "instead": "Use `matchMedia(\"(prefers-reduced-motion: reduce)\")` directly."
61
+ },
62
+ {
63
+ "when": "You want to react to load only, ignoring the device.",
64
+ "instead": "Use `useAnimationBudget`. This hook deliberately clamps to a device floor and will not report tier 0 on a machine it judged weak, no matter how clean the frames are."
65
+ }
66
+ ],
67
+ "docsUrl": "https://vectorvesper.dev/docs/adaptive-quality",
68
+ "labUrl": "https://vectorvesper.dev/lab/adaptive-quality",
69
+ "disclosure": {
70
+ "exposeImplementation": false,
71
+ "reason": "Reproducing this hook's implementation makes the consumer run a private requestAnimationFrame loop instead of joining the shared FrameConductor. That silently removes the coordination, scheduling and frame-budget governance the runtime exists to provide, and nothing errors when it happens. Copying is a correctness bug, not just a licensing question.",
72
+ "correctUsage": "Install @vectorvesper/motion and import the hook. Never inline, reimplement, or paste an equivalent."
73
+ },
74
+ "runtime": {
75
+ "requiresClient": true,
76
+ "ssrSafeImport": true,
77
+ "respectsReducedMotion": true,
78
+ "ownsTransform": false,
79
+ "conflictsWith": [],
80
+ "usesPointer": false,
81
+ "usesScroll": false,
82
+ "usesWebGL": true
83
+ },
84
+ "upgrade": "This is the lean contract: enough to use the hook correctly. The full contract adds real-world recipes with code, accumulated dos and don'ts, scheduling detail for composing several effects on one page, and links to the motion guardrails. Available to Vector Vesper members — authenticate the CLI with `npx vectorvesper login <token>`."
85
+ },
86
+ {
87
+ "contractLevel": "lean",
88
+ "name": "useAnimationBudget",
89
+ "importFrom": "@vectorvesper/motion/react",
90
+ "packageName": "@vectorvesper/motion",
91
+ "since": "0.1.0",
92
+ "category": "governor",
93
+ "tier": "free",
94
+ "tagline": "A live quality tier from measured frame health, so effects can shed themselves.",
95
+ "problem": "A page looks fine on the machine it was built on and janks on the device it ships to, and nothing in the code knows the difference.",
96
+ "summary": "Measures real frame intervals on the shared conductor and collapses them into a coarse tier every effect can consume. Hysteresis is asymmetric on purpose: degrading is fast (users feel jank within a second), recovering takes 8 seconds of clean frames, so quality never flaps. Since v0.3 the thresholds are relative to the measured display refresh rate, so 70fps on a 120Hz panel correctly reads as degraded.",
97
+ "signature": "useAnimationBudget(): BudgetState",
98
+ "options": [],
99
+ "returns": [
100
+ {
101
+ "name": "tier",
102
+ "type": "0 | 1 | 2",
103
+ "description": "0 healthy, 1 shed extras, 2 survival mode. Consume this."
104
+ },
105
+ {
106
+ "name": "label",
107
+ "type": "\"high\" | \"medium\" | \"low\"",
108
+ "description": "Human-readable form of `tier`."
109
+ },
110
+ {
111
+ "name": "avgFrameMs",
112
+ "type": "number",
113
+ "description": "Rolling average frame interval in milliseconds."
114
+ },
115
+ {
116
+ "name": "slowRatio",
117
+ "type": "number",
118
+ "description": "Share of recent frames below the degradation line (~90% of display rate), 0..1."
119
+ },
120
+ {
121
+ "name": "headroom",
122
+ "type": "number",
123
+ "description": "Milliseconds still free in the current frame, smoothed. Negative means already over budget."
124
+ },
125
+ {
126
+ "name": "workMs",
127
+ "type": "number",
128
+ "description": "Milliseconds the motion runtime itself spent last frame. Everything else belongs to the browser, React or third-party scripts."
129
+ },
130
+ {
131
+ "name": "frameBudgetMs",
132
+ "type": "number",
133
+ "description": "One presented frame at the detected display rate."
134
+ }
135
+ ],
136
+ "quickStart": "\"use client\";\nimport { useAnimationBudget } from \"@vectorvesper/motion/react\";\n\nexport function Hero() {\n const { tier } = useAnimationBudget();\n return (\n <section>\n {tier === 0 && <ExpensiveParticleLayer />}\n <HeroContent />\n </section>\n );\n}",
137
+ "whenNotToUse": [
138
+ {
139
+ "when": "You want to gate a one-time expensive mount.",
140
+ "instead": "Use `useSafeToMount`. It answers \"may I mount now?\" once and latches, rather than giving you a value that keeps changing."
141
+ },
142
+ {
143
+ "when": "You need to know about the device before any frames have been measured.",
144
+ "instead": "Use `useAdaptiveQuality`, which fuses a static device probe with this budget. The budget alone is blind for its first ~40 frames."
145
+ },
146
+ {
147
+ "when": "You only need to respect prefers-reduced-motion.",
148
+ "instead": "Use a plain `matchMedia(\"(prefers-reduced-motion: reduce)\")` check. Starting a frame-measurement loop for an accessibility preference is disproportionate."
149
+ }
150
+ ],
151
+ "docsUrl": "https://vectorvesper.dev/docs/animation-budget",
152
+ "labUrl": "https://vectorvesper.dev/lab/animation-budget",
153
+ "disclosure": {
154
+ "exposeImplementation": false,
155
+ "reason": "Reproducing this hook's implementation makes the consumer run a private requestAnimationFrame loop instead of joining the shared FrameConductor. That silently removes the coordination, scheduling and frame-budget governance the runtime exists to provide, and nothing errors when it happens. Copying is a correctness bug, not just a licensing question.",
156
+ "correctUsage": "Install @vectorvesper/motion and import the hook. Never inline, reimplement, or paste an equivalent."
157
+ },
158
+ "runtime": {
159
+ "requiresClient": true,
160
+ "ssrSafeImport": true,
161
+ "respectsReducedMotion": false,
162
+ "ownsTransform": false,
163
+ "conflictsWith": [],
164
+ "usesPointer": false,
165
+ "usesScroll": false,
166
+ "usesWebGL": false
167
+ },
168
+ "upgrade": "This is the lean contract: enough to use the hook correctly. The full contract adds real-world recipes with code, accumulated dos and don'ts, scheduling detail for composing several effects on one page, and links to the motion guardrails. Available to Vector Vesper members — authenticate the CLI with `npx vectorvesper login <token>`."
169
+ },
170
+ {
171
+ "contractLevel": "lean",
172
+ "name": "useImageTrail",
173
+ "importFrom": "@vectorvesper/motion/react",
174
+ "packageName": "@vectorvesper/motion",
175
+ "since": "0.1.0",
176
+ "category": "effect",
177
+ "tier": "free",
178
+ "tagline": "Images spawn along the pointer's path and fade — the award-site gallery flourish.",
179
+ "problem": "The tutorial version creates a DOM node per pointer move and lets the garbage collector deal with it, which is exactly the allocation pattern that produces stutter.",
180
+ "summary": "A fixed pool of `<img>` nodes is created once and recycled. Each flight is animated with the Web Animations API, so there is no rAF loop and nothing is allocated per move — the oldest flight is simply cancelled and reused. Spawns are distance-gated, not event-gated, so the spacing stays even at any pointer speed. Fails open (does nothing) on touch devices and under reduced motion.",
181
+ "signature": "useImageTrail<T>(options: UseImageTrailOptions): { ref }",
182
+ "options": [
183
+ {
184
+ "name": "images",
185
+ "type": "string[]",
186
+ "default": "—",
187
+ "required": true,
188
+ "description": "Image sources, cycled in order. Preload or serve them small; a cold fetch mid-trail shows as a gap."
189
+ },
190
+ {
191
+ "name": "size",
192
+ "type": "number",
193
+ "default": "160",
194
+ "required": false,
195
+ "description": "Rendered width of each trail image in px. Height is automatic."
196
+ },
197
+ {
198
+ "name": "spacing",
199
+ "type": "number",
200
+ "default": "90",
201
+ "required": false,
202
+ "description": "Pointer distance between spawns in px. Lower is denser and more expensive."
203
+ },
204
+ {
205
+ "name": "life",
206
+ "type": "number",
207
+ "default": "900",
208
+ "required": false,
209
+ "description": "Flight duration in ms."
210
+ },
211
+ {
212
+ "name": "maxActive",
213
+ "type": "number",
214
+ "default": "10",
215
+ "required": false,
216
+ "description": "Pool size, i.e. maximum simultaneously visible images. This is a hard ceiling on cost."
217
+ }
218
+ ],
219
+ "returns": [
220
+ {
221
+ "name": "ref",
222
+ "type": "RefObject<T | null>",
223
+ "description": "Attach to the container. The hook sets its position and overflow, and restores both on unmount."
224
+ }
225
+ ],
226
+ "quickStart": "\"use client\";\nimport { useImageTrail } from \"@vectorvesper/motion/react\";\n\nexport function Gallery() {\n const { ref } = useImageTrail<HTMLElement>({\n images: [\"/a.jpg\", \"/b.jpg\", \"/c.jpg\"],\n });\n return <section ref={ref} className=\"relative h-[70vh]\">Move your cursor</section>;\n}",
227
+ "whenNotToUse": [
228
+ {
229
+ "when": "You want a single image that follows the cursor.",
230
+ "instead": "Use `useSensorBus` and write `transform` on one element. A pool and a spawn scheduler for one node is all overhead."
231
+ },
232
+ {
233
+ "when": "The site's primary audience is on mobile.",
234
+ "instead": "Use a scroll-triggered gallery reveal. This hook does nothing without a fine pointer, so mobile users would see an empty hero."
235
+ },
236
+ {
237
+ "when": "The images are content the user needs to see.",
238
+ "instead": "Use a real gallery grid. Trail images are decorative, `aria-hidden`, and visible for under a second each."
239
+ }
240
+ ],
241
+ "docsUrl": "https://vectorvesper.dev/docs/image-trail",
242
+ "labUrl": "https://vectorvesper.dev/lab/image-trail",
243
+ "disclosure": {
244
+ "exposeImplementation": false,
245
+ "reason": "Reproducing this hook's implementation makes the consumer run a private requestAnimationFrame loop instead of joining the shared FrameConductor. That silently removes the coordination, scheduling and frame-budget governance the runtime exists to provide, and nothing errors when it happens. Copying is a correctness bug, not just a licensing question.",
246
+ "correctUsage": "Install @vectorvesper/motion and import the hook. Never inline, reimplement, or paste an equivalent."
247
+ },
248
+ "runtime": {
249
+ "requiresClient": true,
250
+ "ssrSafeImport": true,
251
+ "respectsReducedMotion": true,
252
+ "ownsTransform": false,
253
+ "conflictsWith": [
254
+ "A container with `overflow: visible` required — the hook sets overflow hidden to clip the trail."
255
+ ],
256
+ "usesPointer": true,
257
+ "usesScroll": false,
258
+ "usesWebGL": false
259
+ },
260
+ "upgrade": "This is the lean contract: enough to use the hook correctly. The full contract adds real-world recipes with code, accumulated dos and don'ts, scheduling detail for composing several effects on one page, and links to the motion guardrails. Available to Vector Vesper members — authenticate the CLI with `npx vectorvesper login <token>`."
261
+ },
262
+ {
263
+ "contractLevel": "lean",
264
+ "name": "useLazyScene",
265
+ "importFrom": "@vectorvesper/motion/react",
266
+ "packageName": "@vectorvesper/motion",
267
+ "since": "0.1.0",
268
+ "category": "gate",
269
+ "tier": "free",
270
+ "tagline": "Mount a heavy scene when it's near the viewport, the thread is idle, and frames are healthy.",
271
+ "problem": "A WebGL canvas three screens down initialises during page load, competing with the content the user is actually looking at.",
272
+ "summary": "Three gates in sequence: an IntersectionObserver with a generous rootMargin so the scene is ready just before it is seen; an idle-callback so it doesn't land mid-scroll; and a scroll-velocity check that defers while the user is actively moving. Optionally waits for the frame budget to recover, with a 3-second fail-open so a permanently busy page still eventually renders. Once mounted it stays mounted.",
273
+ "signature": "useLazyScene<T>(options?: UseLazySceneOptions): { ref, ready }",
274
+ "options": [
275
+ {
276
+ "name": "rootMargin",
277
+ "type": "string",
278
+ "default": "\"200px\"",
279
+ "required": false,
280
+ "description": "IntersectionObserver margin. Larger mounts earlier and wastes more when the user never scrolls there."
281
+ },
282
+ {
283
+ "name": "requireIdle",
284
+ "type": "boolean",
285
+ "default": "true",
286
+ "required": false,
287
+ "description": "Wait for requestIdleCallback (1500ms timeout) so mounting doesn't land in a busy frame."
288
+ },
289
+ {
290
+ "name": "deferWhileLow",
291
+ "type": "boolean",
292
+ "default": "false",
293
+ "required": false,
294
+ "description": "Also wait for the AnimationBudget to leave tier 2, capped at 3 seconds so it always fails open."
295
+ }
296
+ ],
297
+ "returns": [
298
+ {
299
+ "name": "ref",
300
+ "type": "RefObject<T | null>",
301
+ "description": "Attach to the placeholder element. It must occupy real space or the observer never fires."
302
+ },
303
+ {
304
+ "name": "ready",
305
+ "type": "boolean",
306
+ "description": "False until every gate passes, then true for the rest of the mount."
307
+ }
308
+ ],
309
+ "quickStart": "\"use client\";\nimport { useLazyScene } from \"@vectorvesper/motion/react\";\n\nexport function SceneSection() {\n const { ref, ready } = useLazyScene<HTMLDivElement>();\n return (\n <div ref={ref} className=\"min-h-[60vh]\">\n {ready ? <HeavyWebGLCanvas /> : <ScenePoster />}\n </div>\n );\n}",
310
+ "whenNotToUse": [
311
+ {
312
+ "when": "The content is above the fold.",
313
+ "instead": "Use `useSafeToMount`, which gates purely on frame headroom. Viewport gating is meaningless for something already on screen."
314
+ },
315
+ {
316
+ "when": "You only need to defer the JavaScript bundle.",
317
+ "instead": "Use `next/dynamic` or `React.lazy` alone. This hook gates the mount, not the download, and adds a runtime for a problem the bundler already solves."
318
+ },
319
+ {
320
+ "when": "You want a simple entrance animation when something scrolls into view.",
321
+ "instead": "Use a bare IntersectionObserver or a CSS scroll-driven animation. This hook is about deferring expensive work, not about revealing things."
322
+ }
323
+ ],
324
+ "docsUrl": "https://vectorvesper.dev/docs/lazy-scene",
325
+ "labUrl": "https://vectorvesper.dev/lab/lazy-scene",
326
+ "disclosure": {
327
+ "exposeImplementation": false,
328
+ "reason": "Reproducing this hook's implementation makes the consumer run a private requestAnimationFrame loop instead of joining the shared FrameConductor. That silently removes the coordination, scheduling and frame-budget governance the runtime exists to provide, and nothing errors when it happens. Copying is a correctness bug, not just a licensing question.",
329
+ "correctUsage": "Install @vectorvesper/motion and import the hook. Never inline, reimplement, or paste an equivalent."
330
+ },
331
+ "runtime": {
332
+ "requiresClient": true,
333
+ "ssrSafeImport": true,
334
+ "respectsReducedMotion": false,
335
+ "ownsTransform": false,
336
+ "conflictsWith": [],
337
+ "usesPointer": false,
338
+ "usesScroll": true,
339
+ "usesWebGL": false
340
+ },
341
+ "upgrade": "This is the lean contract: enough to use the hook correctly. The full contract adds real-world recipes with code, accumulated dos and don'ts, scheduling detail for composing several effects on one page, and links to the motion guardrails. Available to Vector Vesper members — authenticate the CLI with `npx vectorvesper login <token>`."
342
+ },
343
+ {
344
+ "contractLevel": "lean",
345
+ "name": "useMagneticIntent",
346
+ "importFrom": "@vectorvesper/motion/react",
347
+ "packageName": "@vectorvesper/motion",
348
+ "since": "0.1.0",
349
+ "category": "interaction",
350
+ "tier": "free",
351
+ "tagline": "A magnetic button that starts reaching before the cursor gets there.",
352
+ "problem": "Every magnetic-button tutorial reacts to hover, so the element only starts moving once the cursor has already arrived — which is exactly too late to feel alive.",
353
+ "summary": "Engagement is max(PointerIntent confidence, proximity falloff), so the element begins reaching while the cursor is still on its way — the pre-touch that makes award-site buttons feel intentional. Writes only `transform`, and restores the previous inline transform exactly on unmount. Fails open to a perfectly normal element on touch-only devices and under reduced motion.",
354
+ "signature": "useMagneticIntent<T>(options?: MagneticOptions): { ref, active }",
355
+ "options": [
356
+ {
357
+ "name": "strength",
358
+ "type": "number",
359
+ "default": "12",
360
+ "required": false,
361
+ "description": "Maximum translation in px at full engagement."
362
+ },
363
+ {
364
+ "name": "reach",
365
+ "type": "number",
366
+ "default": "90",
367
+ "required": false,
368
+ "description": "Distance from centre, in px, where proximity pull begins."
369
+ },
370
+ {
371
+ "name": "damp",
372
+ "type": "number",
373
+ "default": "12",
374
+ "required": false,
375
+ "description": "Damping responsiveness. Higher tracks tighter, lower feels heavier."
376
+ },
377
+ {
378
+ "name": "scale",
379
+ "type": "number",
380
+ "default": "1.04",
381
+ "required": false,
382
+ "description": "Scale at full engagement. 1 disables scaling."
383
+ },
384
+ {
385
+ "name": "intent",
386
+ "type": "boolean",
387
+ "default": "true",
388
+ "required": false,
389
+ "description": "Use trajectory prediction for pre-attraction. False falls back to proximity only."
390
+ }
391
+ ],
392
+ "returns": [
393
+ {
394
+ "name": "ref",
395
+ "type": "RefObject<T | null>",
396
+ "description": "Attach to the element. The hook owns its inline transform while mounted."
397
+ },
398
+ {
399
+ "name": "active",
400
+ "type": "boolean",
401
+ "description": "Whether the effect actually engaged. False on touch-only devices and under reduced motion."
402
+ }
403
+ ],
404
+ "quickStart": "\"use client\";\nimport { useMagneticIntent } from \"@vectorvesper/motion/react\";\n\nexport function Cta() {\n const { ref } = useMagneticIntent<HTMLButtonElement>();\n return <button ref={ref}>Get started</button>;\n}",
405
+ "whenNotToUse": [
406
+ {
407
+ "when": "You want a simple hover lift or scale.",
408
+ "instead": "Use a CSS transition on transform. It runs on the compositor, costs no JavaScript, and needs no runtime at all."
409
+ },
410
+ {
411
+ "when": "The target is a primary action on a touch-first product.",
412
+ "instead": "Use a plain button with a clear pressed state. This hook is inert without a fine pointer, so on mobile you would be shipping the setup cost for nothing."
413
+ },
414
+ {
415
+ "when": "You need many magnetic elements in one dense grid.",
416
+ "instead": "Use one shared pointer-driven effect over the container. Each instance retains its own PointerIntent, and dozens of independent predictors is the wrong shape."
417
+ }
418
+ ],
419
+ "docsUrl": "https://vectorvesper.dev/docs/magnetic-intent",
420
+ "labUrl": "https://vectorvesper.dev/lab/magnetic-element",
421
+ "disclosure": {
422
+ "exposeImplementation": false,
423
+ "reason": "Reproducing this hook's implementation makes the consumer run a private requestAnimationFrame loop instead of joining the shared FrameConductor. That silently removes the coordination, scheduling and frame-budget governance the runtime exists to provide, and nothing errors when it happens. Copying is a correctness bug, not just a licensing question.",
424
+ "correctUsage": "Install @vectorvesper/motion and import the hook. Never inline, reimplement, or paste an equivalent."
425
+ },
426
+ "runtime": {
427
+ "requiresClient": true,
428
+ "ssrSafeImport": true,
429
+ "respectsReducedMotion": true,
430
+ "ownsTransform": true,
431
+ "conflictsWith": [
432
+ "Anything else writing the same element's transform — framer-motion `animate`, GSAP tweens, Tailwind `hover:scale-*`, CSS transitions on transform.",
433
+ "A parent that also transforms during the same interaction, which compounds the offset."
434
+ ],
435
+ "usesPointer": true,
436
+ "usesScroll": false,
437
+ "usesWebGL": false
438
+ },
439
+ "upgrade": "This is the lean contract: enough to use the hook correctly. The full contract adds real-world recipes with code, accumulated dos and don'ts, scheduling detail for composing several effects on one page, and links to the motion guardrails. Available to Vector Vesper members — authenticate the CLI with `npx vectorvesper login <token>`."
440
+ },
441
+ {
442
+ "contractLevel": "lean",
443
+ "name": "useNumberTicker",
444
+ "importFrom": "@vectorvesper/motion/react",
445
+ "packageName": "@vectorvesper/motion",
446
+ "since": "0.1.0",
447
+ "category": "effect",
448
+ "tier": "free",
449
+ "tagline": "Animate a number to its target without re-rendering anything.",
450
+ "problem": "The obvious implementation re-renders the component sixty times a second to change one string, dragging every sibling through reconciliation for a counter.",
451
+ "summary": "Writes `textContent` directly on the target node from the shared frame loop, so React renders exactly once regardless of how far the number travels. Damping is frame-rate-independent, so it feels identical at 30 and 120fps, and it snaps and stops when close enough rather than easing forever. Enforces tabular figures so digits don't wobble the layout, and jumps straight to the target under reduced motion.",
452
+ "signature": "useNumberTicker<T>(value: number, options?: UseNumberTickerOptions): { ref }",
453
+ "options": [
454
+ {
455
+ "name": "k",
456
+ "type": "number",
457
+ "default": "6",
458
+ "required": false,
459
+ "description": "Damping responsiveness. Higher arrives faster and snappier; 6 is a smooth deceleration."
460
+ },
461
+ {
462
+ "name": "format",
463
+ "type": "Intl.NumberFormatOptions",
464
+ "default": "—",
465
+ "required": false,
466
+ "description": "Standard Intl options — currency, percent, decimal places, compact notation."
467
+ },
468
+ {
469
+ "name": "locale",
470
+ "type": "string",
471
+ "default": "browser locale",
472
+ "required": false,
473
+ "description": "e.g. \"en-US\", \"de-DE\". Defaults to the user's locale."
474
+ },
475
+ {
476
+ "name": "prefix",
477
+ "type": "string",
478
+ "default": "\"\"",
479
+ "required": false,
480
+ "description": "Prepended to the formatted output, e.g. \"+\"."
481
+ },
482
+ {
483
+ "name": "suffix",
484
+ "type": "string",
485
+ "default": "\"\"",
486
+ "required": false,
487
+ "description": "Appended to the formatted output, e.g. \"%\" or \" ms\"."
488
+ }
489
+ ],
490
+ "returns": [
491
+ {
492
+ "name": "ref",
493
+ "type": "RefObject<T | null>",
494
+ "description": "Attach to the element that displays the number. Its text content is owned by the hook."
495
+ }
496
+ ],
497
+ "quickStart": "\"use client\";\nimport { useNumberTicker } from \"@vectorvesper/motion/react\";\n\nexport function Stat({ value }: { value: number }) {\n const { ref } = useNumberTicker<HTMLSpanElement>(value);\n return <span ref={ref} />;\n}",
498
+ "whenNotToUse": [
499
+ {
500
+ "when": "The number changes once and the animation is decorative.",
501
+ "instead": "Render it. A count-up on a static figure adds a frame loop and delays the fact the user came for."
502
+ },
503
+ {
504
+ "when": "You need the animated value in JavaScript, not just on screen.",
505
+ "instead": "Use `damp()` from the core in your own frame callback. This hook writes to the DOM and deliberately never surfaces the intermediate value to React."
506
+ },
507
+ {
508
+ "when": "It's a countdown timer or a clock.",
509
+ "instead": "Use a `setInterval` and render the formatted string. Those need to be exact at each step, and easing toward a time reads as broken."
510
+ }
511
+ ],
512
+ "docsUrl": "https://vectorvesper.dev/docs/number-ticker",
513
+ "labUrl": "https://vectorvesper.dev/lab/number-ticker",
514
+ "disclosure": {
515
+ "exposeImplementation": false,
516
+ "reason": "Reproducing this hook's implementation makes the consumer run a private requestAnimationFrame loop instead of joining the shared FrameConductor. That silently removes the coordination, scheduling and frame-budget governance the runtime exists to provide, and nothing errors when it happens. Copying is a correctness bug, not just a licensing question.",
517
+ "correctUsage": "Install @vectorvesper/motion and import the hook. Never inline, reimplement, or paste an equivalent."
518
+ },
519
+ "runtime": {
520
+ "requiresClient": true,
521
+ "ssrSafeImport": true,
522
+ "respectsReducedMotion": true,
523
+ "ownsTransform": false,
524
+ "conflictsWith": [
525
+ "Rendering children into the same element — the hook overwrites its textContent every frame."
526
+ ],
527
+ "usesPointer": false,
528
+ "usesScroll": false,
529
+ "usesWebGL": false
530
+ },
531
+ "upgrade": "This is the lean contract: enough to use the hook correctly. The full contract adds real-world recipes with code, accumulated dos and don'ts, scheduling detail for composing several effects on one page, and links to the motion guardrails. Available to Vector Vesper members — authenticate the CLI with `npx vectorvesper login <token>`."
532
+ },
533
+ {
534
+ "contractLevel": "lean",
535
+ "name": "usePointerIntent",
536
+ "importFrom": "@vectorvesper/motion/react",
537
+ "packageName": "@vectorvesper/motion",
538
+ "since": "0.1.0",
539
+ "category": "interaction",
540
+ "tier": "free",
541
+ "tagline": "Know the pointer is coming 100–300ms before it arrives, and pre-warm.",
542
+ "problem": "Expensive hover states start their work at the moment of hover, so the first 200ms of every interaction is the user watching something load.",
543
+ "summary": "Casts the pointer's smoothed trajectory forward from the SensorBus and measures time-to-impact against the element's inflated rect. Confidence rises as impact nears; being inside is confidence 1. Enter/exit hysteresis keeps the boolean calm. Use it to start the video, compile the shader or fetch the preview before the cursor lands.",
544
+ "signature": "usePointerIntent<T>(options?: UsePointerIntentOptions): UsePointerIntentReturn<T>",
545
+ "options": [
546
+ {
547
+ "name": "horizon",
548
+ "type": "number",
549
+ "default": "0.5",
550
+ "required": false,
551
+ "description": "Look-ahead in seconds. Larger fires earlier and guesses more."
552
+ },
553
+ {
554
+ "name": "extend",
555
+ "type": "number",
556
+ "default": "12",
557
+ "required": false,
558
+ "description": "Rect inflation in px — how generous the target is."
559
+ },
560
+ {
561
+ "name": "minSpeed",
562
+ "type": "number",
563
+ "default": "80",
564
+ "required": false,
565
+ "description": "Below this pointer speed (px/s) prediction is off. Hover still counts."
566
+ },
567
+ {
568
+ "name": "enter",
569
+ "type": "number",
570
+ "default": "0.35",
571
+ "required": false,
572
+ "description": "Confidence needed to gain intent."
573
+ },
574
+ {
575
+ "name": "exit",
576
+ "type": "number",
577
+ "default": "0.18",
578
+ "required": false,
579
+ "description": "Confidence below which intent is lost. Keep it under `enter`."
580
+ },
581
+ {
582
+ "name": "dynamic",
583
+ "type": "boolean",
584
+ "default": "false",
585
+ "required": false,
586
+ "description": "Re-measure the element every frame while it is being approached. For carousels and animating targets; costs a layout read."
587
+ },
588
+ {
589
+ "name": "onChange",
590
+ "type": "(intent: boolean) => void",
591
+ "default": "—",
592
+ "required": false,
593
+ "description": "Fires on each intent transition, without needing the re-render."
594
+ }
595
+ ],
596
+ "returns": [
597
+ {
598
+ "name": "ref",
599
+ "type": "RefObject<T | null>",
600
+ "description": "Attach to the target element."
601
+ },
602
+ {
603
+ "name": "intent",
604
+ "type": "boolean",
605
+ "description": "Hysteresis-gated. Safe to render on — it changes rarely, not per frame."
606
+ },
607
+ {
608
+ "name": "confidenceRef",
609
+ "type": "RefObject<number>",
610
+ "description": "Smoothed 0..1, updated per frame with no re-render. Read inside frame callbacks."
611
+ }
612
+ ],
613
+ "quickStart": "\"use client\";\nimport { usePointerIntent } from \"@vectorvesper/motion/react\";\n\nexport function PreviewCard() {\n const { ref, intent } = usePointerIntent<HTMLDivElement>();\n return (\n <article ref={ref}>\n {intent && <WarmVideoPreview />} {/* mounts before the cursor arrives */}\n <CardContent />\n </article>\n );\n}",
614
+ "whenNotToUse": [
615
+ {
616
+ "when": "The hover state is cheap — a colour change, an underline, a small transform.",
617
+ "instead": "Use CSS `:hover`. Predicting a repaint costs more than the repaint, and CSS runs off the main thread."
618
+ },
619
+ {
620
+ "when": "You are on a touch device.",
621
+ "instead": "Use `useLazyScene` or intersection-based warming. There is no pointer trajectory to extrapolate from a tap, so prediction has nothing to work with."
622
+ },
623
+ {
624
+ "when": "You need to know the pointer actually entered.",
625
+ "instead": "Use `onMouseEnter`. This hook fires on approach by design and will fire for pointers that pass by without landing."
626
+ }
627
+ ],
628
+ "docsUrl": "https://vectorvesper.dev/docs/pointer-intent",
629
+ "labUrl": "https://vectorvesper.dev/lab/pointer-intent",
630
+ "disclosure": {
631
+ "exposeImplementation": false,
632
+ "reason": "Reproducing this hook's implementation makes the consumer run a private requestAnimationFrame loop instead of joining the shared FrameConductor. That silently removes the coordination, scheduling and frame-budget governance the runtime exists to provide, and nothing errors when it happens. Copying is a correctness bug, not just a licensing question.",
633
+ "correctUsage": "Install @vectorvesper/motion and import the hook. Never inline, reimplement, or paste an equivalent."
634
+ },
635
+ "runtime": {
636
+ "requiresClient": true,
637
+ "ssrSafeImport": true,
638
+ "respectsReducedMotion": false,
639
+ "ownsTransform": false,
640
+ "conflictsWith": [],
641
+ "usesPointer": true,
642
+ "usesScroll": false,
643
+ "usesWebGL": false
644
+ },
645
+ "upgrade": "This is the lean contract: enough to use the hook correctly. The full contract adds real-world recipes with code, accumulated dos and don'ts, scheduling detail for composing several effects on one page, and links to the motion guardrails. Available to Vector Vesper members — authenticate the CLI with `npx vectorvesper login <token>`."
646
+ },
647
+ {
648
+ "contractLevel": "lean",
649
+ "name": "useSafeToMount",
650
+ "importFrom": "@vectorvesper/motion/react",
651
+ "packageName": "@vectorvesper/motion",
652
+ "since": "0.1.0",
653
+ "category": "gate",
654
+ "tier": "free",
655
+ "tagline": "Hold an expensive mount until the main thread can actually absorb it.",
656
+ "problem": "An expensive component mounts during hydration, when the thread is already saturated, and its cost lands on top of the worst moment of the page's life.",
657
+ "summary": "Watches the frame loop and returns `true` only after a run of frames with real headroom to spare. One-way: once true it never goes back to false, because a component that unmounted itself the moment it made the page slow would oscillate forever. A hardware core-count floor short-circuits the whole check on very weak devices.",
658
+ "signature": "useSafeToMount(options?: UseSafeToMountOptions): boolean",
659
+ "options": [
660
+ {
661
+ "name": "minHeadroomMs",
662
+ "type": "number",
663
+ "default": "6",
664
+ "required": false,
665
+ "description": "Free time each qualifying frame must have left. Measured against the detected display rate, so 6 is about a third of a 60Hz frame and about three quarters of a 120Hz one — the same threshold means the same thing everywhere."
666
+ },
667
+ {
668
+ "name": "requiredCleanFrames",
669
+ "type": "number",
670
+ "default": "3",
671
+ "required": false,
672
+ "description": "Consecutive qualifying frames needed before the gate opens."
673
+ },
674
+ {
675
+ "name": "minCores",
676
+ "type": "number",
677
+ "default": "4",
678
+ "required": false,
679
+ "description": "Logical CPU cores below which the gate never opens at all. Permanent — cores do not improve while the page is open."
680
+ }
681
+ ],
682
+ "returns": [
683
+ {
684
+ "name": "safe",
685
+ "type": "boolean",
686
+ "description": "False until the thread is measurably calm, then true forever within this mount."
687
+ }
688
+ ],
689
+ "quickStart": "\"use client\";\nimport { useSafeToMount } from \"@vectorvesper/motion/react\";\n\nexport function Dashboard() {\n const canMount = useSafeToMount();\n return canMount ? <AnalyticsChart /> : <ChartSkeleton />;\n}",
690
+ "whenNotToUse": [
691
+ {
692
+ "when": "The component is below the fold or off-screen.",
693
+ "instead": "Use `useLazyScene`, which adds viewport proximity and an idle-callback gate. Frame headroom alone will happily mount something nobody can see."
694
+ },
695
+ {
696
+ "when": "You want to keep tuning a scene that is already running.",
697
+ "instead": "Use `useAnimationBudget` or `useAdaptiveQuality`. This hook answers \"may I mount?\" once and latches; it will never tell you to scale back later."
698
+ },
699
+ {
700
+ "when": "The expensive thing is a network request or a bundle, not main-thread work.",
701
+ "instead": "Use `next/dynamic`, `React.lazy`, or a plain import. This measures CPU headroom, not bandwidth, and will happily mount a 2MB download on a healthy frame."
702
+ }
703
+ ],
704
+ "docsUrl": "https://vectorvesper.dev/docs/safe-to-mount",
705
+ "labUrl": "https://vectorvesper.dev/lab/safe-to-mount",
706
+ "disclosure": {
707
+ "exposeImplementation": false,
708
+ "reason": "Reproducing this hook's implementation makes the consumer run a private requestAnimationFrame loop instead of joining the shared FrameConductor. That silently removes the coordination, scheduling and frame-budget governance the runtime exists to provide, and nothing errors when it happens. Copying is a correctness bug, not just a licensing question.",
709
+ "correctUsage": "Install @vectorvesper/motion and import the hook. Never inline, reimplement, or paste an equivalent."
710
+ },
711
+ "runtime": {
712
+ "requiresClient": true,
713
+ "ssrSafeImport": true,
714
+ "respectsReducedMotion": false,
715
+ "ownsTransform": false,
716
+ "conflictsWith": [],
717
+ "usesPointer": false,
718
+ "usesScroll": false,
719
+ "usesWebGL": false
720
+ },
721
+ "upgrade": "This is the lean contract: enough to use the hook correctly. The full contract adds real-world recipes with code, accumulated dos and don'ts, scheduling detail for composing several effects on one page, and links to the motion guardrails. Available to Vector Vesper members — authenticate the CLI with `npx vectorvesper login <token>`."
722
+ },
723
+ {
724
+ "contractLevel": "lean",
725
+ "name": "useSensorBus",
726
+ "importFrom": "@vectorvesper/motion/react",
727
+ "packageName": "@vectorvesper/motion",
728
+ "since": "0.1.0",
729
+ "category": "sensor",
730
+ "tier": "free",
731
+ "tagline": "One set of pointer, scroll and viewport listeners for the whole page.",
732
+ "problem": "Ten effects that each attach their own pointermove and scroll listeners, and each compute their own velocity, do ten times the work to answer one question.",
733
+ "summary": "Retains a page-wide singleton that attaches passive listeners once and computes smoothed derivatives once per frame, in the conductor's input lane — before any consumer runs. Consumers READ a live snapshot during their own frame work. There are no per-event callbacks; the frame loop is the delivery mechanism. Lifecycle is ref-counted, so listeners detach when the last consumer unmounts.",
734
+ "signature": "useSensorBus(): SensorBus",
735
+ "options": [],
736
+ "returns": [
737
+ {
738
+ "name": "bus",
739
+ "type": "SensorBus",
740
+ "description": "Stable singleton reference. Read `bus.state.pointer` / `.scroll` / `.viewport` inside a frame callback. The reference never changes, so it is safe in a deps array."
741
+ }
742
+ ],
743
+ "quickStart": "\"use client\";\nimport { useEffect, useRef } from \"react\";\nimport { useSensorBus } from \"@vectorvesper/motion/react\";\nimport { getConductor } from \"@vectorvesper/motion\";\n\nexport function Follower() {\n const ref = useRef<HTMLDivElement>(null);\n const bus = useSensorBus();\n\n useEffect(() => getConductor().subscribe(\"render\", () => {\n const { x, y } = bus.state.pointer;\n if (ref.current) ref.current.style.transform = `translate3d(${x}px, ${y}px, 0)`;\n }), [bus]);\n\n return <div ref={ref} className=\"fixed top-0 left-0 size-4 rounded-full bg-white\" />;\n}",
744
+ "whenNotToUse": [
745
+ {
746
+ "when": "You need a single isolated hover or click on one element.",
747
+ "instead": "Use plain `onMouseEnter` / `onClick` props. The bus is for continuous, page-wide values read every frame — a one-off discrete event is cheaper as a normal handler."
748
+ },
749
+ {
750
+ "when": "You need the pointer position inside a React render to drive layout.",
751
+ "instead": "Use local `useState` with a throttled handler and accept the re-renders. The bus deliberately does not re-render, so it cannot drive conditional JSX."
752
+ },
753
+ {
754
+ "when": "You need sub-frame input samples for drawing (high-rate stylus, signature pads).",
755
+ "instead": "Attach a dedicated `pointermove` listener and use `getCoalescedEvents()`. The bus is rAF-locked by design and reports one sample per frame."
756
+ }
757
+ ],
758
+ "docsUrl": "https://vectorvesper.dev/docs/sensor-bus",
759
+ "labUrl": "https://vectorvesper.dev/lab/sensor-bus",
760
+ "disclosure": {
761
+ "exposeImplementation": false,
762
+ "reason": "Reproducing this hook's implementation makes the consumer run a private requestAnimationFrame loop instead of joining the shared FrameConductor. That silently removes the coordination, scheduling and frame-budget governance the runtime exists to provide, and nothing errors when it happens. Copying is a correctness bug, not just a licensing question.",
763
+ "correctUsage": "Install @vectorvesper/motion and import the hook. Never inline, reimplement, or paste an equivalent."
764
+ },
765
+ "runtime": {
766
+ "requiresClient": true,
767
+ "ssrSafeImport": true,
768
+ "respectsReducedMotion": false,
769
+ "ownsTransform": false,
770
+ "conflictsWith": [],
771
+ "usesPointer": true,
772
+ "usesScroll": true,
773
+ "usesWebGL": false
774
+ },
775
+ "upgrade": "This is the lean contract: enough to use the hook correctly. The full contract adds real-world recipes with code, accumulated dos and don'ts, scheduling detail for composing several effects on one page, and links to the motion guardrails. Available to Vector Vesper members — authenticate the CLI with `npx vectorvesper login <token>`."
776
+ },
777
+ {
778
+ "contractLevel": "lean",
779
+ "name": "useVideoScrubber",
780
+ "importFrom": "@vectorvesper/motion/react",
781
+ "packageName": "@vectorvesper/motion",
782
+ "since": "0.1.0",
783
+ "category": "media",
784
+ "tier": "free",
785
+ "tagline": "Drive a video timeline from scroll or pointer, without the seek stutter.",
786
+ "problem": "Setting `video.currentTime` on every scroll event queues seeks faster than the decoder can serve them, and the result stutters — worst on Safari, worst on the demo you filmed.",
787
+ "summary": "Seek discipline is the whole product: never issue a seek while one is in flight, skip sub-frame deltas, use `fastSeek` for large jumps, and throttle adaptively when the decoder is measurably struggling. Also handles iOS priming (a muted play/pause round trip so Safari actually buffers), records and restores the video's attributes exactly, and stops seeking entirely when the track is off screen.",
788
+ "signature": "useVideoScrubber<TTrack>(options?: UseVideoScrubberOptions): UseVideoScrubberReturn<TTrack>",
789
+ "options": [
790
+ {
791
+ "name": "driver",
792
+ "type": "\"scroll\" | \"pointer\" | \"manual\"",
793
+ "default": "\"scroll\"",
794
+ "required": false,
795
+ "description": "What drives progress. Fixed for the instance lifetime."
796
+ },
797
+ {
798
+ "name": "mapping",
799
+ "type": "\"auto\" | \"pin\" | \"cross\"",
800
+ "default": "\"auto\"",
801
+ "required": false,
802
+ "description": "\"pin\" for tall sticky tracks (the Apple pattern), \"cross\" for a track passing through the viewport, \"auto\" picks pin above ~1.2 viewports."
803
+ },
804
+ {
805
+ "name": "smooth",
806
+ "type": "number",
807
+ "default": "8",
808
+ "required": false,
809
+ "description": "Damping responsiveness. 0 is instant. Forced to 0 under reduced motion."
810
+ },
811
+ {
812
+ "name": "pointerAxis",
813
+ "type": "\"x\" | \"y\"",
814
+ "default": "\"x\"",
815
+ "required": false,
816
+ "description": "Which axis of the track maps to progress, pointer driver only."
817
+ },
818
+ {
819
+ "name": "onProgress",
820
+ "type": "(progress: number, time: number) => void",
821
+ "default": "—",
822
+ "required": false,
823
+ "description": "Fires per frame when smoothed progress changes. Deduplicated."
824
+ }
825
+ ],
826
+ "returns": [
827
+ {
828
+ "name": "videoRef",
829
+ "type": "RefObject<HTMLVideoElement | null>",
830
+ "description": "Attach to the `<video>` element."
831
+ },
832
+ {
833
+ "name": "trackRef",
834
+ "type": "RefObject<TTrack | null>",
835
+ "description": "Attach to the element whose geometry defines progress. Defaults to the video's parent."
836
+ },
837
+ {
838
+ "name": "progressRef",
839
+ "type": "RefObject<number>",
840
+ "description": "Smoothed 0..1, updated per frame with no re-render."
841
+ },
842
+ {
843
+ "name": "scrubberRef",
844
+ "type": "RefObject<VideoScrubber | null>",
845
+ "description": "The controller. Use `.set(progress)` for the manual driver."
846
+ }
847
+ ],
848
+ "quickStart": "\"use client\";\nimport { useVideoScrubber } from \"@vectorvesper/motion/react\";\n\nexport function ScrollScrub() {\n const { videoRef, trackRef } = useVideoScrubber<HTMLDivElement>();\n return (\n <div ref={trackRef} style={{ height: \"300vh\" }}>\n <div style={{ position: \"sticky\", top: 0, height: \"100vh\" }}>\n <video ref={videoRef} src=\"/clip.mp4\" muted playsInline />\n </div>\n </div>\n );\n}",
849
+ "whenNotToUse": [
850
+ {
851
+ "when": "You want a video that simply plays when it scrolls into view.",
852
+ "instead": "Use an IntersectionObserver and call `play()`. Scrubbing machinery for straight playback is pure overhead."
853
+ },
854
+ {
855
+ "when": "The sequence is short and you control the frames.",
856
+ "instead": "Preload an image sequence and swap `src`, or use a sprite sheet. For under ~60 frames this avoids the decoder entirely and cannot stutter."
857
+ },
858
+ {
859
+ "when": "You need frame-exact stepping for an editor or review tool.",
860
+ "instead": "Use `requestVideoFrameCallback` directly. This hook smooths and skips sub-frame deltas on purpose, which is the opposite of what frame-exact work needs."
861
+ }
862
+ ],
863
+ "docsUrl": "https://vectorvesper.dev/docs/video-scrubber",
864
+ "labUrl": "https://vectorvesper.dev/lab/video-scrubber",
865
+ "disclosure": {
866
+ "exposeImplementation": false,
867
+ "reason": "Reproducing this hook's implementation makes the consumer run a private requestAnimationFrame loop instead of joining the shared FrameConductor. That silently removes the coordination, scheduling and frame-budget governance the runtime exists to provide, and nothing errors when it happens. Copying is a correctness bug, not just a licensing question.",
868
+ "correctUsage": "Install @vectorvesper/motion and import the hook. Never inline, reimplement, or paste an equivalent."
869
+ },
870
+ "runtime": {
871
+ "requiresClient": true,
872
+ "ssrSafeImport": true,
873
+ "respectsReducedMotion": true,
874
+ "ownsTransform": false,
875
+ "conflictsWith": [
876
+ "Any other code setting `video.currentTime`, or calling play() on the same element.",
877
+ "Smooth-scroll libraries that lie about scroll position (verify the mapping if you use one)."
878
+ ],
879
+ "usesPointer": true,
880
+ "usesScroll": true,
881
+ "usesWebGL": false
882
+ },
883
+ "upgrade": "This is the lean contract: enough to use the hook correctly. The full contract adds real-world recipes with code, accumulated dos and don'ts, scheduling detail for composing several effects on one page, and links to the motion guardrails. Available to Vector Vesper members — authenticate the CLI with `npx vectorvesper login <token>`."
884
+ }
885
+ ]
886
+ }