image-map-route 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.
Files changed (59) hide show
  1. package/README.md +164 -0
  2. package/dist/hooks/useBoundingClientRect.d.ts +3 -0
  3. package/dist/hooks/useBoundingClientRect.d.ts.map +1 -0
  4. package/dist/hooks/useBoundingClientRect.js +12 -0
  5. package/dist/hooks/useControlledState.d.ts +4 -0
  6. package/dist/hooks/useControlledState.d.ts.map +1 -0
  7. package/dist/hooks/useControlledState.js +7 -0
  8. package/dist/hooks/useResizeObserver.d.ts +3 -0
  9. package/dist/hooks/useResizeObserver.d.ts.map +1 -0
  10. package/dist/hooks/useResizeObserver.js +14 -0
  11. package/dist/imageMapRoute.d.ts +3 -0
  12. package/dist/imageMapRoute.d.ts.map +1 -0
  13. package/dist/imageMapRoute.js +76 -0
  14. package/dist/imageMapRouteContainer.d.ts +22 -0
  15. package/dist/imageMapRouteContainer.d.ts.map +1 -0
  16. package/dist/imageMapRouteContainer.js +166 -0
  17. package/dist/imageMapRoutePaths.d.ts +9 -0
  18. package/dist/imageMapRoutePaths.d.ts.map +1 -0
  19. package/dist/imageMapRoutePaths.js +18 -0
  20. package/dist/imageMapRoutePoints.d.ts +11 -0
  21. package/dist/imageMapRoutePoints.d.ts.map +1 -0
  22. package/dist/imageMapRoutePoints.js +8 -0
  23. package/dist/index.d.ts +5 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +4 -0
  26. package/dist/types.d.ts +65 -0
  27. package/dist/types.d.ts.map +1 -0
  28. package/dist/types.js +1 -0
  29. package/dist/useRouteVideoSync.d.ts +10 -0
  30. package/dist/useRouteVideoSync.d.ts.map +1 -0
  31. package/dist/useRouteVideoSync.js +61 -0
  32. package/dist/utils/calculateCenterZoom.d.ts +9 -0
  33. package/dist/utils/calculateCenterZoom.d.ts.map +1 -0
  34. package/dist/utils/calculateCenterZoom.js +13 -0
  35. package/dist/utils/calculateOptimalZoom.d.ts +9 -0
  36. package/dist/utils/calculateOptimalZoom.d.ts.map +1 -0
  37. package/dist/utils/calculateOptimalZoom.js +32 -0
  38. package/dist/utils/clampPosition.d.ts +5 -0
  39. package/dist/utils/clampPosition.d.ts.map +1 -0
  40. package/dist/utils/clampPosition.js +12 -0
  41. package/dist/utils/findSpotByTime.d.ts +3 -0
  42. package/dist/utils/findSpotByTime.d.ts.map +1 -0
  43. package/dist/utils/findSpotByTime.js +62 -0
  44. package/dist/utils/findTimeBySpot.d.ts +3 -0
  45. package/dist/utils/findTimeBySpot.d.ts.map +1 -0
  46. package/dist/utils/findTimeBySpot.js +60 -0
  47. package/dist/utils/getClosestPointOnPath.d.ts +3 -0
  48. package/dist/utils/getClosestPointOnPath.d.ts.map +1 -0
  49. package/dist/utils/getClosestPointOnPath.js +63 -0
  50. package/dist/utils/index.d.ts +5 -0
  51. package/dist/utils/index.d.ts.map +1 -0
  52. package/dist/utils/index.js +4 -0
  53. package/dist/utils/mouseToContainer.d.ts +11 -0
  54. package/dist/utils/mouseToContainer.d.ts.map +1 -0
  55. package/dist/utils/mouseToContainer.js +9 -0
  56. package/dist/utils/offsetToZoom.d.ts +8 -0
  57. package/dist/utils/offsetToZoom.d.ts.map +1 -0
  58. package/dist/utils/offsetToZoom.js +9 -0
  59. package/package.json +34 -0
package/README.md ADDED
@@ -0,0 +1,164 @@
1
+ # image-map-route
2
+
3
+ React utilities for rendering and syncing image map routes.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm i image-map-route
9
+ ```
10
+
11
+ ## Example
12
+
13
+ - Example site: Coming soon.
14
+
15
+ ## Usage
16
+
17
+ ### Basic route rendering
18
+
19
+ ```tsx
20
+ import { ImageRoute, type Point } from 'image-map-route';
21
+
22
+ const points: Point[] = [
23
+ { x: 0.12, y: 0.3, marked: 1, start: 0, end: 1.2 },
24
+ { x: 0.2, y: 0.35, marked: 0 },
25
+ { x: 0.45, y: 0.5, marked: 2 },
26
+ ];
27
+
28
+ export function RoutePreview() {
29
+ return <ImageRoute points={points} />;
30
+ }
31
+ ```
32
+
33
+ ### Sync with video playback
34
+
35
+ ```tsx
36
+ import {
37
+ ImageRoute,
38
+ type Point,
39
+ type Spot,
40
+ useRouteVideoSync,
41
+ } from 'image-map-route';
42
+
43
+ const points: Point[] = [
44
+ { x: 0.12, y: 0.3, start: 0, end: 1.2 },
45
+ { x: 0.2, y: 0.35 },
46
+ { x: 0.45, y: 0.5 },
47
+ ];
48
+
49
+ export function RouteWithVideo() {
50
+ const { routeRef, videoRef, activeSpot, setActiveSpot } = useRouteVideoSync(points);
51
+
52
+ return (
53
+ <div>
54
+ <video ref={videoRef} src="/route.mp4" controls />
55
+ <ImageRoute
56
+ ref={routeRef}
57
+ points={points}
58
+ activeSpot={activeSpot}
59
+ setActiveSpot={setActiveSpot}
60
+ followActiveSpot
61
+ />
62
+ </div>
63
+ );
64
+ }
65
+ ```
66
+
67
+ ## API
68
+
69
+ ### Components
70
+
71
+ #### `ImageRoute`
72
+
73
+ ```ts
74
+ function ImageRoute(props: MapImageRouteProps): JSX.Element;
75
+ ```
76
+
77
+ Main component that renders a scalable route on an image map.
78
+
79
+ **Props (MapImageRouteProps)**
80
+
81
+ - `points: Point[]`
82
+ Route points in normalized coordinates (0..1).
83
+ - `addPoint?: Dispatch<Point>`
84
+ If provided, clicking the route adds a point. Otherwise clicks select a spot.
85
+ - `activeSpot?: Spot` / `setActiveSpot?: Dispatch<Spot>`
86
+ Controlled active spot selection.
87
+ - `followActiveSpot?: boolean`
88
+ Auto-center the view on the active spot.
89
+ - `RenderPoint?: ComponentType<RenderPointProps>`
90
+ Custom point renderer.
91
+ - `RenderPath?: ComponentType<RenderPathProps>`
92
+ Custom path renderer.
93
+ - `RenderExtra?: ComponentType<RenderExtraProps>`
94
+ Custom extra overlay renderer.
95
+ - `deps?: string`
96
+ Trigger recalculation of initial/animated position when changed.
97
+ -
98
+
99
+ `getInitialPosition?: (containerSize: DOMRect) => { scale?: number; offset?: { x: number; y: number } }`
100
+ Initial zoom/offset when the container is ready.
101
+
102
+ -
103
+
104
+ `getAnimatedPosition?: (containerSize: DOMRect) => { scale?: number; offset?: { x: number; y: number } }`
105
+ Optional animated zoom/offset after initial render.
106
+
107
+ - `innerChildren?: ReactNode`
108
+ Children rendered inside the container.
109
+ - `sx?: CSSProperties`
110
+ Inline styles for the container.
111
+
112
+ ### Hooks
113
+
114
+ #### `useRouteVideoSync`
115
+
116
+ ```ts
117
+ function useRouteVideoSync(points: Point[]): {
118
+ routeRef: React.RefObject<HTMLDivElement>;
119
+ videoRef: React.RefObject<HTMLVideoElement>;
120
+ time: number;
121
+ setTime: (time: number) => void;
122
+ activeSpot: Spot | null;
123
+ setActiveSpot: (spot: Spot) => void;
124
+ };
125
+ ```
126
+
127
+ Syncs the map route with an HTML video element. Call `setActiveSpot` to seek the
128
+ video to the corresponding time, or scrub the video to update the active spot.
129
+
130
+ ### Types
131
+
132
+ ```ts
133
+ type Point = {
134
+ x: number;
135
+ y: number;
136
+ marked?: number;
137
+ start?: number;
138
+ end?: number;
139
+ type?: string;
140
+ extra?: string;
141
+ };
142
+
143
+ type Spot = {
144
+ point: { x: number; y: number };
145
+ pointIndex?: number;
146
+ percentage?: number;
147
+ };
148
+ ```
149
+
150
+ ### Utilities
151
+
152
+ ```ts
153
+ import {
154
+ calculateCenterZoom,
155
+ calculateOptimalZoom,
156
+ findSpotByTime,
157
+ findTimeBySpot,
158
+ } from 'image-map-route';
159
+ ```
160
+
161
+ - `calculateCenterZoom(point, containerSize, scale)`
162
+ - `calculateOptimalZoom(points, containerSize, paddingPercent)`
163
+ - `findSpotByTime(points, time)`
164
+ - `findTimeBySpot(points, spot)`
@@ -0,0 +1,3 @@
1
+ import { type RefObject } from 'react';
2
+ export default function useBoundingClientRect(ref: RefObject<HTMLElement>): DOMRect;
3
+ //# sourceMappingURL=useBoundingClientRect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useBoundingClientRect.d.ts","sourceRoot":"","sources":["../../src/hooks/useBoundingClientRect.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAyB,MAAM,OAAO,CAAC;AAI9D,MAAM,CAAC,OAAO,UAAU,qBAAqB,CAAC,GAAG,EAAE,SAAS,CAAC,WAAW,CAAC,GAAG,OAAO,CAYlF"}
@@ -0,0 +1,12 @@
1
+ import { useCallback, useState } from 'react';
2
+ import { useDidMount } from 'rooks';
3
+ import useResizeObserver from './useResizeObserver';
4
+ export default function useBoundingClientRect(ref) {
5
+ const [domRect, setDomRect] = useState(null);
6
+ const update = useCallback(() => {
7
+ setDomRect(ref.current ? ref.current.getBoundingClientRect() : null);
8
+ }, [ref]);
9
+ useDidMount(update);
10
+ useResizeObserver(ref, update);
11
+ return domRect;
12
+ }
@@ -0,0 +1,4 @@
1
+ import { type Dispatch, type SetStateAction } from 'react';
2
+ export default function useControlledState<S>(defaultState?: S): [S, Dispatch<SetStateAction<S>>];
3
+ export default function useControlledState<S>(state: S, setState: Dispatch<SetStateAction<S>>): [S, Dispatch<SetStateAction<S>>];
4
+ //# sourceMappingURL=useControlledState.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useControlledState.d.ts","sourceRoot":"","sources":["../../src/hooks/useControlledState.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAE,KAAK,cAAc,EAAY,MAAM,OAAO,CAAC;AAGrE,MAAM,CAAC,OAAO,UAAU,kBAAkB,CAAC,CAAC,EAAE,YAAY,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAClG,MAAM,CAAC,OAAO,UAAU,kBAAkB,CAAC,CAAC,EAC3C,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,QAAQ,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,GACnC,CAAC,CAAC,EAAE,QAAQ,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC"}
@@ -0,0 +1,7 @@
1
+ import { useState } from 'react';
2
+ export default function useControlledState(state, setState) {
3
+ const controlledState = useState(state);
4
+ if (setState)
5
+ return [state, setState];
6
+ return controlledState;
7
+ }
@@ -0,0 +1,3 @@
1
+ import { type RefObject } from 'react';
2
+ export default function useResizeObserver(ref: RefObject<HTMLElement>, callback: ResizeObserverCallback): void;
3
+ //# sourceMappingURL=useResizeObserver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useResizeObserver.d.ts","sourceRoot":"","sources":["../../src/hooks/useResizeObserver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAqB,MAAM,OAAO,CAAC;AAE1D,MAAM,CAAC,OAAO,UAAU,iBAAiB,CACxC,GAAG,EAAE,SAAS,CAAC,WAAW,CAAC,EAC3B,QAAQ,EAAE,sBAAsB,GAC9B,IAAI,CAcN"}
@@ -0,0 +1,14 @@
1
+ import { useEffect, useRef } from 'react';
2
+ export default function useResizeObserver(ref, callback) {
3
+ const callbackRef = useRef(callback);
4
+ useEffect(() => {
5
+ callbackRef.current = callback;
6
+ }, [callback]);
7
+ useEffect(() => {
8
+ if (ref.current) {
9
+ const observer = new ResizeObserver((...args) => callbackRef.current(...args));
10
+ observer.observe(ref.current);
11
+ return () => observer.disconnect();
12
+ }
13
+ }, [ref]);
14
+ }
@@ -0,0 +1,3 @@
1
+ import { type MapImageRouteProps } from './types';
2
+ export default function ImageMapRoute({ ref, points, addPoint, activeSpot: _activeSpot, setActiveSpot: _setActiveSpot, RenderPoint, RenderPath, RenderExtra, deps, getInitialPosition, getAnimatedPosition, followActiveSpot, sx, children, ...props }: MapImageRouteProps): import("react/jsx-runtime").JSX.Element;
3
+ //# sourceMappingURL=imageMapRoute.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"imageMapRoute.d.ts","sourceRoot":"","sources":["../src/imageMapRoute.tsx"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,kBAAkB,EAAa,MAAM,SAAS,CAAC;AAI7D,MAAM,CAAC,OAAO,UAAU,aAAa,CAAC,EACrC,GAAG,EACH,MAAM,EACN,QAAQ,EACR,UAAU,EAAE,WAAW,EACvB,aAAa,EAAE,cAAc,EAC7B,WAAW,EACX,UAAU,EACV,WAAW,EACX,IAAI,EACJ,kBAAiE,EACjE,mBAAmB,EACnB,gBAAgB,EAChB,EAAE,EACF,QAAQ,EACR,GAAG,KAAK,EACR,EAAE,kBAAkB,2CA6FpB"}
@@ -0,0 +1,76 @@
1
+ var __rest = (this && this.__rest) || function (s, e) {
2
+ var t = {};
3
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
+ t[p] = s[p];
5
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
+ t[p[i]] = s[p[i]];
9
+ }
10
+ return t;
11
+ };
12
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
+ import { useEffect, useRef, useState } from 'react';
14
+ import useBoundingClientRect from './hooks/useBoundingClientRect';
15
+ import useControlledState from './hooks/useControlledState';
16
+ import ImageMapRouteContainer from './imageMapRouteContainer';
17
+ import ImageMapRoutePaths from './imageMapRoutePaths';
18
+ import ImageMapRoutePoints from './imageMapRoutePoints';
19
+ import { calculateCenterZoom } from './utils';
20
+ import { getClosestPointOnPath } from './utils/getClosestPointOnPath';
21
+ export default function ImageMapRoute(_a) {
22
+ var { ref, points, addPoint, activeSpot: _activeSpot, setActiveSpot: _setActiveSpot, RenderPoint, RenderPath, RenderExtra, deps, getInitialPosition = () => ({ scale: 1, offset: { x: 0, y: 0 } }), getAnimatedPosition, followActiveSpot, sx, children } = _a, props = __rest(_a, ["ref", "points", "addPoint", "activeSpot", "setActiveSpot", "RenderPoint", "RenderPath", "RenderExtra", "deps", "getInitialPosition", "getAnimatedPosition", "followActiveSpot", "sx", "children"]);
23
+ const internalRef = useRef(null);
24
+ const containerRef = ref !== null && ref !== void 0 ? ref : internalRef;
25
+ const containerSize = useBoundingClientRect(containerRef);
26
+ const [scale, setScale] = useState(1);
27
+ const [mapOffset, setMapOffset] = useState({ x: 0, y: 0 });
28
+ const [isAnimating, setIsAnimating] = useState(false);
29
+ const [activeSpot, setActiveSpot] = useControlledState(_activeSpot, _setActiveSpot);
30
+ const [hoverSpot, setHoverSpot] = useState(null);
31
+ useEffect(() => {
32
+ if (!points || !(containerSize === null || containerSize === void 0 ? void 0 : containerSize.width) || !(containerSize === null || containerSize === void 0 ? void 0 : containerSize.height))
33
+ return;
34
+ const { scale, offset } = getInitialPosition(containerSize);
35
+ if (scale)
36
+ setScale(scale);
37
+ if (offset)
38
+ setMapOffset(offset);
39
+ setIsAnimating(false);
40
+ if (!getAnimatedPosition)
41
+ return;
42
+ const animationFrame = setTimeout(() => {
43
+ const { scale, offset } = getAnimatedPosition(containerSize);
44
+ if (scale)
45
+ setScale(scale);
46
+ if (offset)
47
+ setMapOffset(offset);
48
+ setIsAnimating(true);
49
+ setTimeout(() => setIsAnimating(false), 2000);
50
+ }, 1000);
51
+ return () => clearTimeout(animationFrame);
52
+ // eslint-disable-next-line react-hooks/exhaustive-deps
53
+ }, [deps, Boolean(points), Boolean(containerSize)]);
54
+ useEffect(() => {
55
+ if (!followActiveSpot ||
56
+ !(activeSpot === null || activeSpot === void 0 ? void 0 : activeSpot.point) ||
57
+ !(containerSize === null || containerSize === void 0 ? void 0 : containerSize.width) ||
58
+ !(containerSize === null || containerSize === void 0 ? void 0 : containerSize.height))
59
+ return;
60
+ requestAnimationFrame(() => {
61
+ const { offset } = calculateCenterZoom(activeSpot.point, containerSize, scale);
62
+ setMapOffset(offset);
63
+ });
64
+ // eslint-disable-next-line react-hooks/exhaustive-deps
65
+ }, [followActiveSpot, activeSpot === null || activeSpot === void 0 ? void 0 : activeSpot.point]);
66
+ return (_jsxs(ImageMapRouteContainer, Object.assign({ containerRef: containerRef, containerSize: containerSize, scale: scale, setScale: setScale, mapOffset: mapOffset, setMapOffset: setMapOffset, isAnimating: isAnimating, onHoverRoute: (point) => {
67
+ if (addPoint)
68
+ return;
69
+ setHoverSpot(getClosestPointOnPath(points, point.x, point.y, 15 / containerSize.width));
70
+ }, onClickRoute: (point) => {
71
+ if (addPoint)
72
+ return addPoint(point);
73
+ if (hoverSpot)
74
+ setActiveSpot(hoverSpot);
75
+ }, sx: sx }, props, { children: [children, containerSize && (_jsxs("svg", { style: { width: '100%', height: '100%' }, children: [_jsx(ImageMapRoutePaths, { containerSize: containerSize, scale: scale, points: points, RenderPath: RenderPath }), _jsx(ImageMapRoutePoints, { containerSize: containerSize, scale: scale, points: points, activeSpot: activeSpot, hoverSpot: hoverSpot, RenderPoint: RenderPoint }), RenderExtra && (_jsx(RenderExtra, { containerSize: containerSize, scale: scale, points: points }))] }))] })));
76
+ }
@@ -0,0 +1,22 @@
1
+ import type { CSSProperties, Dispatch, HTMLAttributes, ReactNode, RefObject } from 'react';
2
+ import { type Point } from './types';
3
+ export default function ImageMapRouteContainer({ containerRef, containerSize, scale, setScale, mapOffset, setMapOffset, isAnimating, onHoverRoute, onClickRoute, sx, children, ...props }: {
4
+ containerRef: RefObject<HTMLDivElement>;
5
+ containerSize: DOMRect;
6
+ scale: number;
7
+ setScale: Dispatch<number>;
8
+ mapOffset: Point;
9
+ setMapOffset: Dispatch<Point>;
10
+ isAnimating: boolean;
11
+ onHoverRoute?: (point: {
12
+ x: number;
13
+ y: number;
14
+ }) => void;
15
+ onClickRoute?: (point: {
16
+ x: number;
17
+ y: number;
18
+ }) => void;
19
+ innerChildren?: ReactNode;
20
+ sx?: CSSProperties;
21
+ } & Omit<HTMLAttributes<HTMLDivElement>, 'ref'>): import("react/jsx-runtime").JSX.Element;
22
+ //# sourceMappingURL=imageMapRouteContainer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"imageMapRouteContainer.d.ts","sourceRoot":"","sources":["../src/imageMapRouteContainer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,aAAa,EACb,QAAQ,EACR,cAAc,EACd,SAAS,EACT,SAAS,EAET,MAAM,OAAO,CAAC;AAGf,OAAO,EAAE,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAIrC,MAAM,CAAC,OAAO,UAAU,sBAAsB,CAAC,EAC9C,YAAY,EACZ,aAAa,EACb,KAAK,EACL,QAAQ,EACR,SAAS,EACT,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,EAAE,EACF,QAAQ,EACR,GAAG,KAAK,EACR,EAAE;IACF,YAAY,EAAE,SAAS,CAAC,cAAc,CAAC,CAAC;IACxC,aAAa,EAAE,OAAO,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC3B,SAAS,EAAE,KAAK,CAAC;IACjB,YAAY,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC9B,WAAW,EAAE,OAAO,CAAC;IACrB,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACzD,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACzD,aAAa,CAAC,EAAE,SAAS,CAAC;IAC1B,EAAE,CAAC,EAAE,aAAa,CAAC;CACnB,GAAG,IAAI,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,KAAK,CAAC,2CAoL9C"}
@@ -0,0 +1,166 @@
1
+ var __rest = (this && this.__rest) || function (s, e) {
2
+ var t = {};
3
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
+ t[p] = s[p];
5
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
+ t[p[i]] = s[p[i]];
9
+ }
10
+ return t;
11
+ };
12
+ import { jsx as _jsx } from "react/jsx-runtime";
13
+ import { useEffect, useRef } from 'react';
14
+ import { clamp } from 'remeda';
15
+ import { clampPosition } from './utils/clampPosition';
16
+ import { mouseToContainer } from './utils/mouseToContainer';
17
+ export default function ImageMapRouteContainer(_a) {
18
+ var { containerRef, containerSize, scale, setScale, mapOffset, setMapOffset, isAnimating, onHoverRoute, onClickRoute, sx, children } = _a, props = __rest(_a, ["containerRef", "containerSize", "scale", "setScale", "mapOffset", "setMapOffset", "isAnimating", "onHoverRoute", "onClickRoute", "sx", "children"]);
19
+ const isDragging = useRef(false);
20
+ const hasMoved = useRef(false);
21
+ const dragStart = useRef({ x: 0, y: 0 });
22
+ const lastTouchDistance = useRef(null);
23
+ const touchStartTime = useRef(0);
24
+ const interactionStartPos = useRef(null);
25
+ const { style } = props, restProps = __rest(props, ["style"]);
26
+ const startDrag = (clientX, clientY) => {
27
+ isDragging.current = true;
28
+ hasMoved.current = false;
29
+ interactionStartPos.current = { x: clientX, y: clientY };
30
+ dragStart.current = { x: clientX - mapOffset.x, y: clientY - mapOffset.y };
31
+ touchStartTime.current = Date.now();
32
+ };
33
+ const performDrag = (clientX, clientY) => {
34
+ if (!containerSize)
35
+ return;
36
+ hasMoved.current = true;
37
+ const newX = clientX - dragStart.current.x;
38
+ const newY = clientY - dragStart.current.y;
39
+ setMapOffset(clampPosition(containerSize, newX, newY, scale));
40
+ };
41
+ const performZoom = (zoomCenter, scaleDelta) => {
42
+ var _a, _b;
43
+ if (!containerSize)
44
+ return;
45
+ const newScale = clamp(scale * scaleDelta, { min: 1, max: 16 }) || 1;
46
+ const liveRect = (_b = (_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect()) !== null && _b !== void 0 ? _b : containerSize;
47
+ const mouseX = zoomCenter.x - liveRect.left;
48
+ const mouseY = zoomCenter.y - liveRect.top;
49
+ const centerX = containerSize.width / 2;
50
+ const centerY = containerSize.height / 2;
51
+ const imagePointX = (mouseX - centerX - mapOffset.x) / scale;
52
+ const imagePointY = (mouseY - centerY - mapOffset.y) / scale;
53
+ const newX = mouseX - centerX - imagePointX * newScale;
54
+ const newY = mouseY - centerY - imagePointY * newScale;
55
+ setScale(newScale);
56
+ setMapOffset(clampPosition(containerSize, newX, newY, newScale));
57
+ };
58
+ const handleClick = (click) => {
59
+ var _a, _b;
60
+ if (!containerSize)
61
+ return;
62
+ const liveRect = (_b = (_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect()) !== null && _b !== void 0 ? _b : containerSize;
63
+ onClickRoute === null || onClickRoute === void 0 ? void 0 : onClickRoute(mouseToContainer({ clientX: click.x, clientY: click.y }, liveRect, mapOffset, scale));
64
+ };
65
+ const endDrag = () => {
66
+ isDragging.current = false;
67
+ lastTouchDistance.current = null;
68
+ interactionStartPos.current = null;
69
+ };
70
+ const getTouchDistance = (touches) => {
71
+ if (touches.length < 2)
72
+ return null;
73
+ const dx = touches[0].clientX - touches[1].clientX;
74
+ const dy = touches[0].clientY - touches[1].clientY;
75
+ return Math.sqrt(dx * dx + dy * dy);
76
+ };
77
+ const getTouchCenter = (touches) => {
78
+ if (touches.length < 2)
79
+ return null;
80
+ return {
81
+ x: (touches[0].clientX + touches[1].clientX) / 2,
82
+ y: (touches[0].clientY + touches[1].clientY) / 2,
83
+ };
84
+ };
85
+ useEffect(() => {
86
+ const element = containerRef.current;
87
+ if (!element)
88
+ return;
89
+ const handleWheel = (e) => {
90
+ e.preventDefault();
91
+ const delta = e.deltaY > 0 ? 0.9 : 1.1;
92
+ performZoom({ x: e.clientX, y: e.clientY }, delta);
93
+ };
94
+ element.addEventListener('wheel', handleWheel, { passive: false });
95
+ return () => element.removeEventListener('wheel', handleWheel);
96
+ // eslint-disable-next-line react-hooks/exhaustive-deps
97
+ }, [containerRef, containerSize, scale, mapOffset]);
98
+ const outerStyle = Object.assign(Object.assign({ overflow: 'hidden', cursor: 'crosshair', touchAction: 'none', backgroundColor: 'black' }, sx), style);
99
+ return (_jsx("div", Object.assign({ ref: containerRef, style: outerStyle, onMouseDown: (e) => {
100
+ if (e.button !== 0 && e.button !== 2)
101
+ return;
102
+ if (e.button === 2)
103
+ e.preventDefault();
104
+ startDrag(e.clientX, e.clientY);
105
+ }, onMouseMove: (e) => {
106
+ if (isDragging.current) {
107
+ performDrag(e.clientX, e.clientY);
108
+ return;
109
+ }
110
+ if (!containerSize)
111
+ return;
112
+ const liveRect = e.currentTarget.getBoundingClientRect();
113
+ onHoverRoute === null || onHoverRoute === void 0 ? void 0 : onHoverRoute(mouseToContainer(e, liveRect, mapOffset, scale));
114
+ }, onMouseUp: endDrag, onClick: (e) => {
115
+ if (hasMoved.current) {
116
+ hasMoved.current = false;
117
+ return;
118
+ }
119
+ if (!containerSize)
120
+ return;
121
+ const liveRect = e.currentTarget.getBoundingClientRect();
122
+ onClickRoute === null || onClickRoute === void 0 ? void 0 : onClickRoute(mouseToContainer(e, liveRect, mapOffset, scale));
123
+ }, onContextMenu: (e) => e.preventDefault(), onTouchStart: (e) => {
124
+ if (e.touches.length === 1) {
125
+ const touch = e.touches[0];
126
+ startDrag(touch.clientX, touch.clientY);
127
+ }
128
+ else if (e.touches.length === 2) {
129
+ lastTouchDistance.current = getTouchDistance(e.touches);
130
+ }
131
+ }, onTouchMove: (e) => {
132
+ e.preventDefault();
133
+ if (e.touches.length === 1) {
134
+ const touch = e.touches[0];
135
+ performDrag(touch.clientX, touch.clientY);
136
+ }
137
+ else if (e.touches.length === 2) {
138
+ const distance = getTouchDistance(e.touches);
139
+ const center = getTouchCenter(e.touches);
140
+ if (distance && center && lastTouchDistance.current) {
141
+ const scaleDelta = distance / lastTouchDistance.current;
142
+ performZoom(center, scaleDelta);
143
+ lastTouchDistance.current = distance;
144
+ }
145
+ }
146
+ }, onTouchEnd: (e) => {
147
+ if (e.touches.length === 0) {
148
+ const touchDuration = Date.now() - touchStartTime.current;
149
+ const wasTap = touchDuration < 200 && !isDragging.current;
150
+ if (wasTap && interactionStartPos.current) {
151
+ handleClick(interactionStartPos.current);
152
+ }
153
+ endDrag();
154
+ }
155
+ else if (e.touches.length === 1) {
156
+ lastTouchDistance.current = null;
157
+ }
158
+ } }, restProps, { children: _jsx("div", { style: {
159
+ position: 'relative',
160
+ transform: `translate(${mapOffset.x}px, ${mapOffset.y}px) scale(${scale})`,
161
+ transformOrigin: '50% 50%',
162
+ width: '100%',
163
+ height: '100%',
164
+ transition: isAnimating ? 'transform 1s ease' : 'none',
165
+ }, children: children }) })));
166
+ }
@@ -0,0 +1,9 @@
1
+ import { type ComponentType } from 'react';
2
+ import { type Point, type RenderPathProps } from './types';
3
+ export default function ImageMapRoutePaths({ containerSize, scale, points, RenderPath, }: {
4
+ containerSize: DOMRect;
5
+ scale: number;
6
+ points: Point[];
7
+ RenderPath?: ComponentType<RenderPathProps>;
8
+ }): import("react/jsx-runtime").JSX.Element;
9
+ //# sourceMappingURL=imageMapRoutePaths.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"imageMapRoutePaths.d.ts","sourceRoot":"","sources":["../src/imageMapRoutePaths.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAY,MAAM,OAAO,CAAC;AACrD,OAAO,EAAE,KAAK,KAAK,EAAE,KAAK,eAAe,EAAE,MAAM,SAAS,CAAC;AAW3D,MAAM,CAAC,OAAO,UAAU,kBAAkB,CAAC,EAC1C,aAAa,EACb,KAAK,EACL,MAAM,EACN,UAA8B,GAC9B,EAAE;IACF,aAAa,EAAE,OAAO,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,UAAU,CAAC,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;CAC5C,2CAmBA"}
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Fragment } from 'react';
3
+ function DefaultRenderPath({ point1, point2, containerSize }) {
4
+ const x1 = point1.x * containerSize.width;
5
+ const y1 = point1.y * containerSize.height;
6
+ const x2 = point2.x * containerSize.width;
7
+ const y2 = point2.y * containerSize.height;
8
+ return _jsx("line", { x1: x1, y1: y1, x2: x2, y2: y2, stroke: 'white' });
9
+ }
10
+ export default function ImageMapRoutePaths({ containerSize, scale, points, RenderPath = DefaultRenderPath, }) {
11
+ var _a;
12
+ return (_jsx(Fragment, { children: (_a = points === null || points === void 0 ? void 0 : points.map) === null || _a === void 0 ? void 0 : _a.call(points, (point, i) => {
13
+ if (i === 0)
14
+ return null;
15
+ const prevPoint = points[i - 1];
16
+ return (_jsx(RenderPath, { point1: prevPoint, point2: point, containerSize: containerSize, scale: scale }, `path-${i}`));
17
+ }) }));
18
+ }
@@ -0,0 +1,11 @@
1
+ import { type ComponentType } from 'react';
2
+ import { type Point, type RenderPointProps, type Spot } from './types';
3
+ export default function ImageMapRoutePoints({ containerSize, scale, points, hoverSpot, activeSpot, RenderPoint, }: {
4
+ containerSize: DOMRect;
5
+ scale: number;
6
+ points: Point[];
7
+ hoverSpot: Spot;
8
+ activeSpot: Spot;
9
+ RenderPoint?: ComponentType<RenderPointProps>;
10
+ }): import("react/jsx-runtime").JSX.Element;
11
+ //# sourceMappingURL=imageMapRoutePoints.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"imageMapRoutePoints.d.ts","sourceRoot":"","sources":["../src/imageMapRoutePoints.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAY,MAAM,OAAO,CAAC;AACrD,OAAO,EAAE,KAAK,KAAK,EAAE,KAAK,gBAAgB,EAAE,KAAK,IAAI,EAAE,MAAM,SAAS,CAAC;AAcvE,MAAM,CAAC,OAAO,UAAU,mBAAmB,CAAC,EAC3C,aAAa,EACb,KAAK,EACL,MAAM,EACN,SAAS,EACT,UAAU,EACV,WAAgC,GAChC,EAAE;IACF,aAAa,EAAE,OAAO,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;IAChB,UAAU,EAAE,IAAI,CAAC;IACjB,WAAW,CAAC,EAAE,aAAa,CAAC,gBAAgB,CAAC,CAAC;CAC9C,2CA+BA"}
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Fragment } from 'react';
3
+ function DefaultRenderPoint({ point, containerSize, type }) {
4
+ return (_jsx("circle", { cx: point.x * containerSize.width, cy: point.y * containerSize.height, r: type ? 2 : 1, fill: type === 'active' ? 'blue' : 'lime', fillOpacity: type ? 0.5 : 1 }));
5
+ }
6
+ export default function ImageMapRoutePoints({ containerSize, scale, points, hoverSpot, activeSpot, RenderPoint = DefaultRenderPoint, }) {
7
+ return (_jsxs(Fragment, { children: [points === null || points === void 0 ? void 0 : points.map((point, i) => (_jsx(RenderPoint, { point: point, containerSize: containerSize, scale: scale }, `point-${i}`))), hoverSpot && (_jsx(RenderPoint, { point: hoverSpot.point, containerSize: containerSize, scale: scale, percentage: hoverSpot.percentage, type: 'hover' })), activeSpot && (_jsx(RenderPoint, { point: activeSpot.point, containerSize: containerSize, scale: scale, percentage: activeSpot.percentage, type: 'active' }))] }));
8
+ }
@@ -0,0 +1,5 @@
1
+ export { default as ImageRoute } from './imageMapRoute';
2
+ export * from './types';
3
+ export * from './utils';
4
+ export { default as useRouteVideoSync } from './useRouteVideoSync';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,iBAAiB,CAAC;AACxD,cAAc,SAAS,CAAC;AACxB,cAAc,SAAS,CAAC;AACxB,OAAO,EAAE,OAAO,IAAI,iBAAiB,EAAE,MAAM,qBAAqB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { default as ImageRoute } from './imageMapRoute';
2
+ export * from './types';
3
+ export * from './utils';
4
+ export { default as useRouteVideoSync } from './useRouteVideoSync';
@@ -0,0 +1,65 @@
1
+ import type { ComponentType, CSSProperties, Dispatch, HTMLAttributes, ReactNode, RefObject } from 'react';
2
+ export type Point = {
3
+ x: number;
4
+ y: number;
5
+ marked?: number;
6
+ start?: number;
7
+ end?: number;
8
+ type?: string;
9
+ extra?: string;
10
+ };
11
+ export type Spot = {
12
+ point: {
13
+ x: number;
14
+ y: number;
15
+ };
16
+ pointIndex?: number;
17
+ percentage?: number;
18
+ };
19
+ export type MapImageRouteProps = {
20
+ ref?: RefObject<HTMLDivElement>;
21
+ points: Point[];
22
+ addPoint?: Dispatch<Point>;
23
+ activeSpot?: Spot;
24
+ setActiveSpot?: Dispatch<Spot>;
25
+ followActiveSpot?: boolean;
26
+ RenderPoint?: ComponentType<RenderPointProps>;
27
+ RenderPath?: ComponentType<RenderPathProps>;
28
+ RenderExtra?: ComponentType<RenderExtraProps>;
29
+ deps?: string;
30
+ getInitialPosition?: (containerSize: DOMRect) => {
31
+ scale?: number;
32
+ offset?: {
33
+ x: number;
34
+ y: number;
35
+ };
36
+ };
37
+ getAnimatedPosition?: (containerSize: DOMRect) => {
38
+ scale?: number;
39
+ offset?: {
40
+ x: number;
41
+ y: number;
42
+ };
43
+ };
44
+ innerChildren?: ReactNode;
45
+ sx?: CSSProperties;
46
+ } & HTMLAttributes<HTMLDivElement>;
47
+ export type RenderPointProps = {
48
+ point: Point;
49
+ containerSize: DOMRect;
50
+ scale: number;
51
+ percentage?: number;
52
+ type?: string;
53
+ };
54
+ export type RenderPathProps = {
55
+ point1: Point;
56
+ point2: Point;
57
+ containerSize: DOMRect;
58
+ scale: number;
59
+ };
60
+ export type RenderExtraProps = {
61
+ containerSize: DOMRect;
62
+ scale: number;
63
+ points: Point[];
64
+ };
65
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,aAAa,EACb,aAAa,EACb,QAAQ,EACR,cAAc,EACd,SAAS,EACT,SAAS,EACT,MAAM,OAAO,CAAC;AAEf,MAAM,MAAM,KAAK,GAAG;IACnB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,IAAI,GAAG;IAClB,KAAK,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAChC,GAAG,CAAC,EAAE,SAAS,CAAC,cAAc,CAAC,CAAC;IAChC,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,QAAQ,CAAC,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC3B,UAAU,CAAC,EAAE,IAAI,CAAC;IAClB,aAAa,CAAC,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC/B,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,WAAW,CAAC,EAAE,aAAa,CAAC,gBAAgB,CAAC,CAAC;IAC9C,UAAU,CAAC,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;IAC5C,WAAW,CAAC,EAAE,aAAa,CAAC,gBAAgB,CAAC,CAAC;IAC9C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kBAAkB,CAAC,EAAE,CAAC,aAAa,EAAE,OAAO,KAAK;QAChD,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE;YAAE,CAAC,EAAE,MAAM,CAAC;YAAC,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;KAClC,CAAC;IACF,mBAAmB,CAAC,EAAE,CAAC,aAAa,EAAE,OAAO,KAAK;QACjD,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE;YAAE,CAAC,EAAE,MAAM,CAAC;YAAC,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;KAClC,CAAC;IACF,aAAa,CAAC,EAAE,SAAS,CAAC;IAC1B,EAAE,CAAC,EAAE,aAAa,CAAC;CACnB,GAAG,cAAc,CAAC,cAAc,CAAC,CAAC;AAEnC,MAAM,MAAM,gBAAgB,GAAG;IAC9B,KAAK,EAAE,KAAK,CAAC;IACb,aAAa,EAAE,OAAO,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC7B,MAAM,EAAE,KAAK,CAAC;IACd,MAAM,EAAE,KAAK,CAAC;IACd,aAAa,EAAE,OAAO,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC9B,aAAa,EAAE,OAAO,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,KAAK,EAAE,CAAC;CAChB,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ import { type Point, type Spot } from './types';
2
+ export default function useRouteVideoSync(points: Point[]): {
3
+ routeRef: import("react").RefObject<HTMLDivElement>;
4
+ videoRef: import("react").RefObject<HTMLVideoElement>;
5
+ time: number;
6
+ setTime: (time: number) => void;
7
+ activeSpot: Spot;
8
+ setActiveSpot: (spot: Spot) => void;
9
+ };
10
+ //# sourceMappingURL=useRouteVideoSync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useRouteVideoSync.d.ts","sourceRoot":"","sources":["../src/useRouteVideoSync.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,KAAK,EAAE,KAAK,IAAI,EAAE,MAAM,SAAS,CAAC;AAGhD,MAAM,CAAC,OAAO,UAAU,iBAAiB,CAAC,MAAM,EAAE,KAAK,EAAE;;;;oBAiDvC,MAAM;;0BAKA,IAAI;EAU3B"}
@@ -0,0 +1,61 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { useVideo } from 'rooks';
3
+ import { findSpotByTime, findTimeBySpot } from './utils';
4
+ export default function useRouteVideoSync(points) {
5
+ const routeRef = useRef(null);
6
+ const [videoRef, videoState, videoControls] = useVideo();
7
+ const animationFrameRef = useRef(null);
8
+ const [time, setTime] = useState(0);
9
+ const [activeSpot, setActiveSpot] = useState(null);
10
+ // Sync activeSpot with time
11
+ useEffect(() => {
12
+ const spot = findSpotByTime(points, time);
13
+ if (!spot)
14
+ return;
15
+ setActiveSpot(spot);
16
+ }, [points, time]);
17
+ useEffect(() => {
18
+ videoControls.pause();
19
+ videoControls.setCurrentTime(0);
20
+ setTime(0);
21
+ // eslint-disable-next-line react-hooks/exhaustive-deps
22
+ }, [Boolean(points)]);
23
+ useEffect(() => {
24
+ const updateTime = () => {
25
+ if (videoRef.current && !videoState.isPaused) {
26
+ setTime(videoRef.current.currentTime);
27
+ }
28
+ animationFrameRef.current = requestAnimationFrame(updateTime);
29
+ };
30
+ if (!videoState.isPaused) {
31
+ animationFrameRef.current = requestAnimationFrame(updateTime);
32
+ }
33
+ return () => {
34
+ if (animationFrameRef.current) {
35
+ cancelAnimationFrame(animationFrameRef.current);
36
+ }
37
+ };
38
+ }, [videoRef, videoState.isPaused]);
39
+ useEffect(() => {
40
+ if (videoState.isPaused)
41
+ setTime(videoState.currentTime);
42
+ }, [videoState]);
43
+ return {
44
+ routeRef,
45
+ videoRef,
46
+ time,
47
+ setTime: (time) => {
48
+ videoRef.current.currentTime = time;
49
+ setTime(time);
50
+ },
51
+ activeSpot,
52
+ setActiveSpot: (spot) => {
53
+ setActiveSpot(spot);
54
+ const calculatedTime = findTimeBySpot(points, spot);
55
+ if (calculatedTime !== null) {
56
+ setTime(calculatedTime);
57
+ videoControls.setCurrentTime(calculatedTime);
58
+ }
59
+ },
60
+ };
61
+ }
@@ -0,0 +1,9 @@
1
+ import { type Point } from '../types';
2
+ export declare function calculateCenterZoom(point: Point, containerSize: DOMRect, scale: number): {
3
+ scale: number;
4
+ offset: {
5
+ x: number;
6
+ y: number;
7
+ };
8
+ };
9
+ //# sourceMappingURL=calculateCenterZoom.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"calculateCenterZoom.d.ts","sourceRoot":"","sources":["../../src/utils/calculateCenterZoom.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,KAAK,EAAE,MAAM,UAAU,CAAC;AAGtC,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM;;;;;;EActF"}
@@ -0,0 +1,13 @@
1
+ import { offsetToZoom } from './offsetToZoom';
2
+ export function calculateCenterZoom(point, containerSize, scale) {
3
+ if (!point || !(containerSize === null || containerSize === void 0 ? void 0 : containerSize.width) || !(containerSize === null || containerSize === void 0 ? void 0 : containerSize.height) || !scale) {
4
+ return { scale: 1, offset: { x: 0, y: 0 } };
5
+ }
6
+ // Convert normalized point (0-1) to pixel coordinates
7
+ const pointX = point.x * containerSize.width;
8
+ const pointY = point.y * containerSize.height;
9
+ // Calculate offset to center the point
10
+ const offsetX = (containerSize.width / 2 - pointX) * scale;
11
+ const offsetY = (containerSize.height / 2 - pointY) * scale;
12
+ return offsetToZoom(offsetX, offsetY, containerSize, scale);
13
+ }
@@ -0,0 +1,9 @@
1
+ import { type Point } from '../types';
2
+ export declare function calculateOptimalZoom(points: Point[], containerSize: DOMRect, zoom: number, maxScale?: number): {
3
+ scale: number;
4
+ offset: {
5
+ x: number;
6
+ y: number;
7
+ };
8
+ };
9
+ //# sourceMappingURL=calculateOptimalZoom.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"calculateOptimalZoom.d.ts","sourceRoot":"","sources":["../../src/utils/calculateOptimalZoom.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,KAAK,EAAE,MAAM,UAAU,CAAC;AAGtC,wBAAgB,oBAAoB,CACnC,MAAM,EAAE,KAAK,EAAE,EACf,aAAa,EAAE,OAAO,EACtB,IAAI,EAAE,MAAM,EACZ,QAAQ,GAAE,MAAU;;;;;;EAyCpB"}
@@ -0,0 +1,32 @@
1
+ import { offsetToZoom } from './offsetToZoom';
2
+ export function calculateOptimalZoom(points, containerSize, zoom, maxScale = 2) {
3
+ if (!(points === null || points === void 0 ? void 0 : points.length) || !(containerSize === null || containerSize === void 0 ? void 0 : containerSize.width) || !(containerSize === null || containerSize === void 0 ? void 0 : containerSize.height) || !zoom) {
4
+ return { scale: 1, offset: { x: 0, y: 0 } };
5
+ }
6
+ // Find bounding box of all points (normalized coordinates)
7
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
8
+ for (const point of points) {
9
+ minX = Math.min(minX, point.x);
10
+ maxX = Math.max(maxX, point.x);
11
+ minY = Math.min(minY, point.y);
12
+ maxY = Math.max(maxY, point.y);
13
+ }
14
+ // Convert to pixel coordinates
15
+ const boundingBox = {
16
+ width: (maxX - minX) * containerSize.width,
17
+ height: (maxY - minY) * containerSize.height,
18
+ centerX: ((minX + maxX) / 2) * containerSize.width,
19
+ centerY: ((minY + maxY) / 2) * containerSize.height,
20
+ };
21
+ // Calculate scale to fit bounding box with some padding
22
+ const scaleX = (containerSize.width * zoom) / boundingBox.width;
23
+ const scaleY = (containerSize.height * zoom) / boundingBox.height;
24
+ const scale = Math.max(1, Math.min(scaleX, scaleY, maxScale));
25
+ // Calculate offset to center the bounding box
26
+ const containerCenterX = containerSize.width / 2;
27
+ const containerCenterY = containerSize.height / 2;
28
+ // We need to translate the image center by -offsetToBounding * scale
29
+ const offsetX = -(boundingBox.centerX - containerCenterX) * scale;
30
+ const offsetY = -(boundingBox.centerY - containerCenterY) * scale;
31
+ return offsetToZoom(offsetX, offsetY, containerSize, scale);
32
+ }
@@ -0,0 +1,5 @@
1
+ export declare function clampPosition(containerSize: DOMRect, x: number, y: number, currentScale: number): {
2
+ x: number;
3
+ y: number;
4
+ };
5
+ //# sourceMappingURL=clampPosition.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clampPosition.d.ts","sourceRoot":"","sources":["../../src/utils/clampPosition.ts"],"names":[],"mappings":"AAEA,wBAAgB,aAAa,CAAC,aAAa,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM;;;EAY/F"}
@@ -0,0 +1,12 @@
1
+ import { clamp } from 'remeda';
2
+ export function clampPosition(containerSize, x, y, currentScale) {
3
+ const imageWidth = containerSize.width * currentScale;
4
+ const imageHeight = containerSize.height * currentScale;
5
+ // Calculate bounds - with center origin, the offset represents the center position
6
+ const halfScaledWidth = (imageWidth - containerSize.width) / 2;
7
+ const halfScaledHeight = (imageHeight - containerSize.height) / 2;
8
+ return {
9
+ x: clamp(x, { min: -halfScaledWidth, max: halfScaledWidth }),
10
+ y: clamp(y, { min: -halfScaledHeight, max: halfScaledHeight }),
11
+ };
12
+ }
@@ -0,0 +1,3 @@
1
+ import { type Point, type Spot } from '../types';
2
+ export declare function findSpotByTime(points: Point[], time: number): Spot;
3
+ //# sourceMappingURL=findSpotByTime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"findSpotByTime.d.ts","sourceRoot":"","sources":["../../src/utils/findSpotByTime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,KAAK,EAAE,KAAK,IAAI,EAAE,MAAM,UAAU,CAAC;AAGjD,wBAAgB,cAAc,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAuElE"}
@@ -0,0 +1,62 @@
1
+ // Find active spot based on time
2
+ export function findSpotByTime(points, time) {
3
+ var _a, _b, _c, _d, _e, _f, _g, _h;
4
+ if (!(points === null || points === void 0 ? void 0 : points.length))
5
+ return null;
6
+ let lastPoint = { point: points[0], i: 0, maxTime: 0 };
7
+ // Iterate through points to find the spot
8
+ for (let i = 0; i < points.length; i++) {
9
+ const point = points[i];
10
+ if (point.start === undefined && point.end === undefined && point.marked === undefined)
11
+ continue;
12
+ const minTime = (_b = (_a = point.start) !== null && _a !== void 0 ? _a : point.marked) !== null && _b !== void 0 ? _b : 0;
13
+ const maxTime = (_e = (_d = (_c = point.end) !== null && _c !== void 0 ? _c : point.marked) !== null && _d !== void 0 ? _d : point.start) !== null && _e !== void 0 ? _e : 0;
14
+ // Time is at this point
15
+ if (minTime <= time && time <= maxTime) {
16
+ return { point, pointIndex: i, percentage: 0 };
17
+ }
18
+ if (time > maxTime) {
19
+ lastPoint = { point, i, maxTime };
20
+ continue;
21
+ }
22
+ const startTime = (_h = (_g = (_f = point.start) !== null && _f !== void 0 ? _f : point.marked) !== null && _g !== void 0 ? _g : point.end) !== null && _h !== void 0 ? _h : 0;
23
+ const endTime = lastPoint.maxTime;
24
+ const segmentPoints = points.slice(lastPoint.i, i + 1);
25
+ // Calculate cumulative distances
26
+ const distances = [0];
27
+ let totalDistance = 0;
28
+ for (let j = 1; j < segmentPoints.length; j++) {
29
+ const dx = segmentPoints[j].x - segmentPoints[j - 1].x;
30
+ const dy = segmentPoints[j].y - segmentPoints[j - 1].y;
31
+ totalDistance += Math.sqrt(dx * dx + dy * dy);
32
+ distances.push(totalDistance);
33
+ }
34
+ if (totalDistance === 0) {
35
+ return { point: lastPoint.point, pointIndex: lastPoint.i, percentage: 0 };
36
+ }
37
+ // Interpolate position based on time
38
+ const timePercentage = (time - endTime) / (startTime - endTime);
39
+ const targetDistance = totalDistance * timePercentage;
40
+ // Find segment containing target distance
41
+ let segIdx = distances.findIndex((d, idx) => idx > 0 && targetDistance <= d) - 1;
42
+ if (segIdx < 0)
43
+ segIdx = distances.length - 2;
44
+ // Interpolate within segment
45
+ const segStart = distances[segIdx];
46
+ const segEnd = distances[segIdx + 1];
47
+ const segT = (targetDistance - segStart) / (segEnd - segStart);
48
+ const p1 = segmentPoints[segIdx];
49
+ const p2 = segmentPoints[segIdx + 1];
50
+ return {
51
+ point: { x: p1.x + (p2.x - p1.x) * segT, y: p1.y + (p2.y - p1.y) * segT },
52
+ pointIndex: lastPoint.i + segIdx,
53
+ percentage: Math.round(segT * 1000) / 10,
54
+ };
55
+ }
56
+ // Time is beyond all points, return last point
57
+ return {
58
+ point: points[points.length - 1],
59
+ pointIndex: points.length - 1,
60
+ percentage: 0,
61
+ };
62
+ }
@@ -0,0 +1,3 @@
1
+ import { type Point, type Spot } from '../types';
2
+ export declare function findTimeBySpot(points: Point[], spot: Spot): number;
3
+ //# sourceMappingURL=findTimeBySpot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"findTimeBySpot.d.ts","sourceRoot":"","sources":["../../src/utils/findTimeBySpot.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,KAAK,EAAE,KAAK,IAAI,EAAE,MAAM,UAAU,CAAC;AAGjD,wBAAgB,cAAc,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,IAAI,UAiEzD"}
@@ -0,0 +1,60 @@
1
+ // Find time based on spot
2
+ export function findTimeBySpot(points, spot) {
3
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
4
+ if (!(points === null || points === void 0 ? void 0 : points.length) || !spot)
5
+ return null;
6
+ // Check if we're snapped directly to a point
7
+ if (spot.percentage === 100) {
8
+ const point = points[spot.pointIndex + 1];
9
+ const res = (_a = point.marked) !== null && _a !== void 0 ? _a : point.start;
10
+ if (res)
11
+ return res;
12
+ }
13
+ else if (spot.percentage === 0) {
14
+ const point = points[spot.pointIndex];
15
+ return (_c = (_b = point.marked) !== null && _b !== void 0 ? _b : point.start) !== null && _c !== void 0 ? _c : 0;
16
+ }
17
+ // Find timing segment boundaries in a single pass
18
+ let startIdx = 0;
19
+ let endIdx = points.length - 1;
20
+ for (let i = spot.pointIndex; i >= 0; i--) {
21
+ if ((_e = (_d = points[i].start) !== null && _d !== void 0 ? _d : points[i].marked) !== null && _e !== void 0 ? _e : points[i].end) {
22
+ startIdx = i;
23
+ break;
24
+ }
25
+ }
26
+ for (let i = spot.pointIndex + 1; i < points.length; i++) {
27
+ if ((_g = (_f = points[i].start) !== null && _f !== void 0 ? _f : points[i].marked) !== null && _g !== void 0 ? _g : points[i].end) {
28
+ endIdx = i;
29
+ break;
30
+ }
31
+ }
32
+ const startPoint = points[startIdx];
33
+ const endPoint = points[endIdx];
34
+ const startPointTime = (_j = (_h = startPoint.end) !== null && _h !== void 0 ? _h : startPoint.marked) !== null && _j !== void 0 ? _j : startPoint.start;
35
+ const endPointTime = (_l = (_k = endPoint.start) !== null && _k !== void 0 ? _k : endPoint.marked) !== null && _l !== void 0 ? _l : endPoint.end;
36
+ // Calculate cumulative distances along the path
37
+ const pathPoints = points.slice(startIdx, endIdx + 1);
38
+ const distances = [0];
39
+ let totalDistance = 0;
40
+ for (let i = 1; i < pathPoints.length; i++) {
41
+ const dx = pathPoints[i].x - pathPoints[i - 1].x;
42
+ const dy = pathPoints[i].y - pathPoints[i - 1].y;
43
+ totalDistance += Math.sqrt(dx * dx + dy * dy);
44
+ distances.push(totalDistance);
45
+ }
46
+ if (totalDistance === 0)
47
+ return startPointTime;
48
+ // Calculate distance to the spot
49
+ const pointIndexInPath = spot.pointIndex - startIdx;
50
+ const distanceToSegmentStart = distances[pointIndexInPath];
51
+ const segmentStart = pathPoints[pointIndexInPath];
52
+ const segmentEnd = pathPoints[pointIndexInPath + 1];
53
+ const dx = segmentEnd.x - segmentStart.x;
54
+ const dy = segmentEnd.y - segmentStart.y;
55
+ const segmentLength = Math.sqrt(dx * dx + dy * dy);
56
+ const totalDistanceToSpot = distanceToSegmentStart + segmentLength * (spot.percentage / 100);
57
+ // Interpolate time based on distance ratio
58
+ const distanceRatio = totalDistanceToSpot / totalDistance;
59
+ return startPointTime + (endPointTime - startPointTime) * distanceRatio;
60
+ }
@@ -0,0 +1,3 @@
1
+ import { type Point, type Spot } from '../types';
2
+ export declare function getClosestPointOnPath(points: Point[], mouseX: number, mouseY: number, snapThreshold: number): Spot;
3
+ //# sourceMappingURL=getClosestPointOnPath.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getClosestPointOnPath.d.ts","sourceRoot":"","sources":["../../src/utils/getClosestPointOnPath.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,KAAK,EAAE,KAAK,IAAI,EAAE,MAAM,UAAU,CAAC;AAGjD,wBAAgB,qBAAqB,CACpC,MAAM,EAAE,KAAK,EAAE,EACf,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,MAAM,GACnB,IAAI,CAuEN"}
@@ -0,0 +1,63 @@
1
+ import { clamp } from 'remeda';
2
+ // Calculate closest point on path with segment info and snapping
3
+ export function getClosestPointOnPath(points, mouseX, mouseY, snapThreshold) {
4
+ if (!points || points.length < 2)
5
+ return null;
6
+ let closestPoint = null;
7
+ let minDistance = Infinity;
8
+ let closestPointIndex = -1;
9
+ let segmentPercentage = 0;
10
+ // Check each segment
11
+ for (let i = 1; i < points.length; i++) {
12
+ const x1 = points[i - 1].x;
13
+ const y1 = points[i - 1].y;
14
+ const x2 = points[i].x;
15
+ const y2 = points[i].y;
16
+ const segmentLength = Math.pow(Math.sqrt(Math.pow((x2 - x1), 2) + Math.pow((y2 - y1), 2)), 2);
17
+ // Vector from start to end of segment
18
+ const dx = x2 - x1;
19
+ const dy = y2 - y1;
20
+ // Vector from start to mouse
21
+ const mx = mouseX - x1;
22
+ const my = mouseY - y1;
23
+ // Project mouse onto segment
24
+ const t = clamp((mx * dx + my * dy) / segmentLength, {
25
+ min: 0,
26
+ max: 1,
27
+ });
28
+ // Closest point on this segment
29
+ let px = x1 + t * dx;
30
+ let py = y1 + t * dy;
31
+ // Check if we should snap to start point
32
+ const distToStart = Math.sqrt(Math.pow((mouseX - x1), 2) + Math.pow((mouseY - y1), 2));
33
+ const distToEnd = Math.sqrt(Math.pow((mouseX - x2), 2) + Math.pow((mouseY - y2), 2));
34
+ let snappedT = t;
35
+ let preferredSegmentIndex = i - 1;
36
+ if (points[i - 1].marked && distToStart < snapThreshold && distToStart < distToEnd) {
37
+ px = x1;
38
+ py = y1;
39
+ snappedT = 0;
40
+ }
41
+ else if (points[i].marked && distToEnd < snapThreshold) {
42
+ px = x2;
43
+ py = y2;
44
+ preferredSegmentIndex = i;
45
+ snappedT = 0;
46
+ }
47
+ // Distance from mouse to closest point
48
+ const dist = Math.sqrt(Math.pow((mouseX - px), 2) + Math.pow((mouseY - py), 2));
49
+ if (dist < minDistance) {
50
+ minDistance = dist;
51
+ closestPoint = { x: px, y: py };
52
+ closestPointIndex = preferredSegmentIndex;
53
+ segmentPercentage = snappedT * 100;
54
+ }
55
+ }
56
+ if (minDistance >= snapThreshold)
57
+ return null;
58
+ return {
59
+ point: closestPoint,
60
+ pointIndex: closestPointIndex,
61
+ percentage: Math.round(segmentPercentage * 10) / 10,
62
+ };
63
+ }
@@ -0,0 +1,5 @@
1
+ export { calculateCenterZoom } from './calculateCenterZoom';
2
+ export { calculateOptimalZoom } from './calculateOptimalZoom';
3
+ export { findSpotByTime } from './findSpotByTime';
4
+ export { findTimeBySpot } from './findTimeBySpot';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,4 @@
1
+ export { calculateCenterZoom } from './calculateCenterZoom';
2
+ export { calculateOptimalZoom } from './calculateOptimalZoom';
3
+ export { findSpotByTime } from './findSpotByTime';
4
+ export { findTimeBySpot } from './findTimeBySpot';
@@ -0,0 +1,11 @@
1
+ export declare function mouseToContainer(mouse: {
2
+ clientX: number;
3
+ clientY: number;
4
+ }, containerSize: DOMRect, mapOffset: {
5
+ x: number;
6
+ y: number;
7
+ }, scale: number): {
8
+ x: number;
9
+ y: number;
10
+ };
11
+ //# sourceMappingURL=mouseToContainer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mouseToContainer.d.ts","sourceRoot":"","sources":["../../src/utils/mouseToContainer.ts"],"names":[],"mappings":"AAAA,wBAAgB,gBAAgB,CAC/B,KAAK,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,EAC3C,aAAa,EAAE,OAAO,EACtB,SAAS,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,EACnC,KAAK,EAAE,MAAM;;;EASb"}
@@ -0,0 +1,9 @@
1
+ export function mouseToContainer(mouse, containerSize, mapOffset, scale) {
2
+ const centerX = containerSize.width / 2;
3
+ const centerY = containerSize.height / 2;
4
+ const mouseX = (mouse.clientX - containerSize.x - centerX - mapOffset.x) / scale;
5
+ const mouseY = (mouse.clientY - containerSize.y - centerY - mapOffset.y) / scale;
6
+ const normalizedX = (mouseX + centerX) / containerSize.width;
7
+ const normalizedY = (mouseY + centerY) / containerSize.height;
8
+ return { x: normalizedX, y: normalizedY };
9
+ }
@@ -0,0 +1,8 @@
1
+ export declare function offsetToZoom(offsetX: number, offsetY: number, containerSize: DOMRect, scale: number): {
2
+ scale: number;
3
+ offset: {
4
+ x: number;
5
+ y: number;
6
+ };
7
+ };
8
+ //# sourceMappingURL=offsetToZoom.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"offsetToZoom.d.ts","sourceRoot":"","sources":["../../src/utils/offsetToZoom.ts"],"names":[],"mappings":"AAEA,wBAAgB,YAAY,CAC3B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,aAAa,EAAE,OAAO,EACtB,KAAK,EAAE,MAAM;;;;;;EAUb"}
@@ -0,0 +1,9 @@
1
+ import { clamp } from 'remeda';
2
+ export function offsetToZoom(offsetX, offsetY, containerSize, scale) {
3
+ // Constrain offset to never show beyond image bounds
4
+ const halfScaledWidth = (containerSize.width * scale - containerSize.width) / 2;
5
+ const halfScaledHeight = (containerSize.height * scale - containerSize.height) / 2;
6
+ offsetX = clamp(offsetX, { min: -halfScaledWidth, max: halfScaledWidth });
7
+ offsetY = clamp(offsetY, { min: -halfScaledHeight, max: halfScaledHeight });
8
+ return { scale, offset: { x: offsetX, y: offsetY } };
9
+ }
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "image-map-route",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsc -p tsconfig.build.json",
20
+ "publish": "npm publish --access public",
21
+ "prepack": "npm run build"
22
+ },
23
+ "dependencies": {
24
+ "remeda": "2.33.6",
25
+ "rooks": "9.5.0"
26
+ },
27
+ "peerDependencies": {
28
+ "react": ">=18"
29
+ },
30
+ "devDependencies": {
31
+ "@types/react": "19.2.14",
32
+ "typescript": "5.9.3"
33
+ }
34
+ }