@multitrack/react 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-present Jack Hsu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # @multitrack/react
2
+
3
+ React bindings for [`@multitrack/core`](https://github.com/jakhsu/multitrack/tree/main/packages/core) — a scroll-driven animation engine with a multi-track timeline architecture.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @multitrack/core @multitrack/react
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```tsx
14
+ import { MultitrackProvider, ScrollContainer, FixedStage, Show, useStep } from "@multitrack/react";
15
+ import type { StepConfig } from "@multitrack/core";
16
+
17
+ const config: StepConfig[] = [
18
+ { name: "intro", duration: 3, track: "main", easing: "linear" },
19
+ { name: "feature", duration: 5, track: "main" },
20
+ { name: "outro", duration: 3, track: "main", easing: "linear" },
21
+ ];
22
+
23
+ function App() {
24
+ return (
25
+ <MultitrackProvider config={config}>
26
+ <ScrollContainer>
27
+ <FixedStage>
28
+ <Show when="intro">
29
+ <IntroSection />
30
+ </Show>
31
+ <Show when="feature">
32
+ <FeatureSection />
33
+ </Show>
34
+ </FixedStage>
35
+ </ScrollContainer>
36
+ </MultitrackProvider>
37
+ );
38
+ }
39
+
40
+ function IntroSection() {
41
+ const { opacity } = useStep("intro");
42
+ return (
43
+ <div style={{ opacity, transform: `translateY(${(1 - opacity) * 40}px)` }}>
44
+ <h1>Welcome</h1>
45
+ </div>
46
+ );
47
+ }
48
+ ```
49
+
50
+ ## API
51
+
52
+ ### `<MultitrackProvider>`
53
+
54
+ Wraps your app and creates the timeline instance. All hooks and components must be used within this provider.
55
+
56
+ ```tsx
57
+ <MultitrackProvider config={config} breakpoints={breakpoints} devtools>
58
+ {children}
59
+ </MultitrackProvider>
60
+ ```
61
+
62
+ ### Hooks
63
+
64
+ #### `useStep(name)`
65
+
66
+ Returns `{ opacity, isActive }` for a single step. Re-renders only when the step's values change.
67
+
68
+ #### `useOpacities()`
69
+
70
+ Returns an `Opacities` record (`{ [stepName]: number }`) for all steps.
71
+
72
+ #### `useScrollProgress()`
73
+
74
+ Returns the current scroll progress (0–1).
75
+
76
+ #### `useTimeline()`
77
+
78
+ Returns the underlying `Timeline` instance for advanced use cases.
79
+
80
+ ### Components
81
+
82
+ #### `<ScrollContainer>`
83
+
84
+ The scrollable container that drives the timeline. Renders a `<div>` with the correct scroll height.
85
+
86
+ #### `<FixedStage>`
87
+
88
+ A fixed-position overlay inside `ScrollContainer` for content that stays in the viewport while scrolling.
89
+
90
+ #### `<Show when="stepName">`
91
+
92
+ Conditionally renders children when the named step is active (opacity > 0).
93
+
94
+ ## License
95
+
96
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,141 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var core = require('@multitrack/core');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+
7
+ // src/provider.tsx
8
+ var MultitrackContext = react.createContext(
9
+ null
10
+ );
11
+ function MultitrackProvider({
12
+ config,
13
+ devtools = false,
14
+ breakpoints,
15
+ children
16
+ }) {
17
+ const timelineRef = react.useRef(null);
18
+ const timeline = react.useMemo(() => {
19
+ timelineRef.current?.destroy();
20
+ const t = new core.Timeline({ config, devtools, breakpoints });
21
+ timelineRef.current = t;
22
+ return t;
23
+ }, [config, devtools, breakpoints]);
24
+ const [state, setState] = react.useState({
25
+ scrollPercentage: 0,
26
+ currentStep: 0,
27
+ opacities: timeline.getOpacities(0)
28
+ });
29
+ react.useEffect(() => {
30
+ const ctx = timeline.scope(() => {
31
+ timeline.on("scroll", ({ scrollPercentage, currentStep }) => {
32
+ setState({
33
+ scrollPercentage,
34
+ currentStep,
35
+ opacities: timeline.getOpacities(scrollPercentage)
36
+ });
37
+ });
38
+ timeline.on("timeline:reconfigure", () => {
39
+ setState({
40
+ scrollPercentage: timeline.scrollPercentage,
41
+ currentStep: timeline.currentStep,
42
+ opacities: timeline.getOpacities()
43
+ });
44
+ });
45
+ });
46
+ timeline.start();
47
+ return () => {
48
+ ctx.dispose();
49
+ timeline.destroy();
50
+ };
51
+ }, [timeline]);
52
+ const contextValue = {
53
+ timeline,
54
+ steps: timeline.steps,
55
+ totalSteps: timeline.totalSteps,
56
+ scrollPercentage: state.scrollPercentage,
57
+ currentStep: state.currentStep,
58
+ opacities: state.opacities
59
+ };
60
+ return /* @__PURE__ */ jsxRuntime.jsx(MultitrackContext.Provider, { value: contextValue, children });
61
+ }
62
+ function useMultitrackContext() {
63
+ const ctx = react.useContext(MultitrackContext);
64
+ if (!ctx) {
65
+ throw new Error(
66
+ "[@multitrack/react] useTimeline must be used within a <MultitrackProvider>"
67
+ );
68
+ }
69
+ return ctx;
70
+ }
71
+ function useTimeline() {
72
+ return useMultitrackContext().timeline;
73
+ }
74
+ function useOpacities() {
75
+ return useMultitrackContext().opacities;
76
+ }
77
+ function useStep(name) {
78
+ const { opacities } = useMultitrackContext();
79
+ const opacity = opacities[name] ?? 0;
80
+ return { opacity, isActive: opacity > 0 };
81
+ }
82
+ function useScrollProgress() {
83
+ const { scrollPercentage, currentStep, totalSteps } = useMultitrackContext();
84
+ return { scrollPercentage, currentStep, totalSteps };
85
+ }
86
+ function ScrollContainer({
87
+ children,
88
+ className,
89
+ style
90
+ }) {
91
+ const { totalSteps } = useScrollProgress();
92
+ return /* @__PURE__ */ jsxRuntime.jsx(
93
+ "div",
94
+ {
95
+ style: { height: `${totalSteps * 100}vh`, position: "relative", ...style },
96
+ className,
97
+ children
98
+ }
99
+ );
100
+ }
101
+ function FixedStage({
102
+ children,
103
+ className,
104
+ style
105
+ }) {
106
+ return /* @__PURE__ */ jsxRuntime.jsx(
107
+ "div",
108
+ {
109
+ style: {
110
+ position: "fixed",
111
+ top: 0,
112
+ left: 0,
113
+ width: "100%",
114
+ height: "100vh",
115
+ touchAction: "pan-y",
116
+ ...style
117
+ },
118
+ className,
119
+ children
120
+ }
121
+ );
122
+ }
123
+ function Show({
124
+ when,
125
+ children
126
+ }) {
127
+ const { isActive } = useStep(when);
128
+ if (!isActive) return null;
129
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
130
+ }
131
+
132
+ exports.FixedStage = FixedStage;
133
+ exports.MultitrackProvider = MultitrackProvider;
134
+ exports.ScrollContainer = ScrollContainer;
135
+ exports.Show = Show;
136
+ exports.useOpacities = useOpacities;
137
+ exports.useScrollProgress = useScrollProgress;
138
+ exports.useStep = useStep;
139
+ exports.useTimeline = useTimeline;
140
+ //# sourceMappingURL=index.cjs.map
141
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/context.ts","../src/provider.tsx","../src/hooks.ts","../src/components.tsx"],"names":["createContext","useRef","useMemo","Timeline","useState","useEffect","useContext","jsx","Fragment"],"mappings":"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","file":"index.cjs","sourcesContent":["import { createContext } from \"react\";\nimport type { Timeline, Opacities, Step } from \"@multitrack/core\";\n\nexport interface MultitrackContextValue {\n timeline: Timeline;\n steps: Step[];\n totalSteps: number;\n scrollPercentage: number;\n currentStep: number;\n opacities: Opacities;\n}\n\nexport const MultitrackContext = createContext<MultitrackContextValue | null>(\n null,\n);\n","import {\n useRef,\n useState,\n useEffect,\n useMemo,\n type ReactNode,\n} from \"react\";\nimport { Timeline, type StepConfig } from \"@multitrack/core\";\nimport { MultitrackContext, type MultitrackContextValue } from \"./context.js\";\n\nexport interface MultitrackProviderProps {\n /** Step configuration array. */\n config: StepConfig[];\n /** Enable devtools integration. */\n devtools?: boolean;\n /** Named breakpoints for responsive step inclusion. */\n breakpoints?: Record<string, string>;\n children: ReactNode;\n}\n\n/**\n * Provides timeline state to all child components via React context.\n * Manages scroll listening and re-renders children when scroll position changes.\n */\nexport function MultitrackProvider({\n config,\n devtools = false,\n breakpoints,\n children,\n}: MultitrackProviderProps) {\n const timelineRef = useRef<Timeline | null>(null);\n\n // Create timeline instance (stable across renders unless config changes)\n const timeline = useMemo(() => {\n timelineRef.current?.destroy();\n const t = new Timeline({ config, devtools, breakpoints });\n timelineRef.current = t;\n return t;\n }, [config, devtools, breakpoints]);\n\n const [state, setState] = useState({\n scrollPercentage: 0,\n currentStep: 0,\n opacities: timeline.getOpacities(0),\n });\n\n useEffect(() => {\n const ctx = timeline.scope(() => {\n timeline.on(\"scroll\", ({ scrollPercentage, currentStep }) => {\n setState({\n scrollPercentage,\n currentStep,\n opacities: timeline.getOpacities(scrollPercentage),\n });\n });\n\n // Re-render when breakpoints change and steps are reconfigured\n timeline.on(\"timeline:reconfigure\", () => {\n setState({\n scrollPercentage: timeline.scrollPercentage,\n currentStep: timeline.currentStep,\n opacities: timeline.getOpacities(),\n });\n });\n });\n\n timeline.start();\n\n return () => {\n ctx.dispose();\n timeline.destroy();\n };\n }, [timeline]);\n\n const contextValue: MultitrackContextValue = {\n timeline,\n steps: timeline.steps,\n totalSteps: timeline.totalSteps,\n scrollPercentage: state.scrollPercentage,\n currentStep: state.currentStep,\n opacities: state.opacities,\n };\n\n return (\n <MultitrackContext.Provider value={contextValue}>\n {children}\n </MultitrackContext.Provider>\n );\n}\n","import { useContext } from \"react\";\nimport type { Timeline, Opacities } from \"@multitrack/core\";\nimport { MultitrackContext } from \"./context.js\";\n\nfunction useMultitrackContext() {\n const ctx = useContext(MultitrackContext);\n if (!ctx) {\n throw new Error(\n \"[@multitrack/react] useTimeline must be used within a <MultitrackProvider>\",\n );\n }\n return ctx;\n}\n\n/**\n * Access the Timeline instance directly.\n */\nexport function useTimeline(): Timeline {\n return useMultitrackContext().timeline;\n}\n\n/**\n * Get current opacities for all steps.\n */\nexport function useOpacities<T extends string = string>(): Opacities<T> {\n return useMultitrackContext().opacities as Opacities<T>;\n}\n\n/**\n * Get opacity and active state for a single named step.\n */\nexport function useStep(name: string): { opacity: number; isActive: boolean } {\n const { opacities } = useMultitrackContext();\n const opacity = opacities[name] ?? 0;\n return { opacity, isActive: opacity > 0 };\n}\n\n/**\n * Get raw scroll progress and current step position.\n */\nexport function useScrollProgress(): {\n scrollPercentage: number;\n currentStep: number;\n totalSteps: number;\n} {\n const { scrollPercentage, currentStep, totalSteps } =\n useMultitrackContext();\n return { scrollPercentage, currentStep, totalSteps };\n}\n","import type { ReactNode, CSSProperties } from \"react\";\nimport { useScrollProgress, useStep } from \"./hooks.js\";\n\n/**\n * Creates the tall scrollable container that drives the timeline.\n * Height = totalSteps * 100vh.\n *\n * Extracted from sinking-china MainMap.tsx line 193:\n * `<div style={{ height: `${totalSteps * 100}vh` }} className=\"relative\">`\n */\nexport function ScrollContainer({\n children,\n className,\n style,\n}: {\n children: ReactNode;\n className?: string;\n style?: CSSProperties;\n}) {\n const { totalSteps } = useScrollProgress();\n\n return (\n <div\n style={{ height: `${totalSteps * 100}vh`, position: \"relative\", ...style }}\n className={className}\n >\n {children}\n </div>\n );\n}\n\n/**\n * Fixed viewport stage that stays in place while the user scrolls.\n * All animated content lives inside this.\n *\n * Extracted from sinking-china MainMap.tsx lines 200-207:\n * `<div style={{ position: \"fixed\", width: \"100%\", height: \"100vh\", touchAction: \"pan-y\" }}>`\n */\nexport function FixedStage({\n children,\n className,\n style,\n}: {\n children: ReactNode;\n className?: string;\n style?: CSSProperties;\n}) {\n return (\n <div\n style={{\n position: \"fixed\",\n top: 0,\n left: 0,\n width: \"100%\",\n height: \"100vh\",\n touchAction: \"pan-y\",\n ...style,\n }}\n className={className}\n >\n {children}\n </div>\n );\n}\n\n/**\n * Conditionally renders children based on a step's opacity.\n * Unmounts children when the step is inactive (opacity 0) for performance.\n *\n * Extracted from sinking-china ConditionalRender.tsx.\n */\nexport function Show({\n when,\n children,\n}: {\n /** Step name to check. Children render when this step's opacity > 0. */\n when: string;\n children: ReactNode;\n}) {\n const { isActive } = useStep(when);\n if (!isActive) return null;\n return <>{children}</>;\n}\n"]}
@@ -0,0 +1,81 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode, CSSProperties } from 'react';
3
+ import { StepConfig, Opacities, Timeline } from '@multitrack/core';
4
+ export { EasingPreset, Opacities, StepConfig } from '@multitrack/core';
5
+
6
+ interface MultitrackProviderProps {
7
+ /** Step configuration array. */
8
+ config: StepConfig[];
9
+ /** Enable devtools integration. */
10
+ devtools?: boolean;
11
+ /** Named breakpoints for responsive step inclusion. */
12
+ breakpoints?: Record<string, string>;
13
+ children: ReactNode;
14
+ }
15
+ /**
16
+ * Provides timeline state to all child components via React context.
17
+ * Manages scroll listening and re-renders children when scroll position changes.
18
+ */
19
+ declare function MultitrackProvider({ config, devtools, breakpoints, children, }: MultitrackProviderProps): react_jsx_runtime.JSX.Element;
20
+
21
+ /**
22
+ * Access the Timeline instance directly.
23
+ */
24
+ declare function useTimeline(): Timeline;
25
+ /**
26
+ * Get current opacities for all steps.
27
+ */
28
+ declare function useOpacities<T extends string = string>(): Opacities<T>;
29
+ /**
30
+ * Get opacity and active state for a single named step.
31
+ */
32
+ declare function useStep(name: string): {
33
+ opacity: number;
34
+ isActive: boolean;
35
+ };
36
+ /**
37
+ * Get raw scroll progress and current step position.
38
+ */
39
+ declare function useScrollProgress(): {
40
+ scrollPercentage: number;
41
+ currentStep: number;
42
+ totalSteps: number;
43
+ };
44
+
45
+ /**
46
+ * Creates the tall scrollable container that drives the timeline.
47
+ * Height = totalSteps * 100vh.
48
+ *
49
+ * Extracted from sinking-china MainMap.tsx line 193:
50
+ * `<div style={{ height: `${totalSteps * 100}vh` }} className="relative">`
51
+ */
52
+ declare function ScrollContainer({ children, className, style, }: {
53
+ children: ReactNode;
54
+ className?: string;
55
+ style?: CSSProperties;
56
+ }): react_jsx_runtime.JSX.Element;
57
+ /**
58
+ * Fixed viewport stage that stays in place while the user scrolls.
59
+ * All animated content lives inside this.
60
+ *
61
+ * Extracted from sinking-china MainMap.tsx lines 200-207:
62
+ * `<div style={{ position: "fixed", width: "100%", height: "100vh", touchAction: "pan-y" }}>`
63
+ */
64
+ declare function FixedStage({ children, className, style, }: {
65
+ children: ReactNode;
66
+ className?: string;
67
+ style?: CSSProperties;
68
+ }): react_jsx_runtime.JSX.Element;
69
+ /**
70
+ * Conditionally renders children based on a step's opacity.
71
+ * Unmounts children when the step is inactive (opacity 0) for performance.
72
+ *
73
+ * Extracted from sinking-china ConditionalRender.tsx.
74
+ */
75
+ declare function Show({ when, children, }: {
76
+ /** Step name to check. Children render when this step's opacity > 0. */
77
+ when: string;
78
+ children: ReactNode;
79
+ }): react_jsx_runtime.JSX.Element | null;
80
+
81
+ export { FixedStage, MultitrackProvider, type MultitrackProviderProps, ScrollContainer, Show, useOpacities, useScrollProgress, useStep, useTimeline };
@@ -0,0 +1,81 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode, CSSProperties } from 'react';
3
+ import { StepConfig, Opacities, Timeline } from '@multitrack/core';
4
+ export { EasingPreset, Opacities, StepConfig } from '@multitrack/core';
5
+
6
+ interface MultitrackProviderProps {
7
+ /** Step configuration array. */
8
+ config: StepConfig[];
9
+ /** Enable devtools integration. */
10
+ devtools?: boolean;
11
+ /** Named breakpoints for responsive step inclusion. */
12
+ breakpoints?: Record<string, string>;
13
+ children: ReactNode;
14
+ }
15
+ /**
16
+ * Provides timeline state to all child components via React context.
17
+ * Manages scroll listening and re-renders children when scroll position changes.
18
+ */
19
+ declare function MultitrackProvider({ config, devtools, breakpoints, children, }: MultitrackProviderProps): react_jsx_runtime.JSX.Element;
20
+
21
+ /**
22
+ * Access the Timeline instance directly.
23
+ */
24
+ declare function useTimeline(): Timeline;
25
+ /**
26
+ * Get current opacities for all steps.
27
+ */
28
+ declare function useOpacities<T extends string = string>(): Opacities<T>;
29
+ /**
30
+ * Get opacity and active state for a single named step.
31
+ */
32
+ declare function useStep(name: string): {
33
+ opacity: number;
34
+ isActive: boolean;
35
+ };
36
+ /**
37
+ * Get raw scroll progress and current step position.
38
+ */
39
+ declare function useScrollProgress(): {
40
+ scrollPercentage: number;
41
+ currentStep: number;
42
+ totalSteps: number;
43
+ };
44
+
45
+ /**
46
+ * Creates the tall scrollable container that drives the timeline.
47
+ * Height = totalSteps * 100vh.
48
+ *
49
+ * Extracted from sinking-china MainMap.tsx line 193:
50
+ * `<div style={{ height: `${totalSteps * 100}vh` }} className="relative">`
51
+ */
52
+ declare function ScrollContainer({ children, className, style, }: {
53
+ children: ReactNode;
54
+ className?: string;
55
+ style?: CSSProperties;
56
+ }): react_jsx_runtime.JSX.Element;
57
+ /**
58
+ * Fixed viewport stage that stays in place while the user scrolls.
59
+ * All animated content lives inside this.
60
+ *
61
+ * Extracted from sinking-china MainMap.tsx lines 200-207:
62
+ * `<div style={{ position: "fixed", width: "100%", height: "100vh", touchAction: "pan-y" }}>`
63
+ */
64
+ declare function FixedStage({ children, className, style, }: {
65
+ children: ReactNode;
66
+ className?: string;
67
+ style?: CSSProperties;
68
+ }): react_jsx_runtime.JSX.Element;
69
+ /**
70
+ * Conditionally renders children based on a step's opacity.
71
+ * Unmounts children when the step is inactive (opacity 0) for performance.
72
+ *
73
+ * Extracted from sinking-china ConditionalRender.tsx.
74
+ */
75
+ declare function Show({ when, children, }: {
76
+ /** Step name to check. Children render when this step's opacity > 0. */
77
+ when: string;
78
+ children: ReactNode;
79
+ }): react_jsx_runtime.JSX.Element | null;
80
+
81
+ export { FixedStage, MultitrackProvider, type MultitrackProviderProps, ScrollContainer, Show, useOpacities, useScrollProgress, useStep, useTimeline };
package/dist/index.js ADDED
@@ -0,0 +1,132 @@
1
+ import { createContext, useRef, useMemo, useState, useEffect, useContext } from 'react';
2
+ import { Timeline } from '@multitrack/core';
3
+ import { jsx, Fragment } from 'react/jsx-runtime';
4
+
5
+ // src/provider.tsx
6
+ var MultitrackContext = createContext(
7
+ null
8
+ );
9
+ function MultitrackProvider({
10
+ config,
11
+ devtools = false,
12
+ breakpoints,
13
+ children
14
+ }) {
15
+ const timelineRef = useRef(null);
16
+ const timeline = useMemo(() => {
17
+ timelineRef.current?.destroy();
18
+ const t = new Timeline({ config, devtools, breakpoints });
19
+ timelineRef.current = t;
20
+ return t;
21
+ }, [config, devtools, breakpoints]);
22
+ const [state, setState] = useState({
23
+ scrollPercentage: 0,
24
+ currentStep: 0,
25
+ opacities: timeline.getOpacities(0)
26
+ });
27
+ useEffect(() => {
28
+ const ctx = timeline.scope(() => {
29
+ timeline.on("scroll", ({ scrollPercentage, currentStep }) => {
30
+ setState({
31
+ scrollPercentage,
32
+ currentStep,
33
+ opacities: timeline.getOpacities(scrollPercentage)
34
+ });
35
+ });
36
+ timeline.on("timeline:reconfigure", () => {
37
+ setState({
38
+ scrollPercentage: timeline.scrollPercentage,
39
+ currentStep: timeline.currentStep,
40
+ opacities: timeline.getOpacities()
41
+ });
42
+ });
43
+ });
44
+ timeline.start();
45
+ return () => {
46
+ ctx.dispose();
47
+ timeline.destroy();
48
+ };
49
+ }, [timeline]);
50
+ const contextValue = {
51
+ timeline,
52
+ steps: timeline.steps,
53
+ totalSteps: timeline.totalSteps,
54
+ scrollPercentage: state.scrollPercentage,
55
+ currentStep: state.currentStep,
56
+ opacities: state.opacities
57
+ };
58
+ return /* @__PURE__ */ jsx(MultitrackContext.Provider, { value: contextValue, children });
59
+ }
60
+ function useMultitrackContext() {
61
+ const ctx = useContext(MultitrackContext);
62
+ if (!ctx) {
63
+ throw new Error(
64
+ "[@multitrack/react] useTimeline must be used within a <MultitrackProvider>"
65
+ );
66
+ }
67
+ return ctx;
68
+ }
69
+ function useTimeline() {
70
+ return useMultitrackContext().timeline;
71
+ }
72
+ function useOpacities() {
73
+ return useMultitrackContext().opacities;
74
+ }
75
+ function useStep(name) {
76
+ const { opacities } = useMultitrackContext();
77
+ const opacity = opacities[name] ?? 0;
78
+ return { opacity, isActive: opacity > 0 };
79
+ }
80
+ function useScrollProgress() {
81
+ const { scrollPercentage, currentStep, totalSteps } = useMultitrackContext();
82
+ return { scrollPercentage, currentStep, totalSteps };
83
+ }
84
+ function ScrollContainer({
85
+ children,
86
+ className,
87
+ style
88
+ }) {
89
+ const { totalSteps } = useScrollProgress();
90
+ return /* @__PURE__ */ jsx(
91
+ "div",
92
+ {
93
+ style: { height: `${totalSteps * 100}vh`, position: "relative", ...style },
94
+ className,
95
+ children
96
+ }
97
+ );
98
+ }
99
+ function FixedStage({
100
+ children,
101
+ className,
102
+ style
103
+ }) {
104
+ return /* @__PURE__ */ jsx(
105
+ "div",
106
+ {
107
+ style: {
108
+ position: "fixed",
109
+ top: 0,
110
+ left: 0,
111
+ width: "100%",
112
+ height: "100vh",
113
+ touchAction: "pan-y",
114
+ ...style
115
+ },
116
+ className,
117
+ children
118
+ }
119
+ );
120
+ }
121
+ function Show({
122
+ when,
123
+ children
124
+ }) {
125
+ const { isActive } = useStep(when);
126
+ if (!isActive) return null;
127
+ return /* @__PURE__ */ jsx(Fragment, { children });
128
+ }
129
+
130
+ export { FixedStage, MultitrackProvider, ScrollContainer, Show, useOpacities, useScrollProgress, useStep, useTimeline };
131
+ //# sourceMappingURL=index.js.map
132
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/context.ts","../src/provider.tsx","../src/hooks.ts","../src/components.tsx"],"names":["jsx"],"mappings":"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","file":"index.js","sourcesContent":["import { createContext } from \"react\";\nimport type { Timeline, Opacities, Step } from \"@multitrack/core\";\n\nexport interface MultitrackContextValue {\n timeline: Timeline;\n steps: Step[];\n totalSteps: number;\n scrollPercentage: number;\n currentStep: number;\n opacities: Opacities;\n}\n\nexport const MultitrackContext = createContext<MultitrackContextValue | null>(\n null,\n);\n","import {\n useRef,\n useState,\n useEffect,\n useMemo,\n type ReactNode,\n} from \"react\";\nimport { Timeline, type StepConfig } from \"@multitrack/core\";\nimport { MultitrackContext, type MultitrackContextValue } from \"./context.js\";\n\nexport interface MultitrackProviderProps {\n /** Step configuration array. */\n config: StepConfig[];\n /** Enable devtools integration. */\n devtools?: boolean;\n /** Named breakpoints for responsive step inclusion. */\n breakpoints?: Record<string, string>;\n children: ReactNode;\n}\n\n/**\n * Provides timeline state to all child components via React context.\n * Manages scroll listening and re-renders children when scroll position changes.\n */\nexport function MultitrackProvider({\n config,\n devtools = false,\n breakpoints,\n children,\n}: MultitrackProviderProps) {\n const timelineRef = useRef<Timeline | null>(null);\n\n // Create timeline instance (stable across renders unless config changes)\n const timeline = useMemo(() => {\n timelineRef.current?.destroy();\n const t = new Timeline({ config, devtools, breakpoints });\n timelineRef.current = t;\n return t;\n }, [config, devtools, breakpoints]);\n\n const [state, setState] = useState({\n scrollPercentage: 0,\n currentStep: 0,\n opacities: timeline.getOpacities(0),\n });\n\n useEffect(() => {\n const ctx = timeline.scope(() => {\n timeline.on(\"scroll\", ({ scrollPercentage, currentStep }) => {\n setState({\n scrollPercentage,\n currentStep,\n opacities: timeline.getOpacities(scrollPercentage),\n });\n });\n\n // Re-render when breakpoints change and steps are reconfigured\n timeline.on(\"timeline:reconfigure\", () => {\n setState({\n scrollPercentage: timeline.scrollPercentage,\n currentStep: timeline.currentStep,\n opacities: timeline.getOpacities(),\n });\n });\n });\n\n timeline.start();\n\n return () => {\n ctx.dispose();\n timeline.destroy();\n };\n }, [timeline]);\n\n const contextValue: MultitrackContextValue = {\n timeline,\n steps: timeline.steps,\n totalSteps: timeline.totalSteps,\n scrollPercentage: state.scrollPercentage,\n currentStep: state.currentStep,\n opacities: state.opacities,\n };\n\n return (\n <MultitrackContext.Provider value={contextValue}>\n {children}\n </MultitrackContext.Provider>\n );\n}\n","import { useContext } from \"react\";\nimport type { Timeline, Opacities } from \"@multitrack/core\";\nimport { MultitrackContext } from \"./context.js\";\n\nfunction useMultitrackContext() {\n const ctx = useContext(MultitrackContext);\n if (!ctx) {\n throw new Error(\n \"[@multitrack/react] useTimeline must be used within a <MultitrackProvider>\",\n );\n }\n return ctx;\n}\n\n/**\n * Access the Timeline instance directly.\n */\nexport function useTimeline(): Timeline {\n return useMultitrackContext().timeline;\n}\n\n/**\n * Get current opacities for all steps.\n */\nexport function useOpacities<T extends string = string>(): Opacities<T> {\n return useMultitrackContext().opacities as Opacities<T>;\n}\n\n/**\n * Get opacity and active state for a single named step.\n */\nexport function useStep(name: string): { opacity: number; isActive: boolean } {\n const { opacities } = useMultitrackContext();\n const opacity = opacities[name] ?? 0;\n return { opacity, isActive: opacity > 0 };\n}\n\n/**\n * Get raw scroll progress and current step position.\n */\nexport function useScrollProgress(): {\n scrollPercentage: number;\n currentStep: number;\n totalSteps: number;\n} {\n const { scrollPercentage, currentStep, totalSteps } =\n useMultitrackContext();\n return { scrollPercentage, currentStep, totalSteps };\n}\n","import type { ReactNode, CSSProperties } from \"react\";\nimport { useScrollProgress, useStep } from \"./hooks.js\";\n\n/**\n * Creates the tall scrollable container that drives the timeline.\n * Height = totalSteps * 100vh.\n *\n * Extracted from sinking-china MainMap.tsx line 193:\n * `<div style={{ height: `${totalSteps * 100}vh` }} className=\"relative\">`\n */\nexport function ScrollContainer({\n children,\n className,\n style,\n}: {\n children: ReactNode;\n className?: string;\n style?: CSSProperties;\n}) {\n const { totalSteps } = useScrollProgress();\n\n return (\n <div\n style={{ height: `${totalSteps * 100}vh`, position: \"relative\", ...style }}\n className={className}\n >\n {children}\n </div>\n );\n}\n\n/**\n * Fixed viewport stage that stays in place while the user scrolls.\n * All animated content lives inside this.\n *\n * Extracted from sinking-china MainMap.tsx lines 200-207:\n * `<div style={{ position: \"fixed\", width: \"100%\", height: \"100vh\", touchAction: \"pan-y\" }}>`\n */\nexport function FixedStage({\n children,\n className,\n style,\n}: {\n children: ReactNode;\n className?: string;\n style?: CSSProperties;\n}) {\n return (\n <div\n style={{\n position: \"fixed\",\n top: 0,\n left: 0,\n width: \"100%\",\n height: \"100vh\",\n touchAction: \"pan-y\",\n ...style,\n }}\n className={className}\n >\n {children}\n </div>\n );\n}\n\n/**\n * Conditionally renders children based on a step's opacity.\n * Unmounts children when the step is inactive (opacity 0) for performance.\n *\n * Extracted from sinking-china ConditionalRender.tsx.\n */\nexport function Show({\n when,\n children,\n}: {\n /** Step name to check. Children render when this step's opacity > 0. */\n when: string;\n children: ReactNode;\n}) {\n const { isActive } = useStep(when);\n if (!isActive) return null;\n return <>{children}</>;\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@multitrack/react",
3
+ "version": "0.1.0",
4
+ "description": "React bindings for @multitrack/core",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "peerDependencies": {
25
+ "react": ">=18.0.0",
26
+ "react-dom": ">=18.0.0"
27
+ },
28
+ "dependencies": {
29
+ "@multitrack/core": "^0.1.0"
30
+ },
31
+ "sideEffects": false,
32
+ "license": "MIT",
33
+ "author": "Jack Hsu",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/jakhsu/multitrack.git",
37
+ "directory": "packages/react"
38
+ },
39
+ "homepage": "https://github.com/jakhsu/multitrack/tree/main/packages/react",
40
+ "bugs": {
41
+ "url": "https://github.com/jakhsu/multitrack/issues"
42
+ },
43
+ "keywords": [
44
+ "scroll-animation",
45
+ "scroll-driven",
46
+ "timeline",
47
+ "react",
48
+ "react-hooks",
49
+ "scrollytelling",
50
+ "multi-track"
51
+ ],
52
+ "publishConfig": {
53
+ "access": "public"
54
+ },
55
+ "engines": {
56
+ "node": ">=18"
57
+ },
58
+ "scripts": {
59
+ "build": "tsup",
60
+ "dev": "tsup --watch"
61
+ }
62
+ }