@unrulysystems/native-motion-web 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/animate.ts ADDED
@@ -0,0 +1,39 @@
1
+ 'use client'
2
+ // REQ-API-039 / REQ-WEB-021 — the web value is the raw pin, including its complete overload
3
+ // family and graceful empty-subject behavior. The shallow named compatibility types below remain
4
+ // mapped Native Motion extension names; they do not narrow the exported web value.
5
+ //
6
+ // Return type is deliberately a SHALLOW public contract (not pin overload expansion) so the
7
+ // dual-entry type-identity gate does not hang tsgo on motion's animate overloads.
8
+
9
+ import { animate as pinAnimate } from 'motion/react'
10
+ import type { NormalizeOptions } from './normalize'
11
+
12
+ export const ANIMATE_OWNER = 'animate' as const
13
+
14
+ /** Public playback controls surface (catalog-exercised members). */
15
+ export interface AnimationPlaybackControls {
16
+ time: number
17
+ speed: number
18
+ duration: number
19
+ readonly finished: Promise<unknown>
20
+ play: () => void
21
+ pause: () => void
22
+ stop: () => void
23
+ cancel: () => void
24
+ complete: () => void
25
+ then: (onResolve: () => void, onReject?: () => void) => Promise<void>
26
+ }
27
+
28
+ export type AnimateFn = (
29
+ subject: unknown,
30
+ keyframes?: unknown,
31
+ transition?: unknown,
32
+ ) => AnimationPlaybackControls
33
+
34
+ /** Internal test seam retained for compatibility; it returns the raw pin without adding policy. */
35
+ export function createAnimate(_options: NormalizeOptions): typeof pinAnimate {
36
+ return pinAnimate
37
+ }
38
+
39
+ export const animate = pinAnimate
@@ -0,0 +1,167 @@
1
+ // The prop disposition table (REQ-WEB-012): every public prop has exactly ONE disposition —
2
+ // pass-through, normalized, extension-mapped, or rejected(loud). Compile-time exhaustiveness:
3
+ // the `satisfies Record<PublicPropKey, Disposition>` clause makes a Target key or gesture key
4
+ // without a row a TYPE ERROR (keyof Target is the literal union core builds from its own literal
5
+ // key arrays). Runtime totality: dispositionFor throws on any undeclared key. The registry
6
+ // stores keys widened to string, so the drift guard lives in disposition.test.ts — the same
7
+ // compile-time↔runtime bridge pattern core's component types use against the registry.
8
+ import type { Target } from '@unrulysystems/native-motion-core'
9
+ import { MotionWebIncompatibleError } from './errors'
10
+
11
+ export type RejectionReason =
12
+ // Historical refusal categories remain in the error vocabulary for mapped extensions and
13
+ // schema migrations. Intrinsic-web/pointer capabilities no longer use them (REQ-WEB-019).
14
+ | 'deferred-spike-scope'
15
+ | 'outside-universal-target'
16
+ // Reserved for the native-extension registry category (empty today): a capability with no
17
+ // faithful web mapping fails loud naming the platform it requires (REQ-WEB-015).
18
+ | 'native-only-on-web'
19
+
20
+ export type Disposition =
21
+ | { readonly kind: 'pass-through' }
22
+ // Reserved (REQ-WEB-012 names four dispositions): no current prop differs in shape between the
23
+ // canonical public surface and motion/react, so no row uses 'normalized' yet.
24
+ | { readonly kind: 'normalized' }
25
+ // The touch-first extension mapping (REQ-WEB-013): `dragSnapPoints` is CLASSIFIED here as
26
+ // extension-mapped so the `satisfies` exhaustiveness guard covers it (a dropped or drifted mapping
27
+ // is a TYPE ERROR). The View performs the actual `dragSnapPoints` → `dragTransition.modifyTarget`
28
+ // mapping over core-validated points — the disposition is the classification, not the mechanism.
29
+ | { readonly kind: 'extension-mapped'; readonly extension: 'dragSnapPoints' }
30
+ | { readonly kind: 'rejected'; readonly reason: RejectionReason }
31
+
32
+ // Gesture keys mirror the registry's gesture/pointer-gated entries; web-effect keys mirror its
33
+ // web-only entries. Literal unions (the registry widens to string); the drift-guard test pins
34
+ // set equality in both directions.
35
+ type GestureKey =
36
+ | 'whileTap'
37
+ | 'onTap'
38
+ | 'onTapStart'
39
+ | 'onTapCancel'
40
+ | 'drag'
41
+ | 'dragPropagation'
42
+ | 'dragControls'
43
+ | 'dragListener'
44
+ | 'onMeasureDragConstraints'
45
+ | 'whileDrag'
46
+ | 'onDrag'
47
+ | 'onDragStart'
48
+ | 'onDragEnd'
49
+ | 'whileHover'
50
+ | 'onHoverStart'
51
+ | 'onHoverEnd'
52
+ | 'whileFocus'
53
+ type WebEffectKey = 'filter' | 'clipPath' | 'boxShadow' | 'backgroundImage'
54
+ // Touch-first extension keys (REQ-WEB-013): public props beyond motion/react's own surface that map
55
+ // to a Motion prop. Exported so the drift-guard test pins set equality against the table.
56
+ export const EXTENSION_KEYS = ['dragSnapPoints'] as const
57
+ type ExtensionKey = (typeof EXTENSION_KEYS)[number]
58
+ // Structural target members (T23 B3): carriers that live INSIDE a target whose SUB-keys are the
59
+ // capabilities — they have no registry entry of their own. Declared so the drift guard stays
60
+ // fail-closed on genuinely phantom rows while the table stays total over keyof Target.
61
+ export const STRUCTURAL_TARGET_KEYS = ['transitionEnd'] as const
62
+
63
+ type PublicPropKey = keyof Target | GestureKey | WebEffectKey | ExtensionKey
64
+
65
+ const PASS: Disposition = Object.freeze({ kind: 'pass-through' })
66
+ export const DISPOSITIONS = Object.freeze({
67
+ // Transforms — canonical shapes ARE motion/react shapes.
68
+ x: PASS,
69
+ y: PASS,
70
+ scale: PASS,
71
+ scaleX: PASS,
72
+ scaleY: PASS,
73
+ rotate: PASS,
74
+ rotateX: PASS,
75
+ rotateY: PASS,
76
+ skewX: PASS,
77
+ skewY: PASS,
78
+ perspective: PASS,
79
+ // Opacity + colors.
80
+ opacity: PASS,
81
+ backgroundColor: PASS,
82
+ color: PASS,
83
+ borderColor: PASS,
84
+ // Discrete keywords (T23 B): motion/react's own mixVisibility drives display/visibility on
85
+ // web — the shim passes them through untouched.
86
+ display: PASS,
87
+ visibility: PASS,
88
+ // T23 B3: transitionEnd is a target-level sub-target motion/react executes natively
89
+ // (visual-element-target.ts:159-168 — values jump at settle); the shim forwards it whole.
90
+ transitionEnd: PASS,
91
+ // Layout props.
92
+ width: PASS,
93
+ height: PASS,
94
+ top: PASS,
95
+ right: PASS,
96
+ bottom: PASS,
97
+ left: PASS,
98
+ borderRadius: PASS,
99
+ borderTopLeftRadius: PASS,
100
+ borderTopRightRadius: PASS,
101
+ borderBottomLeftRadius: PASS,
102
+ borderBottomRightRadius: PASS,
103
+ borderWidth: PASS,
104
+ borderTopWidth: PASS,
105
+ borderRightWidth: PASS,
106
+ borderBottomWidth: PASS,
107
+ borderLeftWidth: PASS,
108
+ margin: PASS,
109
+ marginTop: PASS,
110
+ marginRight: PASS,
111
+ marginBottom: PASS,
112
+ marginLeft: PASS,
113
+ padding: PASS,
114
+ paddingTop: PASS,
115
+ paddingRight: PASS,
116
+ paddingBottom: PASS,
117
+ paddingLeft: PASS,
118
+ // Gesture capability + event callbacks — motion/react's own surface, zero new semantics.
119
+ drag: PASS,
120
+ dragPropagation: PASS,
121
+ // R15 D3 (REQ-API-043): pin's own dragControls hub + dragListener gate — pass-through.
122
+ dragControls: PASS,
123
+ dragListener: PASS,
124
+ onMeasureDragConstraints: PASS,
125
+ onDrag: PASS,
126
+ onDragStart: PASS,
127
+ onDragEnd: PASS,
128
+ onTap: PASS,
129
+ onTapStart: PASS,
130
+ onTapCancel: PASS,
131
+ onHoverStart: PASS,
132
+ onHoverEnd: PASS,
133
+ // Gesture-STATE props: R6 (REQ-API-031) shipped the OBJECT form into motion's own
134
+ // gesture-state machinery; R7 (REQ-API-032) shipped the LABEL form — string-only
135
+ // validity at the normalize boundary, resolution/propagation motion's own. The
136
+ // REQ-WEB-019: pointer/keyboard states are intrinsic-web and therefore remain raw pin
137
+ // pass-throughs on this entry. Native keeps its platform-unavailable refusal.
138
+ whileTap: PASS,
139
+ whileDrag: PASS,
140
+ whileHover: PASS,
141
+ whileFocus: PASS,
142
+ // Registry-owned intrinsic-web effects. Motion owns their value grammar and interpolation;
143
+ // the shim does not apply Native Motion's portable-target normalizer to them.
144
+ filter: PASS,
145
+ clipPath: PASS,
146
+ boxShadow: PASS,
147
+ backgroundImage: PASS,
148
+ // Touch-first extension (REQ-WEB-013): classified extension-mapped so a dropped/drifted mapping is
149
+ // a compile-time error; the View owns the runtime dragSnapPoints → dragTransition projection.
150
+ dragSnapPoints: Object.freeze({
151
+ kind: 'extension-mapped',
152
+ extension: 'dragSnapPoints',
153
+ }) as Disposition,
154
+ } satisfies Record<PublicPropKey, Disposition>)
155
+
156
+ // Runtime totality guard (REQ-WEB-012): an undeclared key is schema drift — it throws in EVERY
157
+ // mode; it is never a user error to be report-and-refused.
158
+ export function dispositionFor(key: string): Disposition {
159
+ const disposition = (DISPOSITIONS as Record<string, Disposition>)[key]
160
+ if (disposition === undefined) {
161
+ throw new MotionWebIncompatibleError(
162
+ `no disposition declared for prop "${key}" — the web shim's disposition table must stay ` +
163
+ 'total over the public prop union (REQ-WEB-012)',
164
+ )
165
+ }
166
+ return disposition
167
+ }
@@ -0,0 +1,3 @@
1
+ // REQ-WEB-021: imperative drag controls are owned by the pinned web engine. Preserve the
2
+ // constructor, hook, hub identity, option domain, and empty-subscriber behavior verbatim.
3
+ export { DragControls, useDragControls } from 'motion/react'
package/src/errors.ts ADDED
@@ -0,0 +1,30 @@
1
+ // Loud-failure vocabulary (REQ-WEB-015 + the ratified severity law, 2026-07-06): development
2
+ // throws, production reports through the error channel and REFUSES the offending property. The
3
+ // classes here are pure data — severity is applied by the caller (see normalize.ts), never by an
4
+ // ambient environment read inside this package.
5
+
6
+ export type WebErrorMode = 'development' | 'production'
7
+ export type WebErrorReporter = (error: MotionWebError) => void
8
+
9
+ export class MotionWebError extends Error {
10
+ override name = 'MotionWebError'
11
+ }
12
+
13
+ // A capability the web engine must refuse at this scope: a ratified spike deferral, a registry
14
+ // member outside the universal Target, or (once the category is populated) a native-only
15
+ // extension. Messages name the capability and where it CAN run (REQ-WEB-015).
16
+ export class MotionWebRejectionError extends MotionWebError {
17
+ override name = 'MotionWebRejectionError'
18
+ }
19
+
20
+ // A legal input motion/react cannot express faithfully — a recorded divergence, e.g. differing
21
+ // per-axis snap sets through the single axis-blind modifyTarget.
22
+ export class MotionWebFidelityError extends MotionWebError {
23
+ override name = 'MotionWebFidelityError'
24
+ }
25
+
26
+ // A malformed or internally-inconsistent input (non-finite numbers, snap points for an axis that
27
+ // is not dragged, an undeclared prop key) — never projected into behavior.
28
+ export class MotionWebIncompatibleError extends MotionWebError {
29
+ override name = 'MotionWebIncompatibleError'
30
+ }
package/src/index.ts ADDED
@@ -0,0 +1,157 @@
1
+ 'use client'
2
+ // Web driver-peer entry (SPEC-WEB-SHIM). The 'use client' directive is the ratified RSC posture
3
+ // (2026-07-06): the shim ships its own client-component boundary, matching motion/react.
4
+ //
5
+ // The runtime surface is the spike's universal set: the View/Text/Image wrappers (normalization +
6
+ // validation over motion.div / motion.p|span / motion.img) and motion/react's own presence
7
+ // primitives re-exported under the public names (REQ-WEB-011 thin re-export — zero new
8
+ // semantics). REQ-API-034's closed host set ships here; premature use of anything outside it is a
9
+ // build error, never a silent no-op or an undefined component (REQ-WEB-015/016).
10
+
11
+ export type {
12
+ DragControlOptions,
13
+ DragControlsHandle,
14
+ DragInfo,
15
+ TapCallback,
16
+ TapInfo,
17
+ MotionTargetProps,
18
+ Target,
19
+ TransformProperties,
20
+ TransformTemplate,
21
+ Transition,
22
+ TransitionType,
23
+ } from '@unrulysystems/native-motion-core'
24
+
25
+ export {
26
+ Image,
27
+ Text,
28
+ View,
29
+ createView,
30
+ type ImageProps,
31
+ type TextProps,
32
+ type ViewProps,
33
+ } from './View'
34
+
35
+ // REQ-WEB-019: raw pin namespaces. Native's smaller host floor never narrows the web engine's
36
+ // intrinsic DOM/SVG hosts or motion.create custom-component factory.
37
+ export { motion, m } from 'motion/react'
38
+
39
+ // REQ-WEB-018: Motion-owned public additions stay raw on web while native remains scoped. Keep
40
+ // this list explicit: every exported name is classified by the parity census, so an accidental
41
+ // low-level barrel expansion fails closed instead of silently enlarging the product contract.
42
+ export {
43
+ AnimateSharedLayout,
44
+ LazyMotion,
45
+ Reorder,
46
+ domAnimation,
47
+ domMax,
48
+ useAnimateMini,
49
+ useAnimationControls,
50
+ useInView,
51
+ useMotionTemplate,
52
+ useReducedMotion,
53
+ useTime,
54
+ useViewportScroll,
55
+ } from 'motion/react'
56
+
57
+ // U7b (REQ-WEB-011): the pin's global instant-animation window pair, raw re-exports — zero
58
+ // new semantics. useInstantTransition returns the callback that opens the window
59
+ // (MotionGlobalConfig.instantAnimations, forceUpdate, two-postRender-frame token-guarded
60
+ // close — framer-motion utils/use-instant-transition.ts:9-41); disableInstantTransitions
61
+ // clears the flag directly (:43-45).
62
+ export { disableInstantTransitions, useInstantTransition } from 'motion/react'
63
+
64
+ // Presence: motion/react's own lifecycle primitives ARE the web engine's presence semantics
65
+ // (REQ-WEB-011); the native runtime binds core's createPresenceController to the same names.
66
+ // The container rides the shim wrapper so popLayout is forwarded to the pin's PopChild geometry
67
+ // composition, matching the native host contract; T24 B1 custom is forwarded through the wrapper,
68
+ // and the hooks stay raw motion re-exports so their presence context is unchanged.
69
+ export { AnimatePresence, createAnimatePresence } from './AnimatePresence'
70
+ export { usePresence, useIsPresent, usePresenceData } from 'motion/react'
71
+ // R4 — the public value channel (REQ-API-025..027): raw motion/react re-exports (REQ-WEB-011
72
+ // thin re-export, zero new semantics) — the same real hooks the pinned oracle ships. The
73
+ // MotionValue type follows for name parity with the native entry (engine-specific by design).
74
+ export {
75
+ useMotionValue,
76
+ useMotionValueEvent,
77
+ useSpring,
78
+ useTransform,
79
+ useVelocity,
80
+ } from 'motion/react'
81
+ export { MotionValue } from 'motion/react'
82
+ // REQ-API-037: raw pin hook value; separately named compatibility types remain cross-entry mapped.
83
+ export { useScroll, type ScrollMotionValues, type UseScrollOptions } from './useScroll'
84
+ // R12 M1 (REQ-API-039): thin validated re-export of the pin's animate / useAnimate —
85
+ // entry-gate validation only; the pin owns all playback semantics. AnimateFn is the
86
+ // shallow public callable type (dual-entry type-identity must not see pin overloads).
87
+ export { animate, createAnimate, type AnimateFn, type AnimationPlaybackControls } from './animate'
88
+ export { useAnimate, type AnimationScope, type UseAnimateReturn } from './useAnimate'
89
+ // R13 M1 (REQ-API-040): thin re-export of the pin's useCycle (REQ-WEB-011).
90
+ export { useCycle, type Cycle, type CycleState } from './useCycle'
91
+ // R15 D3 (REQ-API-043): pin hub + empty-start fail-loud (G-INV-8; REQ-WEB-011 session semantics).
92
+ export { useDragControls, DragControls } from './dragControls'
93
+ // R13 value-breadth (REQ-VALUETYPE-013): no-op on web (pin measures); namespace parity with native.
94
+ export {
95
+ MotionLengthLayoutProvider,
96
+ type MotionLengthLayoutProviderProps,
97
+ } from './MotionLengthLayoutProvider'
98
+ // R13 V2 (REQ-LAYOUT-022(c)): no-op composition on web (pin owns overflow:scroll measure).
99
+ export {
100
+ LayoutScrollOffsetProvider,
101
+ createLayoutScrollOffsetRef,
102
+ recordLayoutScrollOffsetFromEvent,
103
+ type LayoutScrollOffset,
104
+ type LayoutScrollOffsetProviderProps,
105
+ type LayoutScrollOffsetRef,
106
+ } from './LayoutScrollOffsetProvider'
107
+ // Product dual-authority release helpers (absolute origin + project-then-snap) — key parity
108
+ // with the dual public entry and the gallery dogfood surface.
109
+ export { absoluteDragOrigin, selectReleaseTarget } from '@unrulysystems/native-motion-core'
110
+ // REQ-WEB-011: utilities are raw pin values on web. Native's scoped core implementations never
111
+ // project their narrower domains or identities onto this entry.
112
+ export { mix, stagger, wrap } from 'motion/react'
113
+ // U7g (REQ-LAYOUT-024): the pin's scale-corrector channel, raw re-export.
114
+ export { addScaleCorrector } from 'motion/react'
115
+ // U7j (REQ-API-058): the pin's JS-frame clock, raw re-export. Native implements the
116
+ // same shape on the value-channel stepper. The `frame` scheduler stays residual.
117
+ export { frameData } from 'motion/react'
118
+ export type { FrameData } from 'motion-dom'
119
+ // U8 (REQ-API-059): pin parametric path factory. Types live on motion-dom; motion/react
120
+ // re-exports the value at runtime. Native implements the same call shape in core.
121
+ export { arc } from 'motion-dom'
122
+ export type { ArcOptions } from 'motion-dom'
123
+ // The R4 public option-type NAMES exist on both entries (r13 major 4 — the namespace gate only
124
+ // covers value exports); shapes are engine-specific by design. Web semantics come straight from
125
+ // the pinned motion-dom types: SpringValueOptions is the exact options type motion's useSpring
126
+ // accepts (its UseSpringOptions alias is unexported upstream), and TransformOptions is
127
+ // motion-dom's own generic re-surfaced with a `never` default so the bare name resolves like
128
+ // the native entry's.
129
+ export type SpringValueOptions = MotionDomSpringOptions &
130
+ Pick<MotionDomFollowValueOptions, 'skipInitialAnimation'>
131
+ export type TransformOptions<T = never> = MotionDomTransformOptions<T>
132
+
133
+ // FLAG 5a (REQ-API-022): the root primitive — pass-through on web (no gesture root needed);
134
+ // exists on both entries for the one-import universal surface (value-key parity).
135
+ export { MotionRoot, type MotionRootProps } from './MotionRoot'
136
+ // R10 M1 (REQ-API-035): thin validated re-export of the pin's MotionConfig — entry-gate
137
+ // validation only; the pin owns subtree defaults and reducedMotion behavior.
138
+ export {
139
+ MotionConfig,
140
+ createMotionConfig,
141
+ type MotionConfigProps,
142
+ type MotionConfigTransition,
143
+ type ReducedMotionConfig,
144
+ } from './MotionConfig'
145
+ // R10 M2 (REQ-API-036): thin validated re-export of the pin's LayoutGroup — entry-gate
146
+ // validation only; the pin owns id namespacing and projection groups.
147
+ export {
148
+ LayoutGroup,
149
+ createLayoutGroup,
150
+ type LayoutGroupInherit,
151
+ type LayoutGroupProps,
152
+ } from './LayoutGroup'
153
+ import type {
154
+ FollowValueOptions as MotionDomFollowValueOptions,
155
+ SpringOptions as MotionDomSpringOptions,
156
+ TransformOptions as MotionDomTransformOptions,
157
+ } from 'motion-dom'
package/src/mode.ts ADDED
@@ -0,0 +1,12 @@
1
+ // Ambient severity-mode resolution — the ONE place the React layer reads the environment (the
2
+ // pure layer takes mode as a parameter; flagged at M2). Bundlers statically replace
3
+ // process.env.NODE_ENV on web; anything not 'production' is development (fail-loud default).
4
+ import type { WebErrorMode } from './errors'
5
+
6
+ declare const process: { env?: { NODE_ENV?: string } } | undefined
7
+
8
+ export function ambientMode(): WebErrorMode {
9
+ return typeof process !== 'undefined' && process.env?.NODE_ENV === 'production'
10
+ ? 'production'
11
+ : 'development'
12
+ }