@codellyson/framely 0.1.0 → 0.1.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.
Files changed (56) hide show
  1. package/package.json +3 -2
  2. package/src/AbsoluteFill.tsx +50 -0
  3. package/src/Audio.tsx +294 -0
  4. package/src/Composition.tsx +378 -0
  5. package/src/Easing.ts +294 -0
  6. package/src/ErrorBoundary.tsx +136 -0
  7. package/src/Folder.tsx +66 -0
  8. package/src/Freeze.tsx +63 -0
  9. package/src/IFrame.tsx +100 -0
  10. package/src/Img.tsx +146 -0
  11. package/src/Loop.tsx +139 -0
  12. package/src/Player.tsx +594 -0
  13. package/src/Sequence.tsx +80 -0
  14. package/src/Series.tsx +181 -0
  15. package/src/Text.tsx +376 -0
  16. package/src/Video.tsx +247 -0
  17. package/src/__tests__/Easing.test.js +119 -0
  18. package/src/__tests__/interpolate.test.js +127 -0
  19. package/src/config.ts +406 -0
  20. package/src/context.tsx +241 -0
  21. package/src/delayRender.ts +278 -0
  22. package/src/getInputProps.ts +217 -0
  23. package/src/hooks/useDelayRender.ts +117 -0
  24. package/src/hooks.ts +28 -0
  25. package/src/index.d.ts +571 -0
  26. package/src/index.ts +260 -0
  27. package/src/interpolate.ts +160 -0
  28. package/src/interpolateColors.ts +368 -0
  29. package/src/makeTransform.ts +339 -0
  30. package/src/measureSpring.ts +152 -0
  31. package/src/noise.ts +308 -0
  32. package/src/preload.ts +303 -0
  33. package/src/registerRoot.ts +346 -0
  34. package/src/shapes/Circle.tsx +37 -0
  35. package/src/shapes/Ellipse.tsx +39 -0
  36. package/src/shapes/Line.tsx +37 -0
  37. package/src/shapes/Path.tsx +56 -0
  38. package/src/shapes/Polygon.tsx +39 -0
  39. package/src/shapes/Rect.tsx +43 -0
  40. package/src/shapes/Svg.tsx +39 -0
  41. package/src/shapes/index.ts +16 -0
  42. package/src/shapes/usePathLength.ts +38 -0
  43. package/src/staticFile.ts +117 -0
  44. package/src/templates/api.ts +165 -0
  45. package/src/templates/index.ts +7 -0
  46. package/src/templates/mockData.ts +271 -0
  47. package/src/templates/types.ts +126 -0
  48. package/src/transitions/TransitionSeries.tsx +399 -0
  49. package/src/transitions/index.ts +109 -0
  50. package/src/transitions/presets/fade.ts +89 -0
  51. package/src/transitions/presets/flip.ts +263 -0
  52. package/src/transitions/presets/slide.ts +154 -0
  53. package/src/transitions/presets/wipe.ts +195 -0
  54. package/src/transitions/presets/zoom.ts +183 -0
  55. package/src/useAudioData.ts +260 -0
  56. package/src/useSpring.ts +215 -0
@@ -0,0 +1,136 @@
1
+ import { Component, ReactNode, ErrorInfo } from 'react';
2
+
3
+ /**
4
+ * Error boundary for catching render errors in compositions.
5
+ *
6
+ * In render mode, sets window.__FRAMELY_RENDER_ERROR so the CLI
7
+ * can detect and report the error.
8
+ *
9
+ * In dev/studio mode, displays the error with a stack trace.
10
+ */
11
+
12
+ declare global {
13
+ interface Window {
14
+ __FRAMELY_RENDER_ERROR?: {
15
+ message: string;
16
+ stack?: string;
17
+ componentStack?: string | null;
18
+ };
19
+ }
20
+ }
21
+
22
+ export interface ErrorBoundaryProps {
23
+ children: ReactNode;
24
+ fallback?: ReactNode;
25
+ onError?: (error: Error, errorInfo: ErrorInfo) => void;
26
+ }
27
+
28
+ export interface ErrorBoundaryState {
29
+ hasError: boolean;
30
+ error: Error | null;
31
+ }
32
+
33
+ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
34
+ constructor(props: ErrorBoundaryProps) {
35
+ super(props);
36
+ this.state = { hasError: false, error: null };
37
+ }
38
+
39
+ static getDerivedStateFromError(error: Error): ErrorBoundaryState {
40
+ return { hasError: true, error };
41
+ }
42
+
43
+ componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
44
+ // In render mode, expose the error for CLI detection
45
+ if (typeof window !== 'undefined') {
46
+ window.__FRAMELY_RENDER_ERROR = {
47
+ message: error.message,
48
+ stack: error.stack,
49
+ componentStack: errorInfo?.componentStack,
50
+ };
51
+
52
+ // Dispatch custom event for render pipeline
53
+ window.dispatchEvent(
54
+ new CustomEvent('framely-render-error', {
55
+ detail: { error, errorInfo },
56
+ }),
57
+ );
58
+ }
59
+
60
+ if (this.props.onError) {
61
+ this.props.onError(error, errorInfo);
62
+ }
63
+ }
64
+
65
+ render(): ReactNode {
66
+ if (this.state.hasError) {
67
+ // Custom fallback UI
68
+ if (this.props.fallback) {
69
+ return this.props.fallback;
70
+ }
71
+
72
+ // In render mode, show nothing (error is on window)
73
+ const params = new URLSearchParams(
74
+ typeof window !== 'undefined' ? window.location.search : '',
75
+ );
76
+ if (params.get('renderMode') === 'true') {
77
+ return null;
78
+ }
79
+
80
+ // Dev/studio mode - show error details
81
+ return (
82
+ <div
83
+ style={{
84
+ position: 'absolute',
85
+ inset: 0,
86
+ display: 'flex',
87
+ flexDirection: 'column',
88
+ alignItems: 'center',
89
+ justifyContent: 'center',
90
+ background: '#1a1a1a',
91
+ color: '#ef4444',
92
+ fontFamily: 'system-ui, sans-serif',
93
+ padding: '24px',
94
+ }}
95
+ >
96
+ <div style={{ fontSize: '18px', fontWeight: 600, marginBottom: '12px' }}>
97
+ Composition Error
98
+ </div>
99
+ <div
100
+ style={{
101
+ fontSize: '14px',
102
+ color: '#fff',
103
+ marginBottom: '16px',
104
+ textAlign: 'center',
105
+ maxWidth: '600px',
106
+ }}
107
+ >
108
+ {this.state.error?.message || 'An unknown error occurred'}
109
+ </div>
110
+ {this.state.error?.stack && (
111
+ <pre
112
+ style={{
113
+ fontSize: '11px',
114
+ color: '#888',
115
+ background: '#111',
116
+ padding: '12px',
117
+ borderRadius: '6px',
118
+ maxWidth: '80%',
119
+ maxHeight: '200px',
120
+ overflow: 'auto',
121
+ whiteSpace: 'pre-wrap',
122
+ wordBreak: 'break-word',
123
+ }}
124
+ >
125
+ {this.state.error.stack}
126
+ </pre>
127
+ )}
128
+ </div>
129
+ );
130
+ }
131
+
132
+ return this.props.children;
133
+ }
134
+ }
135
+
136
+ export default ErrorBoundary;
package/src/Folder.tsx ADDED
@@ -0,0 +1,66 @@
1
+ import { createContext, useContext, type ReactNode } from 'react';
2
+
3
+ /**
4
+ * The folder path represented as an array of folder names from root to current.
5
+ */
6
+ type FolderPath = string[];
7
+
8
+ /**
9
+ * Props for the Folder component.
10
+ */
11
+ interface FolderProps {
12
+ /** The folder name displayed in the sidebar */
13
+ name: string;
14
+ /** Child elements (Compositions or nested Folders) */
15
+ children: ReactNode;
16
+ }
17
+
18
+ /**
19
+ * Context for tracking folder hierarchy
20
+ */
21
+ const FolderContext = createContext<FolderPath>([]);
22
+
23
+ /**
24
+ * Hook to access the current folder path
25
+ *
26
+ * Returns:
27
+ * string[] - Array of folder names from root to current
28
+ *
29
+ * Usage:
30
+ * const path = useFolder();
31
+ * console.log(path.join(' / ')); // "Effects / Transitions"
32
+ */
33
+ export function useFolder(): FolderPath {
34
+ return useContext(FolderContext);
35
+ }
36
+
37
+ /**
38
+ * Folder organizes compositions in the Studio sidebar.
39
+ *
40
+ * Folders can be nested to create hierarchical organization.
41
+ * They don't affect rendering — they're purely for organization in the UI.
42
+ *
43
+ * Props:
44
+ * name - The folder name displayed in the sidebar
45
+ *
46
+ * Usage:
47
+ * <Folder name="Marketing">
48
+ * <Composition id="promo-video" ... />
49
+ * <Folder name="Social">
50
+ * <Composition id="instagram-reel" ... />
51
+ * <Composition id="tiktok-clip" ... />
52
+ * </Folder>
53
+ * </Folder>
54
+ */
55
+ export function Folder({ name, children }: FolderProps): JSX.Element {
56
+ const parentPath: FolderPath = useContext(FolderContext);
57
+ const currentPath: FolderPath = [...parentPath, name];
58
+
59
+ return (
60
+ <FolderContext.Provider value={currentPath}>
61
+ {children}
62
+ </FolderContext.Provider>
63
+ );
64
+ }
65
+
66
+ export default Folder;
package/src/Freeze.tsx ADDED
@@ -0,0 +1,63 @@
1
+ import { useMemo, type ReactNode } from 'react';
2
+ import TimelineContext, { useTimeline } from './context';
3
+
4
+ /**
5
+ * Props for the Freeze component.
6
+ */
7
+ export interface FreezeProps {
8
+ /** The frame number children should see (required). */
9
+ frame: number;
10
+ /** Whether to freeze (default: true). If false, children see the normal frame. */
11
+ active?: boolean;
12
+ /** The content to render inside the freeze boundary. */
13
+ children: ReactNode;
14
+ }
15
+
16
+ /**
17
+ * Freeze pauses children at a specific frame.
18
+ *
19
+ * Children inside a Freeze component will always see the same frame value,
20
+ * regardless of the actual timeline position. This is useful for:
21
+ * - Pausing complex animations at a specific state
22
+ * - Creating "freeze frame" effects
23
+ * - Displaying a static frame from a video or animation
24
+ *
25
+ * Usage:
26
+ * // Freeze at frame 15
27
+ * <Freeze frame={15}>
28
+ * <MyAnimation />
29
+ * </Freeze>
30
+ *
31
+ * // Conditional freeze
32
+ * <Freeze frame={30} active={shouldFreeze}>
33
+ * <MyAnimation />
34
+ * </Freeze>
35
+ */
36
+ export function Freeze({ frame, active = true, children }: FreezeProps): ReactNode {
37
+ const parent = useTimeline();
38
+
39
+ if (frame === undefined) {
40
+ throw new Error('Freeze requires a frame prop');
41
+ }
42
+
43
+ const contextValue = useMemo(
44
+ () => ({
45
+ ...parent,
46
+ frame: active ? frame : parent.frame,
47
+ }),
48
+ [parent, frame, active]
49
+ );
50
+
51
+ // If not active, just render children without context override
52
+ if (!active) {
53
+ return children;
54
+ }
55
+
56
+ return (
57
+ <TimelineContext.Provider value={contextValue}>
58
+ {children}
59
+ </TimelineContext.Provider>
60
+ );
61
+ }
62
+
63
+ export default Freeze;
package/src/IFrame.tsx ADDED
@@ -0,0 +1,100 @@
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import { delayRender, continueRender, cancelRender } from './delayRender';
3
+
4
+ /**
5
+ * Props for the IFrame component.
6
+ *
7
+ * Extends standard iframe HTML attributes so all native iframe props
8
+ * (e.g., sandbox, allow, referrerPolicy) are supported.
9
+ */
10
+ export interface IFrameProps
11
+ extends React.IframeHTMLAttributes<HTMLIFrameElement> {
12
+ /** URL to load in the iframe. */
13
+ src?: string;
14
+ /** Timeout in ms before failing the delay render. Defaults to 30000. */
15
+ delayRenderTimeout?: number;
16
+ }
17
+
18
+ /**
19
+ * IFrame component that delays rendering until the iframe content is loaded.
20
+ *
21
+ * Useful for embedding web content, charts, maps, or other external content
22
+ * that needs to be fully loaded before capturing the frame.
23
+ *
24
+ * Usage:
25
+ * <IFrame
26
+ * src="https://example.com/chart"
27
+ * style={{ width: 800, height: 600 }}
28
+ * />
29
+ */
30
+ export function IFrame({
31
+ src,
32
+ delayRenderTimeout = 30000,
33
+ onLoad,
34
+ onError,
35
+ style,
36
+ className,
37
+ ...rest
38
+ }: IFrameProps): React.ReactElement {
39
+ const iframeRef = useRef<HTMLIFrameElement>(null);
40
+ const handleRef = useRef<number | null>(null);
41
+ const [loaded, setLoaded] = useState<boolean>(false);
42
+
43
+ // Delay render until iframe is loaded
44
+ useEffect(() => {
45
+ handleRef.current = delayRender(`Loading iframe: ${src}`, {
46
+ timeoutInMilliseconds: delayRenderTimeout,
47
+ });
48
+
49
+ return () => {
50
+ if (handleRef.current !== null) {
51
+ continueRender(handleRef.current);
52
+ handleRef.current = null;
53
+ }
54
+ };
55
+ }, [src, delayRenderTimeout]);
56
+
57
+ // Handle iframe load
58
+ const handleLoad = (
59
+ event: React.SyntheticEvent<HTMLIFrameElement>,
60
+ ): void => {
61
+ setLoaded(true);
62
+ onLoad?.(event);
63
+
64
+ if (handleRef.current !== null) {
65
+ continueRender(handleRef.current);
66
+ handleRef.current = null;
67
+ }
68
+ };
69
+
70
+ // Handle iframe error
71
+ const handleError = (
72
+ event: React.SyntheticEvent<HTMLIFrameElement>,
73
+ ): void => {
74
+ const err = new Error(`Failed to load iframe: ${src}`);
75
+ onError?.(event);
76
+
77
+ if (handleRef.current !== null) {
78
+ cancelRender(err);
79
+ handleRef.current = null;
80
+ }
81
+ };
82
+
83
+ return (
84
+ <iframe
85
+ ref={iframeRef}
86
+ src={src}
87
+ onLoad={handleLoad}
88
+ onError={handleError}
89
+ style={{
90
+ border: 'none',
91
+ ...style,
92
+ opacity: loaded ? (style?.opacity ?? 1) : 0,
93
+ }}
94
+ className={className}
95
+ {...rest}
96
+ />
97
+ );
98
+ }
99
+
100
+ export default IFrame;
package/src/Img.tsx ADDED
@@ -0,0 +1,146 @@
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import { delayRender, continueRender, cancelRender } from './delayRender';
3
+
4
+ /**
5
+ * Props for the Img component.
6
+ *
7
+ * Extends standard HTML img attributes with additional props
8
+ * for controlling image loading behavior during rendering.
9
+ */
10
+ export interface ImgProps extends React.ImgHTMLAttributes<HTMLImageElement> {
11
+ /** Image source URL. Use staticFile() for local assets. */
12
+ src: string;
13
+ /** Callback when the image fails to load after all retries. */
14
+ onError?: React.ReactEventHandler<HTMLImageElement>;
15
+ /** Number of retry attempts on load failure. Defaults to 2. */
16
+ maxRetries?: number;
17
+ /** Pause playback while loading in preview mode. Defaults to false. */
18
+ pauseWhenLoading?: boolean;
19
+ /** Custom timeout in milliseconds for delayRender. Defaults to 30000. */
20
+ delayRenderTimeout?: number;
21
+ /** Number of retries for delay render timeout. Defaults to 0. */
22
+ delayRenderRetries?: number;
23
+ }
24
+
25
+ /**
26
+ * Image component that delays rendering until the image is loaded.
27
+ *
28
+ * This ensures that images are always visible in rendered frames,
29
+ * preventing blank spaces or loading states in the final video.
30
+ *
31
+ * Usage:
32
+ * import { Img, staticFile } from './lib';
33
+ *
34
+ * <Img src={staticFile('images/hero.png')} alt="Hero" />
35
+ * <Img src="https://example.com/image.jpg" style={{ width: 200 }} />
36
+ */
37
+ export const Img = React.forwardRef<HTMLImageElement, ImgProps>(
38
+ (
39
+ {
40
+ src,
41
+ onError,
42
+ maxRetries = 2,
43
+ pauseWhenLoading = false,
44
+ delayRenderTimeout = 30000,
45
+ delayRenderRetries = 0,
46
+ ...rest
47
+ },
48
+ forwardedRef,
49
+ ) => {
50
+ const [loaded, setLoaded] = useState<boolean>(false);
51
+ const [error, setError] = useState<Error | null>(null);
52
+ const [retryCount, setRetryCount] = useState<number>(0);
53
+ const handleRef = useRef<number | null>(null);
54
+ const imgRef = useRef<HTMLImageElement | null>(null);
55
+
56
+ // Merge forwarded ref and internal ref
57
+ const setRefs = React.useCallback(
58
+ (node: HTMLImageElement | null) => {
59
+ imgRef.current = node;
60
+ if (typeof forwardedRef === 'function') {
61
+ forwardedRef(node);
62
+ } else if (forwardedRef) {
63
+ (forwardedRef as React.MutableRefObject<HTMLImageElement | null>).current = node;
64
+ }
65
+ },
66
+ [forwardedRef],
67
+ );
68
+
69
+ // Initialize delay render on mount
70
+ useEffect(() => {
71
+ handleRef.current = delayRender(`Loading image: ${src}`, {
72
+ timeoutInMilliseconds: delayRenderTimeout,
73
+ retries: delayRenderRetries,
74
+ });
75
+
76
+ return () => {
77
+ // Cleanup if component unmounts before load
78
+ if (handleRef.current !== null) {
79
+ continueRender(handleRef.current);
80
+ handleRef.current = null;
81
+ }
82
+ };
83
+ }, [src, delayRenderTimeout, delayRenderRetries]);
84
+
85
+ // Handle image load
86
+ const handleLoad = (): void => {
87
+ setLoaded(true);
88
+ setError(null);
89
+
90
+ if (handleRef.current !== null) {
91
+ continueRender(handleRef.current);
92
+ handleRef.current = null;
93
+ }
94
+ };
95
+
96
+ // Handle image error
97
+ const handleError = (event: React.SyntheticEvent<HTMLImageElement, Event>): void => {
98
+ if (retryCount < maxRetries) {
99
+ // Retry by forcing a new request
100
+ setRetryCount((c) => c + 1);
101
+ if (imgRef.current) {
102
+ // Add cache-busting query param
103
+ const separator = src.includes('?') ? '&' : '?';
104
+ imgRef.current.src = `${src}${separator}_retry=${retryCount + 1}`;
105
+ }
106
+ return;
107
+ }
108
+
109
+ const err = new Error(`Failed to load image: ${src}`);
110
+ setError(err);
111
+ onError?.(event);
112
+
113
+ // Cancel the render - image is required
114
+ if (handleRef.current !== null) {
115
+ cancelRender(err);
116
+ handleRef.current = null;
117
+ }
118
+ };
119
+
120
+ // Check if image is already cached (loads synchronously)
121
+ useEffect(() => {
122
+ if (imgRef.current?.complete && imgRef.current?.naturalWidth > 0) {
123
+ handleLoad();
124
+ }
125
+ }, [src]);
126
+
127
+ return (
128
+ <img
129
+ ref={setRefs}
130
+ src={src}
131
+ onLoad={handleLoad}
132
+ onError={handleError}
133
+ {...rest}
134
+ // Hide image until loaded to prevent flash of broken image
135
+ style={{
136
+ ...rest.style,
137
+ opacity: loaded ? (rest.style?.opacity ?? 1) : 0,
138
+ }}
139
+ />
140
+ );
141
+ },
142
+ );
143
+
144
+ Img.displayName = 'Img';
145
+
146
+ export default Img;
package/src/Loop.tsx ADDED
@@ -0,0 +1,139 @@
1
+ import React, { useMemo, createContext, useContext, CSSProperties, ReactNode } from 'react';
2
+ import TimelineContext, { useTimeline } from './context';
3
+
4
+ /**
5
+ * Metadata exposed by the Loop component via LoopContext.
6
+ */
7
+ export interface LoopContextValue {
8
+ /** Current iteration index (0-based) */
9
+ iteration: number;
10
+ /** Duration of each loop iteration in frames */
11
+ durationInFrames: number;
12
+ }
13
+
14
+ /**
15
+ * Props for the Loop component.
16
+ */
17
+ export interface LoopProps {
18
+ /** Number of frames per iteration (required, must be positive) */
19
+ durationInFrames: number;
20
+ /** Number of repetitions (default: Infinity) */
21
+ times?: number;
22
+ /** Layout mode: 'absolute-fill' applies absolute positioning, 'none' applies no layout */
23
+ layout?: 'absolute-fill' | 'none';
24
+ /** Additional CSS styles applied to the container div */
25
+ style?: CSSProperties;
26
+ /** Optional label for debugging */
27
+ name?: string;
28
+ /** Child elements rendered inside the loop */
29
+ children: ReactNode;
30
+ }
31
+
32
+ /**
33
+ * Context for accessing loop metadata
34
+ */
35
+ const LoopContext = createContext<LoopContextValue | null>(null);
36
+
37
+ /**
38
+ * Hook to access loop metadata when inside a Loop component.
39
+ *
40
+ * Returns:
41
+ * { iteration, durationInFrames } - Current iteration (0-indexed) and loop duration
42
+ * null - If not inside a Loop
43
+ *
44
+ * Usage:
45
+ * const loop = Loop.useLoop();
46
+ * if (loop) {
47
+ * console.log(`Iteration ${loop.iteration + 1}`);
48
+ * }
49
+ */
50
+ function useLoop(): LoopContextValue | null {
51
+ return useContext(LoopContext);
52
+ }
53
+
54
+ /**
55
+ * Loop repeats its children for a specified number of iterations.
56
+ *
57
+ * The frame counter resets at the start of each iteration, so children
58
+ * always see frames 0 to durationInFrames-1.
59
+ *
60
+ * Props:
61
+ * durationInFrames - Frames per iteration (required)
62
+ * times - Number of repetitions (default: Infinity)
63
+ * layout - 'absolute-fill' (default) or 'none'
64
+ * style - Additional CSS styles
65
+ * name - Optional label for debugging
66
+ *
67
+ * Usage:
68
+ * <Loop durationInFrames={30} times={3}>
69
+ * <PulseAnimation />
70
+ * </Loop>
71
+ */
72
+ export function Loop({
73
+ durationInFrames,
74
+ times = Infinity,
75
+ layout = 'absolute-fill',
76
+ style,
77
+ name,
78
+ children,
79
+ }: LoopProps): React.ReactElement | null {
80
+ const parent = useTimeline();
81
+
82
+ if (durationInFrames === undefined || durationInFrames <= 0) {
83
+ throw new Error('Loop requires a positive durationInFrames');
84
+ }
85
+
86
+ // Calculate which iteration we're on and the local frame within that iteration
87
+ const iteration: number = Math.floor(parent.frame / durationInFrames);
88
+ const localFrame: number = parent.frame % durationInFrames;
89
+
90
+ const contextValue = useMemo(
91
+ () => ({
92
+ ...parent,
93
+ frame: localFrame,
94
+ durationInFrames,
95
+ }),
96
+ [parent, localFrame, durationInFrames]
97
+ );
98
+
99
+ const loopValue: LoopContextValue = useMemo(
100
+ () => ({
101
+ iteration,
102
+ durationInFrames,
103
+ }),
104
+ [iteration, durationInFrames]
105
+ );
106
+
107
+ const containerStyle: CSSProperties | undefined =
108
+ layout === 'absolute-fill'
109
+ ? {
110
+ position: 'absolute' as const,
111
+ top: 0,
112
+ left: 0,
113
+ right: 0,
114
+ bottom: 0,
115
+ ...style,
116
+ }
117
+ : style;
118
+
119
+ // Check if we've exceeded the number of loops
120
+ if (times !== Infinity && iteration >= times) {
121
+ return null;
122
+ }
123
+
124
+ return (
125
+ <LoopContext.Provider value={loopValue}>
126
+ <TimelineContext.Provider value={contextValue}>
127
+ <div style={containerStyle} data-loop-name={name} data-loop-iteration={iteration}>
128
+ {children}
129
+ </div>
130
+ </TimelineContext.Provider>
131
+ </LoopContext.Provider>
132
+ );
133
+ }
134
+
135
+ // Attach the hook to the Loop component for convenient access
136
+ Loop.useLoop = useLoop;
137
+
138
+ export { useLoop };
139
+ export default Loop;