@enigmax/primitives 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.
@@ -0,0 +1,105 @@
1
+ import { useRef, useState, useEffect, useLayoutEffect, type RefObject } from "react";
2
+ import { createMarquee, type MarqueeOptions, type MarqueeInstance } from "@/core/marquee";
3
+
4
+ /** useLayoutEffect warns on the server; the copy count still has to land before first paint. */
5
+ const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
6
+
7
+ export interface UseMarqueeResult {
8
+ /** Attach to the clipping viewport. */
9
+ laneRef: RefObject<HTMLElement | null>;
10
+ /** Attach to the moved element. Render `copies` children into it. */
11
+ trackRef: RefObject<HTMLElement | null>;
12
+ /**
13
+ * How many times to render the content. Starts at 2 so the server-rendered
14
+ * HTML stays small, then grows to cover the lane plus one whole period.
15
+ */
16
+ copies: number;
17
+ /** True while the row is being grabbed. Useful for a `cursor: grabbing`. */
18
+ dragging: boolean;
19
+ /** Measured distance between two copies, in px. 0 before the first measure. */
20
+ period: number;
21
+ /** Re-read the lap from the DOM. Call after changing the content yourself. */
22
+ measure: () => void;
23
+ }
24
+
25
+ /**
26
+ * Drive a draggable infinite marquee from React.
27
+ *
28
+ * `speed` is pixels per second and never a duration: a duration ties the speed
29
+ * to the item count, so the row accelerates every time content is added.
30
+ *
31
+ * ```tsx
32
+ * const { laneRef, trackRef, copies } = useMarquee({ speed: 80, hoverScale: 0.15 });
33
+ * return (
34
+ * <div ref={laneRef}>
35
+ * <div ref={trackRef} style={{ display: "flex" }}>
36
+ * {Array.from({ length: copies }, (_, copy) => (
37
+ * <div key={copy} aria-hidden={copy > 0} style={{ display: "flex", gap: 32 }}>
38
+ * {items.map(item => <Logo key={item.id} {...item} />)}
39
+ * </div>
40
+ * ))}
41
+ * </div>
42
+ * </div>
43
+ * );
44
+ * ```
45
+ */
46
+ export function useMarquee(options: MarqueeOptions = {}): UseMarqueeResult {
47
+ const laneRef = useRef<HTMLElement | null>(null);
48
+ const trackRef = useRef<HTMLElement | null>(null);
49
+ const instanceRef = useRef<MarqueeInstance | null>(null);
50
+ const optionsRef = useRef(options);
51
+ optionsRef.current = options;
52
+
53
+ const [copies, setCopies] = useState(2);
54
+ const [dragging, setDragging] = useState(false);
55
+ const [period, setPeriod] = useState(0);
56
+
57
+ useIsomorphicLayoutEffect(() => {
58
+ const lane = laneRef.current;
59
+ const track = trackRef.current;
60
+ if (!lane || !track) return;
61
+
62
+ const instance = createMarquee(lane, track, {
63
+ ...optionsRef.current,
64
+ // React owns the DOM; cloning behind its back would be undone on render.
65
+ copies: "external",
66
+ onCopyCountChange: setCopies,
67
+ onMeasure: measured => setPeriod(measured),
68
+ onItemClick: event => optionsRef.current.onItemClick?.(event)
69
+ });
70
+ instanceRef.current = instance;
71
+
72
+ const syncDragging = () => setDragging(instance.dragging);
73
+ lane.addEventListener("pointerdown", syncDragging);
74
+ window.addEventListener("pointerup", syncDragging);
75
+ window.addEventListener("pointercancel", syncDragging);
76
+
77
+ return () => {
78
+ lane.removeEventListener("pointerdown", syncDragging);
79
+ window.removeEventListener("pointerup", syncDragging);
80
+ window.removeEventListener("pointercancel", syncDragging);
81
+ instance.destroy();
82
+ instanceRef.current = null;
83
+ };
84
+ // The instance is created once; option changes go through update() below,
85
+ // because recreating it would restart the row on every render.
86
+ }, []);
87
+
88
+ useIsomorphicLayoutEffect(() => {
89
+ instanceRef.current?.update(options);
90
+ }, [options.speed, options.reverse, options.vertical, options.draggable, options.hoverScale, options.decay]);
91
+
92
+ // A copy count change means new children; the lap has to be read again.
93
+ useIsomorphicLayoutEffect(() => {
94
+ instanceRef.current?.measure();
95
+ }, [copies]);
96
+
97
+ return {
98
+ laneRef,
99
+ trackRef,
100
+ copies,
101
+ dragging,
102
+ period,
103
+ measure: () => instanceRef.current?.measure()
104
+ };
105
+ }