clip-pal 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/dist/index.js ADDED
@@ -0,0 +1,1132 @@
1
+ "use client";
2
+
3
+ // src/types.ts
4
+ var PAL_GESTURES = {
5
+ lean: 4e3,
6
+ // steps up close and stays there while the line is read
7
+ look: 3200,
8
+ // glances left and right, with a beat on each side
9
+ knock: 3400,
10
+ // walks up, points a finger and taps on the glass
11
+ kick: 4200,
12
+ // a ball rolls in, he boots it away
13
+ crack: 3e3,
14
+ // thumps the screen hard enough to crack it
15
+ dance: 3e3,
16
+ // sways with the leg kicking out
17
+ wave: 2400,
18
+ // waves with his leg
19
+ hop: 2200,
20
+ // a decaying bounce on the spot
21
+ spin: 1400,
22
+ // one pirouette
23
+ nod: 1600,
24
+ // yes
25
+ shake: 1600,
26
+ // no
27
+ bow: 2200,
28
+ // a bow from the foot, held a moment
29
+ backflip: 1600,
30
+ // crouch, one full turn in the air, land
31
+ shiver: 1400,
32
+ // a quick shudder with the eyes squeezed shut
33
+ doze: 3200
34
+ // eyes close, he droops, then jolts awake
35
+ };
36
+ var PAL_POSES = [
37
+ "unbend",
38
+ "curl",
39
+ "heart",
40
+ "question",
41
+ "exclaim",
42
+ "key"
43
+ ];
44
+ var PAL_POSE_IN_MS = 900;
45
+ var PAL_POSE_OUT_MS = 700;
46
+ var PAL_EMOTES = [
47
+ ...Object.keys(PAL_GESTURES),
48
+ ...PAL_POSES
49
+ ];
50
+ function isPose(emote) {
51
+ return PAL_POSES.includes(emote);
52
+ }
53
+ var PAL_SHAPES = {
54
+ /** the everyday Gem clip — the default */
55
+ gem: {
56
+ body: "M15 62 L15 14 C15 7 19 3 25 3 C31 3 35 7 35 14 L35 50 C35 55 32 58 28 58 C24 58 21 55 21 50 L21 20",
57
+ leg: "M15 63 C15 70 20 73 27 73 C34 73 41 69 41 61 L41 54",
58
+ hip: [15, 63],
59
+ eyes: [
60
+ { cx: 22, cy: 12, r: 2.4 },
61
+ { cx: 30, cy: 12, r: 2.4 }
62
+ ]
63
+ },
64
+ /** rounder, chubbier loops */
65
+ round: {
66
+ body: "M13 60 L13 18 C13 8 18 3 25 3 C32 3 37 8 37 18 L37 48 C37 55 33 59 28 59 C23 59 20 55 20 49 L20 24",
67
+ leg: "M13 61 C13 70 19 74 27 74 C35 74 42 69 42 61 L42 55",
68
+ hip: [13, 61],
69
+ eyes: [
70
+ { cx: 21, cy: 13, r: 2.7 },
71
+ { cx: 29, cy: 13, r: 2.7 }
72
+ ]
73
+ },
74
+ /** a boxy office clip with squared corners */
75
+ square: {
76
+ body: "M14 62 L14 8 C14 5 15 4 18 4 C28 4 32 4 32 4 L36 4 C36 20 36 40 36 54 C36 58 34 59 30 59 C22 59 20 58 20 54 L20 18",
77
+ leg: "M14 63 C14 72 18 74 26 74 C34 74 42 72 42 64 L42 56",
78
+ hip: [14, 63],
79
+ eyes: [
80
+ { cx: 21, cy: 12, r: 2.4 },
81
+ { cx: 29, cy: 12, r: 2.4 }
82
+ ]
83
+ },
84
+ /** stretched tall and thin */
85
+ long: {
86
+ body: "M17 64 L17 12 C17 6 20 2 25 2 C30 2 33 6 33 12 L33 52 C33 56 31 59 28 59 C25 59 23 56 23 52 L23 18",
87
+ leg: "M17 65 C17 71 21 74 27 74 C33 74 39 70 39 63 L39 56",
88
+ hip: [17, 65],
89
+ eyes: [
90
+ { cx: 22.5, cy: 10, r: 2 },
91
+ { cx: 27.5, cy: 10, r: 2 }
92
+ ]
93
+ }
94
+ };
95
+ function resolveShape(shape) {
96
+ if (!shape) return PAL_SHAPES.gem;
97
+ if (typeof shape === "string") return PAL_SHAPES[shape] ?? PAL_SHAPES.gem;
98
+ return shape;
99
+ }
100
+ var commands = (d) => d.replace(/[^MLCQSTAHVZmlcqstahvz]/g, "");
101
+ function isMorphable(shape) {
102
+ return commands(shape.body) === commands(PAL_SHAPES.gem.body) && commands(shape.leg) === commands(PAL_SHAPES.gem.leg);
103
+ }
104
+ function gestureMs(gesture, tempo = 1) {
105
+ return Math.round(PAL_GESTURES[gesture] * tempo);
106
+ }
107
+ function palVars(colors, motion) {
108
+ const v = {};
109
+ if (colors?.wire) v["--pal-wire"] = colors.wire;
110
+ if (colors?.face) v["--pal-face"] = colors.face;
111
+ if (colors?.ink) v["--pal-ink"] = colors.ink;
112
+ if (colors?.bubbleBg) v["--pal-bubble-bg"] = colors.bubbleBg;
113
+ if (colors?.bubbleBorder) v["--pal-bubble-border"] = colors.bubbleBorder;
114
+ if (colors?.bubbleText) v["--pal-bubble-text"] = colors.bubbleText;
115
+ if (colors?.bubbleDot) v["--pal-bubble-dot"] = colors.bubbleDot;
116
+ if (motion?.tempo !== void 0) v["--pal-tempo"] = String(motion.tempo);
117
+ if (motion?.stepMs !== void 0) v["--pal-step"] = `${motion.stepMs}ms`;
118
+ if (motion?.swayMs !== void 0) v["--pal-sway"] = `${motion.swayMs}ms`;
119
+ if (motion?.swayDeg !== void 0) v["--pal-sway-deg"] = `${motion.swayDeg}deg`;
120
+ if (motion?.blinkMs !== void 0) v["--pal-blink"] = `${motion.blinkMs}ms`;
121
+ if (motion?.floatMs !== void 0) v["--pal-float"] = `${motion.floatMs}ms`;
122
+ if (motion?.floatPx !== void 0) v["--pal-float-px"] = `${motion.floatPx}px`;
123
+ return v;
124
+ }
125
+
126
+ // src/PalSvg.tsx
127
+ import { jsx, jsxs } from "react/jsx-runtime";
128
+ var wire = { stroke: "var(--pal-wire, #d9a441)" };
129
+ var face = { fill: "var(--pal-face, #f2ede0)" };
130
+ var faceLine = { stroke: "var(--pal-face, #f2ede0)" };
131
+ var ink = { fill: "var(--pal-ink, #131f1a)" };
132
+ var inkLine = { stroke: "var(--pal-ink, #131f1a)" };
133
+ var warned = false;
134
+ function PalSvg({
135
+ width = 88,
136
+ height,
137
+ walking = false,
138
+ emote = null,
139
+ emoteOut = false,
140
+ shape,
141
+ strokeWidth = 5,
142
+ eyes = "dots",
143
+ colors,
144
+ motion,
145
+ className,
146
+ style
147
+ }) {
148
+ const h = height ?? Math.round(width * 80 / 50);
149
+ const sh = resolveShape(shape);
150
+ if (!warned && !isMorphable(sh)) {
151
+ warned = true;
152
+ console.warn(
153
+ "[clip-pal] custom shape does not keep the M L C C L C C L / M C C L command sequence \u2014 the morphing poses will cut instead of tween."
154
+ );
155
+ }
156
+ const classes = [
157
+ "pal",
158
+ walking ? "pal-walking" : "",
159
+ emote ? `pal-em-${emote}${emoteOut ? "-out" : ""}` : "",
160
+ className ?? ""
161
+ ].filter(Boolean).join(" ");
162
+ const vars = palVars(colors, motion);
163
+ return /* @__PURE__ */ jsxs(
164
+ "svg",
165
+ {
166
+ width,
167
+ height: h,
168
+ viewBox: "0 0 50 80",
169
+ preserveAspectRatio: height ? "none" : void 0,
170
+ fill: "none",
171
+ "aria-hidden": true,
172
+ className: classes,
173
+ style: { ...vars, ...style },
174
+ children: [
175
+ /* @__PURE__ */ jsx(
176
+ "g",
177
+ {
178
+ className: "pal-crack",
179
+ style: faceLine,
180
+ strokeWidth: "1.3",
181
+ strokeLinecap: "round",
182
+ fill: "none",
183
+ children: /* @__PURE__ */ jsx("path", { d: "M42 30 L48.5 30.0 M42 30 L48.5 22.2 M42 30 L42.0 19.0 M42 30 L34.2 22.2 M42 30 L31.0 30.0 M42 30 L34.2 37.8 M42 30 L42.0 41.0 M42 30 L48.5 37.8 M46.5 30.0 L45.2 26.8 L42.0 25.5 L38.8 26.8 L37.5 30.0 L38.8 33.2 L42.0 34.5 L45.2 33.2 Z M48.5 30.0 L48.0 24.0 L42.0 21.5 L36.0 24.0 L33.5 30.0 L36.0 36.0 L42.0 38.5 L48.0 36.0 Z" })
184
+ }
185
+ ),
186
+ /* @__PURE__ */ jsxs(
187
+ "g",
188
+ {
189
+ style: wire,
190
+ strokeWidth,
191
+ fill: "none",
192
+ strokeLinecap: "round",
193
+ strokeLinejoin: "round",
194
+ children: [
195
+ /* @__PURE__ */ jsx("path", { className: "pal-body", d: sh.body }),
196
+ /* @__PURE__ */ jsx(
197
+ "path",
198
+ {
199
+ className: "pal-leg",
200
+ d: sh.leg,
201
+ style: { transformOrigin: `${sh.hip[0]}px ${sh.hip[1]}px` }
202
+ }
203
+ )
204
+ ]
205
+ }
206
+ ),
207
+ eyes === "dots" && sh.eyes.map((e, i) => /* @__PURE__ */ jsx("circle", { className: "pal-eye", cx: e.cx, cy: e.cy, r: e.r, style: face }, i)),
208
+ /* @__PURE__ */ jsxs("g", { className: "pal-tap", style: faceLine, fill: "none", strokeLinecap: "round", children: [
209
+ /* @__PURE__ */ jsx("circle", { cx: "45", cy: "45", r: "3.8", strokeWidth: "1.9" }),
210
+ /* @__PURE__ */ jsx("circle", { cx: "45", cy: "45", r: "6.2", strokeWidth: "1.3" })
211
+ ] }),
212
+ /* @__PURE__ */ jsxs("g", { className: "pal-ball", children: [
213
+ /* @__PURE__ */ jsx("circle", { cx: "42", cy: "71", r: "6.6", style: face }),
214
+ /* @__PURE__ */ jsx("path", { d: "M42 68 L44.8 70 L43.7 73.4 L40.3 73.4 L39.2 70 Z", style: ink }),
215
+ /* @__PURE__ */ jsx(
216
+ "path",
217
+ {
218
+ d: "M42 68 L42 64.4 M44.8 70 L48.3 68.9 M43.7 73.4 L45.9 76.3 M40.3 73.4 L38.1 76.3 M39.2 70 L35.7 68.9",
219
+ style: inkLine,
220
+ strokeWidth: "1",
221
+ fill: "none"
222
+ }
223
+ )
224
+ ] }),
225
+ /* @__PURE__ */ jsxs("g", { className: "pal-zzz", style: face, children: [
226
+ /* @__PURE__ */ jsx("circle", { className: "pal-zzz-1", cx: "39", cy: "16", r: "1.6" }),
227
+ /* @__PURE__ */ jsx("circle", { className: "pal-zzz-2", cx: "43", cy: "11", r: "2.2" }),
228
+ /* @__PURE__ */ jsx("circle", { className: "pal-zzz-3", cx: "46.5", cy: "5.5", r: "2.7" })
229
+ ] })
230
+ ]
231
+ }
232
+ );
233
+ }
234
+
235
+ // src/PalCompanion.tsx
236
+ import {
237
+ forwardRef,
238
+ useEffect as useEffect2,
239
+ useImperativeHandle,
240
+ useRef as useRef2,
241
+ useState
242
+ } from "react";
243
+
244
+ // src/Bubble.tsx
245
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
246
+ function Bubble({
247
+ side,
248
+ hidden,
249
+ typing,
250
+ line,
251
+ box,
252
+ contentStyle,
253
+ contentRef,
254
+ tailInset = 10
255
+ }) {
256
+ const tailStyle = side === "left" ? { right: tailInset } : side === "right" ? { left: tailInset } : void 0;
257
+ return /* @__PURE__ */ jsxs2(
258
+ "div",
259
+ {
260
+ className: `clip-pal-bubble is-${side} ${hidden ? "pal-bubble-hide" : "pal-say"}`,
261
+ children: [
262
+ /* @__PURE__ */ jsx2(
263
+ "div",
264
+ {
265
+ className: "clip-pal-bubble-box",
266
+ style: box ? { width: box.w, height: box.h } : void 0,
267
+ children: /* @__PURE__ */ jsx2("div", { ref: contentRef, className: "clip-pal-bubble-content", style: contentStyle, children: typing ? /* @__PURE__ */ jsxs2("span", { className: "clip-pal-dots", children: [
268
+ /* @__PURE__ */ jsx2("span", { className: "clip-pal-dot" }),
269
+ /* @__PURE__ */ jsx2("span", { className: "clip-pal-dot" }),
270
+ /* @__PURE__ */ jsx2("span", { className: "clip-pal-dot" })
271
+ ] }) : /* @__PURE__ */ jsx2("span", { className: "pal-line-in", children: line }) })
272
+ }
273
+ ),
274
+ /* @__PURE__ */ jsx2("span", { className: "clip-pal-tail", style: tailStyle })
275
+ ]
276
+ }
277
+ );
278
+ }
279
+
280
+ // src/hover.ts
281
+ import { useEffect, useRef } from "react";
282
+ var HOVER_ATTR = "data-pal-say";
283
+ var HOVER_EMOTE_ATTR = "data-pal-emote";
284
+ function useHoverSay(enabled, cooldownMs, onSay) {
285
+ const cb = useRef(onSay);
286
+ cb.current = onSay;
287
+ useEffect(() => {
288
+ if (!enabled) return;
289
+ let last = 0;
290
+ let lastEl = null;
291
+ const over = (e) => {
292
+ const t = e.target;
293
+ if (!(t instanceof Element)) return;
294
+ const el = t.closest(`[${HOVER_ATTR}]`);
295
+ if (!el) {
296
+ lastEl = null;
297
+ return;
298
+ }
299
+ if (el === lastEl) return;
300
+ const now = performance.now();
301
+ if (now - last < cooldownMs) return;
302
+ const text = el.getAttribute(HOVER_ATTR);
303
+ if (!text) return;
304
+ last = now;
305
+ lastEl = el;
306
+ const raw = el.getAttribute(HOVER_EMOTE_ATTR);
307
+ const emote = raw && PAL_EMOTES.includes(raw) ? raw : null;
308
+ cb.current({ text, emote, el });
309
+ };
310
+ document.addEventListener("pointerover", over, { passive: true });
311
+ return () => document.removeEventListener("pointerover", over);
312
+ }, [enabled, cooldownMs]);
313
+ }
314
+
315
+ // src/PalCompanion.tsx
316
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
317
+ var PalCompanion = forwardRef(
318
+ function PalCompanion2({
319
+ lines,
320
+ width = 90,
321
+ height,
322
+ showMs = 1e4,
323
+ gapMs = 2e4,
324
+ firstMs = 1500,
325
+ typingMs = 850,
326
+ emoteDelayMs = 500,
327
+ bubbleMaxWidth = "15rem",
328
+ random = true,
329
+ speak = true,
330
+ emotes = true,
331
+ hover = true,
332
+ hoverCooldownMs = 1200,
333
+ float = true,
334
+ sway = true,
335
+ visibleThreshold = 0.4,
336
+ shape,
337
+ strokeWidth,
338
+ eyes,
339
+ colors,
340
+ motion,
341
+ onSpeak,
342
+ className,
343
+ style
344
+ }, ref) {
345
+ const rootRef = useRef2(null);
346
+ const [line, setLine] = useState(lines[0]?.[0] ?? "");
347
+ const [shown, setShown] = useState(false);
348
+ const [typing, setTyping] = useState(true);
349
+ const [emote, setEmote] = useState(
350
+ null
351
+ );
352
+ const pose = useRef2(null);
353
+ const [turn, setTurn] = useState(0);
354
+ const lastLine = useRef2(-1);
355
+ const onScreen = useRef2(false);
356
+ const p = useRef2({
357
+ lines,
358
+ random,
359
+ speak,
360
+ emotes,
361
+ typingMs,
362
+ emoteDelayMs,
363
+ tempo: motion?.tempo ?? 1,
364
+ onSpeak,
365
+ showMs,
366
+ gapMs
367
+ });
368
+ p.current = {
369
+ lines,
370
+ random,
371
+ speak,
372
+ emotes,
373
+ typingMs,
374
+ emoteDelayMs,
375
+ tempo: motion?.tempo ?? 1,
376
+ onSpeak,
377
+ showMs,
378
+ gapMs
379
+ };
380
+ const timers = useRef2({});
381
+ const reduced = useRef2(false);
382
+ const allowed = (name) => {
383
+ const e = p.current.emotes;
384
+ if (!name || e === false) return null;
385
+ if (e === true) return name;
386
+ return e.includes(name) ? name : null;
387
+ };
388
+ const unfold = () => {
389
+ const t = timers.current;
390
+ if (pose.current) {
391
+ setEmote({ name: pose.current, out: true });
392
+ pose.current = null;
393
+ t.emoteEnd = setTimeout(() => setEmote(null), PAL_POSE_OUT_MS);
394
+ } else {
395
+ setEmote(null);
396
+ }
397
+ };
398
+ const sayNow = (text, acts) => {
399
+ const t = timers.current;
400
+ clearTimeout(t.type);
401
+ clearTimeout(t.emote);
402
+ clearTimeout(t.emoteEnd);
403
+ if (p.current.speak) {
404
+ setLine(text);
405
+ setTyping(!reduced.current);
406
+ setTurn((n) => n + 1);
407
+ setShown(true);
408
+ if (!reduced.current) {
409
+ t.type = setTimeout(() => setTyping(false), p.current.typingMs);
410
+ }
411
+ }
412
+ p.current.onSpeak?.(text, acts);
413
+ const name = reduced.current ? null : allowed(acts);
414
+ const begin = () => {
415
+ if (!name) return;
416
+ setEmote({ name, out: false });
417
+ if (isPose(name)) {
418
+ pose.current = name;
419
+ } else {
420
+ t.emoteEnd = setTimeout(() => setEmote(null), gestureMs(name, p.current.tempo));
421
+ }
422
+ };
423
+ if (pose.current) {
424
+ unfold();
425
+ t.emote = setTimeout(begin, PAL_POSE_OUT_MS);
426
+ } else {
427
+ setEmote(null);
428
+ t.emote = setTimeout(begin, p.current.emoteDelayMs);
429
+ }
430
+ };
431
+ const hushNow = () => {
432
+ const t = timers.current;
433
+ clearTimeout(t.type);
434
+ clearTimeout(t.emote);
435
+ clearTimeout(t.emoteEnd);
436
+ setShown(false);
437
+ unfold();
438
+ };
439
+ const schedule = (delay) => {
440
+ const t = timers.current;
441
+ clearTimeout(t.phase);
442
+ t.phase = setTimeout(cycle, delay);
443
+ };
444
+ const cycle = () => {
445
+ if (!onScreen.current) return;
446
+ const all = p.current.lines;
447
+ if (all.length === 0) return;
448
+ let i;
449
+ if (p.current.random) {
450
+ i = Math.floor(Math.random() * all.length);
451
+ if (all.length > 1 && i === lastLine.current) i = (i + 1) % all.length;
452
+ } else {
453
+ i = (lastLine.current + 1) % all.length;
454
+ }
455
+ lastLine.current = i;
456
+ const [text, acts] = all[i];
457
+ sayNow(text, acts);
458
+ const t = timers.current;
459
+ clearTimeout(t.phase);
460
+ t.phase = setTimeout(() => {
461
+ hushNow();
462
+ schedule(p.current.gapMs);
463
+ }, p.current.showMs);
464
+ };
465
+ const interrupt = (text, acts) => {
466
+ sayNow(text, acts);
467
+ const t = timers.current;
468
+ clearTimeout(t.phase);
469
+ t.phase = setTimeout(() => {
470
+ hushNow();
471
+ schedule(p.current.gapMs);
472
+ }, p.current.showMs);
473
+ };
474
+ useImperativeHandle(ref, () => ({
475
+ say: (text, e = null) => interrupt(text, e),
476
+ emote: (name) => {
477
+ const t = timers.current;
478
+ clearTimeout(t.emote);
479
+ clearTimeout(t.emoteEnd);
480
+ const n = allowed(name);
481
+ if (!n) return;
482
+ const go = () => {
483
+ setEmote({ name: n, out: false });
484
+ if (isPose(n)) pose.current = n;
485
+ else t.emoteEnd = setTimeout(() => setEmote(null), gestureMs(n, p.current.tempo));
486
+ };
487
+ if (pose.current) {
488
+ unfold();
489
+ t.emote = setTimeout(go, PAL_POSE_OUT_MS);
490
+ } else {
491
+ setEmote(null);
492
+ requestAnimationFrame(go);
493
+ }
494
+ },
495
+ hush: () => {
496
+ hushNow();
497
+ schedule(p.current.gapMs);
498
+ }
499
+ }));
500
+ useHoverSay(hover, hoverCooldownMs, ({ text, emote: e }) => {
501
+ if (!onScreen.current) return;
502
+ interrupt(text, e);
503
+ });
504
+ useEffect2(() => {
505
+ const el = rootRef.current;
506
+ if (!el) return;
507
+ reduced.current = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
508
+ const observer = new IntersectionObserver(
509
+ ([entry]) => {
510
+ if (entry.isIntersecting) {
511
+ if (onScreen.current) return;
512
+ onScreen.current = true;
513
+ schedule(firstMs);
514
+ } else {
515
+ onScreen.current = false;
516
+ clearTimeout(timers.current.phase);
517
+ hushNow();
518
+ }
519
+ },
520
+ { threshold: visibleThreshold }
521
+ );
522
+ observer.observe(el);
523
+ return () => {
524
+ observer.disconnect();
525
+ onScreen.current = false;
526
+ const t = timers.current;
527
+ clearTimeout(t.phase);
528
+ clearTimeout(t.type);
529
+ clearTimeout(t.emote);
530
+ clearTimeout(t.emoteEnd);
531
+ };
532
+ }, [firstMs, visibleThreshold]);
533
+ const vars = palVars(colors, motion);
534
+ return /* @__PURE__ */ jsxs3(
535
+ "div",
536
+ {
537
+ ref: rootRef,
538
+ className: `clip-pal-companion${className ? ` ${className}` : ""}`,
539
+ style: { ...vars, ...style },
540
+ children: [
541
+ speak && /* @__PURE__ */ jsx3("div", { className: "clip-pal-companion-bubble", children: /* @__PURE__ */ jsx3("div", { children: /* @__PURE__ */ jsx3(
542
+ Bubble,
543
+ {
544
+ side: "center",
545
+ hidden: !shown,
546
+ typing,
547
+ line,
548
+ contentStyle: { maxWidth: bubbleMaxWidth }
549
+ }
550
+ ) }, turn) }),
551
+ /* @__PURE__ */ jsx3("div", { className: float ? "pal-float" : void 0, children: /* @__PURE__ */ jsx3("div", { className: sway ? "pal-idle" : void 0, children: /* @__PURE__ */ jsx3(
552
+ PalSvg,
553
+ {
554
+ width,
555
+ height,
556
+ emote: emote?.name ?? null,
557
+ emoteOut: emote?.out ?? false,
558
+ shape,
559
+ strokeWidth,
560
+ eyes
561
+ }
562
+ ) }) })
563
+ ]
564
+ }
565
+ );
566
+ }
567
+ );
568
+ var PalCompanion_default = PalCompanion;
569
+
570
+ // src/ScrollPal.tsx
571
+ import {
572
+ forwardRef as forwardRef2,
573
+ useEffect as useEffect3,
574
+ useImperativeHandle as useImperativeHandle2,
575
+ useLayoutEffect,
576
+ useRef as useRef3,
577
+ useState as useState2
578
+ } from "react";
579
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
580
+ var ScrollPal = forwardRef2(function ScrollPal2({
581
+ stops,
582
+ idleLines,
583
+ idle: idleOn = true,
584
+ idleAfterMs = 12e3,
585
+ idleWanderMs = 2e4,
586
+ minViewport = 1600,
587
+ contentWidth,
588
+ contentVar = "--content-w",
589
+ minWidth = 46,
590
+ maxWidth = 92,
591
+ gap = 12,
592
+ edge = 8,
593
+ side = "auto",
594
+ offsetY = 0,
595
+ yLimitPad = 150,
596
+ zIndex = 40,
597
+ speed = 480,
598
+ speedY = 540,
599
+ ease: easeRate = 6.32,
600
+ leanMax = 9,
601
+ walkThreshold = 27,
602
+ retargetMs = 330,
603
+ typingMs = 850,
604
+ emoteDelayMs = 500,
605
+ bubbleMaxWidth = 260,
606
+ bubbleMinWidth = 150,
607
+ random = true,
608
+ speak = true,
609
+ emotes = true,
610
+ hover = true,
611
+ hoverCooldownMs = 1200,
612
+ draggable = true,
613
+ shape,
614
+ strokeWidth,
615
+ eyes,
616
+ colors,
617
+ motion,
618
+ onArrive,
619
+ onLeave,
620
+ onSpeak,
621
+ onIdle,
622
+ className,
623
+ style
624
+ }, ref) {
625
+ const wrapRef = useRef3(null);
626
+ const palRef = useRef3(null);
627
+ const [line, setLine] = useState2(stops[0]?.lines[0]?.[0] ?? "");
628
+ const [leftGutter, setLeftGutter] = useState2(true);
629
+ const [bubbleOn, setBubbleOn] = useState2(true);
630
+ const [typing, setTyping] = useState2(true);
631
+ const [arrivalId, setArrivalId] = useState2(0);
632
+ const [walking, setWalking] = useState2(false);
633
+ const [emote, setEmote] = useState2(
634
+ null
635
+ );
636
+ const pose = useRef3(null);
637
+ const [palW, setPalW] = useState2(88);
638
+ const [bubbleW, setBubbleW] = useState2(240);
639
+ const [bubSize, setBubSize] = useState2(null);
640
+ const contentRef = useRef3(null);
641
+ const target = useRef3({ x: 40, y: 0 });
642
+ const cur = useRef3({ x: 40, y: 0, lean: 0, facing: 1 });
643
+ const palWRef = useRef3(88);
644
+ const nearRef = useRef3(stops[0] ?? { id: "", lines: [] });
645
+ const idle = useRef3(false);
646
+ const dragging = useRef3(false);
647
+ const dropped = useRef3(false);
648
+ const grab = useRef3({ x: 0, y: 0 });
649
+ const [grabbed, setGrabbed] = useState2(false);
650
+ const [roomy, setRoomy] = useState2(false);
651
+ const [wide, setWide] = useState2(false);
652
+ const lineIdx = useRef3({});
653
+ const isWalkingRef = useRef3(false);
654
+ const p = useRef3({
655
+ stops,
656
+ idleLines,
657
+ random,
658
+ speak,
659
+ emotes,
660
+ tempo: motion?.tempo ?? 1,
661
+ typingMs,
662
+ emoteDelayMs,
663
+ bubbleMaxWidth,
664
+ bubbleMinWidth,
665
+ onArrive,
666
+ onLeave,
667
+ onSpeak,
668
+ onIdle
669
+ });
670
+ p.current = {
671
+ stops,
672
+ idleLines,
673
+ random,
674
+ speak,
675
+ emotes,
676
+ tempo: motion?.tempo ?? 1,
677
+ typingMs,
678
+ emoteDelayMs,
679
+ bubbleMaxWidth,
680
+ bubbleMinWidth,
681
+ onArrive,
682
+ onLeave,
683
+ onSpeak,
684
+ onIdle
685
+ };
686
+ const timers = useRef3({});
687
+ const allowed = (name) => {
688
+ const e = p.current.emotes;
689
+ if (!name || e === false) return null;
690
+ if (e === true) return name;
691
+ return e.includes(name) ? name : null;
692
+ };
693
+ const sayNow = (text, acts) => {
694
+ const t = timers.current;
695
+ clearTimeout(t.type);
696
+ clearTimeout(t.emote);
697
+ clearTimeout(t.emoteEnd);
698
+ if (p.current.speak) {
699
+ setLine(text);
700
+ setBubbleOn(true);
701
+ setTyping(true);
702
+ setBubSize(null);
703
+ setArrivalId((n) => n + 1);
704
+ t.type = setTimeout(() => setTyping(false), p.current.typingMs);
705
+ }
706
+ p.current.onSpeak?.(text, acts);
707
+ const name = allowed(acts);
708
+ const begin = () => {
709
+ if (!name) return;
710
+ setEmote({ name, out: false });
711
+ if (isPose(name)) {
712
+ pose.current = name;
713
+ } else {
714
+ t.emoteEnd = setTimeout(() => setEmote(null), gestureMs(name, p.current.tempo));
715
+ }
716
+ };
717
+ if (pose.current) {
718
+ setEmote({ name: pose.current, out: true });
719
+ pose.current = null;
720
+ t.emote = setTimeout(begin, PAL_POSE_OUT_MS);
721
+ } else {
722
+ setEmote(null);
723
+ t.emote = setTimeout(begin, p.current.emoteDelayMs);
724
+ }
725
+ };
726
+ const hushNow = () => {
727
+ const t = timers.current;
728
+ clearTimeout(t.type);
729
+ clearTimeout(t.emote);
730
+ clearTimeout(t.emoteEnd);
731
+ if (pose.current) {
732
+ setEmote({ name: pose.current, out: true });
733
+ pose.current = null;
734
+ t.emoteEnd = setTimeout(() => setEmote(null), PAL_POSE_OUT_MS);
735
+ } else {
736
+ setEmote(null);
737
+ }
738
+ setBubbleOn(false);
739
+ };
740
+ useImperativeHandle2(ref, () => ({
741
+ say: (text, e = null) => sayNow(text, e),
742
+ emote: (name) => {
743
+ const t = timers.current;
744
+ clearTimeout(t.emote);
745
+ clearTimeout(t.emoteEnd);
746
+ const n = allowed(name);
747
+ if (!n) return;
748
+ if (pose.current) {
749
+ setEmote({ name: pose.current, out: true });
750
+ pose.current = null;
751
+ t.emote = setTimeout(() => {
752
+ setEmote({ name: n, out: false });
753
+ if (isPose(n)) pose.current = n;
754
+ else t.emoteEnd = setTimeout(() => setEmote(null), gestureMs(n, p.current.tempo));
755
+ }, PAL_POSE_OUT_MS);
756
+ return;
757
+ }
758
+ setEmote(null);
759
+ requestAnimationFrame(() => {
760
+ setEmote({ name: n, out: false });
761
+ if (isPose(n)) pose.current = n;
762
+ else t.emoteEnd = setTimeout(() => setEmote(null), gestureMs(n, p.current.tempo));
763
+ });
764
+ },
765
+ hush: hushNow,
766
+ current: () => nearRef.current.id
767
+ }));
768
+ useHoverSay(hover && wide && roomy, hoverCooldownMs, ({ text, emote: e }) => {
769
+ if (isWalkingRef.current || dragging.current) return;
770
+ sayNow(text, e);
771
+ });
772
+ useLayoutEffect(() => {
773
+ const el = contentRef.current;
774
+ if (!el) return;
775
+ setBubSize({ w: el.offsetWidth + 2, h: el.offsetHeight + 2 });
776
+ }, [typing, line, bubbleW, palW, arrivalId]);
777
+ useEffect3(() => {
778
+ const mq = window.matchMedia(`(min-width: ${minViewport}px)`);
779
+ const onMq = () => setWide(mq.matches);
780
+ onMq();
781
+ mq.addEventListener("change", onMq);
782
+ return () => mq.removeEventListener("change", onMq);
783
+ }, [minViewport]);
784
+ useEffect3(() => {
785
+ if (!wide) return;
786
+ if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
787
+ if (p.current.stops.length === 0) return;
788
+ const contentW = () => {
789
+ if (typeof contentWidth === "number") return contentWidth;
790
+ if (typeof contentWidth === "function") return contentWidth();
791
+ const root = getComputedStyle(document.documentElement);
792
+ const raw = root.getPropertyValue(contentVar).trim();
793
+ const n = parseFloat(raw);
794
+ if (!Number.isFinite(n)) return 72 * parseFloat(root.fontSize);
795
+ return raw.endsWith("px") ? n : n * parseFloat(root.fontSize);
796
+ };
797
+ const gutter = () => Math.max(0, (window.innerWidth - contentW()) / 2);
798
+ const sizePal = () => {
799
+ const room = gutter() - gap - edge;
800
+ setRoomy(room >= minWidth);
801
+ const w = Math.max(minWidth, Math.min(maxWidth, Math.floor(room)));
802
+ if (w !== palWRef.current) {
803
+ palWRef.current = w;
804
+ setPalW(w);
805
+ }
806
+ };
807
+ const parkLeft = () => Math.max(edge, gutter() - gap - palWRef.current);
808
+ const parkRight = () => Math.min(
809
+ window.innerWidth - edge - palWRef.current,
810
+ window.innerWidth - gutter() + gap
811
+ );
812
+ const fitBubble = (x, onLeft) => {
813
+ const pw = palWRef.current;
814
+ const space = onLeft ? x + pw - edge : window.innerWidth - x - pw - edge;
815
+ const { bubbleMinWidth: lo, bubbleMaxWidth: hi } = p.current;
816
+ setBubbleW(Math.round(Math.max(lo, Math.min(hi, space))));
817
+ };
818
+ const computeTarget = () => {
819
+ sizePal();
820
+ const all = p.current.stops;
821
+ const h = window.innerHeight;
822
+ const vc = h / 2;
823
+ let near = null;
824
+ let fallback = all[0];
825
+ let bestD = Infinity;
826
+ for (const s of all) {
827
+ const el = document.getElementById(s.id);
828
+ if (!el) continue;
829
+ const r = el.getBoundingClientRect();
830
+ if (vc >= r.top && vc <= r.bottom) {
831
+ near = s;
832
+ break;
833
+ }
834
+ const d = Math.min(Math.abs(r.top - vc), Math.abs(r.bottom - vc));
835
+ if (d < bestD) {
836
+ bestD = d;
837
+ fallback = s;
838
+ }
839
+ }
840
+ if (near === null) near = fallback;
841
+ const atBottom = h + window.scrollY >= document.documentElement.scrollHeight - 2;
842
+ if (atBottom) near = all[all.length - 1];
843
+ const pw = palWRef.current;
844
+ const a = document.getElementById(near.anchor)?.getBoundingClientRect();
845
+ const place = side === "left" ? "gutter-left" : side === "right" ? "gutter-right" : near.place;
846
+ let x;
847
+ if (place === "gutter-left") x = parkLeft();
848
+ else if (place === "gutter-right") x = parkRight();
849
+ else if (a) x = Math.min(window.innerWidth - edge - pw, a.right + (near.gap ?? 26));
850
+ else x = parkLeft();
851
+ const yCenter = a ? a.top + a.height / 2 : vc;
852
+ const yLimit = h / 2 - yLimitPad;
853
+ const y = Math.max(-yLimit, Math.min(yLimit, yCenter - vc + offsetY));
854
+ target.current = { x, y };
855
+ nearRef.current = near;
856
+ const onLeft = place === "gutter-left";
857
+ setLeftGutter(onLeft);
858
+ fitBubble(x, onLeft);
859
+ };
860
+ let idleTimer;
861
+ const wanderSpot = () => {
862
+ sizePal();
863
+ const onLeftNow = cur.current.x < window.innerWidth / 2;
864
+ const x = onLeftNow ? parkRight() : parkLeft();
865
+ const yLimit = window.innerHeight / 2 - yLimitPad - 30;
866
+ target.current = {
867
+ x,
868
+ y: Math.round((Math.random() * 2 - 1) * Math.max(0, yLimit))
869
+ };
870
+ const left = !onLeftNow;
871
+ setLeftGutter(left);
872
+ fitBubble(x, left);
873
+ nearRef.current = { id: "idle", lines: p.current.idleLines ?? [] };
874
+ };
875
+ const idleTick = () => {
876
+ if (!idle.current) return;
877
+ wanderSpot();
878
+ idleTimer = setTimeout(idleTick, idleWanderMs);
879
+ };
880
+ const enterIdle = () => {
881
+ if (idle.current || !idleOn || !p.current.idleLines?.length) return;
882
+ idle.current = true;
883
+ p.current.onIdle?.();
884
+ wanderSpot();
885
+ idleTimer = setTimeout(idleTick, idleWanderMs);
886
+ };
887
+ const exitIdle = () => {
888
+ if (!idle.current) return;
889
+ idle.current = false;
890
+ computeTarget();
891
+ };
892
+ const activity = () => {
893
+ clearTimeout(idleTimer);
894
+ exitIdle();
895
+ idleTimer = setTimeout(enterIdle, idleAfterMs);
896
+ };
897
+ let raf = 0;
898
+ let wasWalking = false;
899
+ let shownStop = null;
900
+ let last = performance.now();
901
+ let lastRetarget = 0;
902
+ const loop = (now) => {
903
+ const c = cur.current;
904
+ const dt = Math.min(0.05, Math.max(1e-3, (now - last) / 1e3));
905
+ last = now;
906
+ if (dragging.current) {
907
+ target.current.x = c.x;
908
+ target.current.y = c.y;
909
+ if (wrapRef.current) {
910
+ wrapRef.current.style.transform = `translate(${c.x}px, calc(-50% + ${c.y}px))`;
911
+ }
912
+ raf = requestAnimationFrame(loop);
913
+ return;
914
+ }
915
+ if (now - lastRetarget > retargetMs) {
916
+ lastRetarget = now;
917
+ if (!idle.current && !dropped.current) computeTarget();
918
+ }
919
+ const ease = 1 - Math.exp(-easeRate * dt);
920
+ const dx = target.current.x - c.x;
921
+ let vx = dx * ease / dt;
922
+ if (vx > speed) vx = speed;
923
+ if (vx < -speed) vx = -speed;
924
+ if (Math.abs(dx) > 0.4) c.x += vx * dt;
925
+ let vy = (target.current.y - c.y) * ease / dt;
926
+ if (vy > speedY) vy = speedY;
927
+ if (vy < -speedY) vy = -speedY;
928
+ c.y += vy * dt;
929
+ const leanTarget = Math.max(-leanMax, Math.min(leanMax, vx / speed * leanMax * 1.42));
930
+ c.lean += (leanTarget - c.lean) * (1 - Math.exp(-(easeRate * 1.21) * dt));
931
+ if (Math.abs(vx) > walkThreshold * 1.33) c.facing = vx > 0 ? 1 : -1;
932
+ const isWalking = Math.abs(vx) > walkThreshold;
933
+ isWalkingRef.current = isWalking;
934
+ if (isWalking !== wasWalking) {
935
+ wasWalking = isWalking;
936
+ setWalking(isWalking);
937
+ if (isWalking) {
938
+ hushNow();
939
+ if (shownStop) p.current.onLeave?.(shownStop);
940
+ shownStop = null;
941
+ }
942
+ }
943
+ if (!isWalking && shownStop !== nearRef.current.id) {
944
+ const s = nearRef.current;
945
+ shownStop = s.id;
946
+ p.current.onArrive?.(s.id);
947
+ if (s.lines.length > 0) {
948
+ const prev = lineIdx.current[s.id];
949
+ let i;
950
+ if (p.current.random) {
951
+ i = Math.floor(Math.random() * s.lines.length);
952
+ if (s.lines.length > 1 && i === prev) i = (i + 1) % s.lines.length;
953
+ } else {
954
+ i = prev === void 0 ? 0 : (prev + 1) % s.lines.length;
955
+ }
956
+ lineIdx.current[s.id] = i;
957
+ const [text, acts] = s.lines[i];
958
+ sayNow(text, acts);
959
+ }
960
+ }
961
+ if (wrapRef.current) {
962
+ wrapRef.current.style.transform = `translate(${c.x}px, calc(-50% + ${c.y}px))`;
963
+ }
964
+ if (palRef.current) {
965
+ palRef.current.style.transform = `rotate(${c.lean}deg) scaleX(${c.facing})`;
966
+ }
967
+ raf = requestAnimationFrame(loop);
968
+ };
969
+ computeTarget();
970
+ cur.current.x = target.current.x;
971
+ cur.current.y = target.current.y;
972
+ raf = requestAnimationFrame(loop);
973
+ const onScroll = () => {
974
+ activity();
975
+ dropped.current = false;
976
+ computeTarget();
977
+ };
978
+ window.addEventListener("scroll", onScroll, { passive: true });
979
+ window.addEventListener("resize", computeTarget);
980
+ window.addEventListener("mousemove", activity, { passive: true });
981
+ window.addEventListener("keydown", activity);
982
+ window.addEventListener("pointerdown", activity, { passive: true });
983
+ window.addEventListener("touchstart", activity, { passive: true });
984
+ window.addEventListener("wheel", activity, { passive: true });
985
+ idleTimer = setTimeout(enterIdle, idleAfterMs);
986
+ return () => {
987
+ cancelAnimationFrame(raf);
988
+ const t = timers.current;
989
+ clearTimeout(t.type);
990
+ clearTimeout(idleTimer);
991
+ clearTimeout(t.emote);
992
+ clearTimeout(t.emoteEnd);
993
+ window.removeEventListener("scroll", onScroll);
994
+ window.removeEventListener("resize", computeTarget);
995
+ window.removeEventListener("mousemove", activity);
996
+ window.removeEventListener("keydown", activity);
997
+ window.removeEventListener("pointerdown", activity);
998
+ window.removeEventListener("touchstart", activity);
999
+ window.removeEventListener("wheel", activity);
1000
+ };
1001
+ }, [
1002
+ wide,
1003
+ contentWidth,
1004
+ contentVar,
1005
+ minWidth,
1006
+ maxWidth,
1007
+ gap,
1008
+ edge,
1009
+ side,
1010
+ offsetY,
1011
+ yLimitPad,
1012
+ speed,
1013
+ speedY,
1014
+ easeRate,
1015
+ leanMax,
1016
+ walkThreshold,
1017
+ retargetMs,
1018
+ idleOn,
1019
+ idleAfterMs,
1020
+ idleWanderMs
1021
+ ]);
1022
+ const onPointerDown = (e) => {
1023
+ if (!draggable) return;
1024
+ e.preventDefault();
1025
+ e.currentTarget.setPointerCapture(e.pointerId);
1026
+ grab.current = {
1027
+ x: e.clientX - cur.current.x,
1028
+ y: e.clientY - (window.innerHeight / 2 + cur.current.y)
1029
+ };
1030
+ dragging.current = true;
1031
+ dropped.current = true;
1032
+ setGrabbed(true);
1033
+ };
1034
+ const onPointerMove = (e) => {
1035
+ if (!dragging.current) return;
1036
+ cur.current.x = e.clientX - grab.current.x;
1037
+ cur.current.y = e.clientY - grab.current.y - window.innerHeight / 2;
1038
+ };
1039
+ const endDrag = (e) => {
1040
+ if (!dragging.current) return;
1041
+ if (e.currentTarget.hasPointerCapture(e.pointerId)) {
1042
+ e.currentTarget.releasePointerCapture(e.pointerId);
1043
+ }
1044
+ dragging.current = false;
1045
+ setGrabbed(false);
1046
+ };
1047
+ const bubbleSide = leftGutter ? "left" : "right";
1048
+ const vars = palVars(colors, motion);
1049
+ return /* @__PURE__ */ jsx4(
1050
+ "div",
1051
+ {
1052
+ ref: wrapRef,
1053
+ className: `clip-pal-scroll${className ? ` ${className}` : ""}`,
1054
+ "data-on": wide && roomy ? "" : void 0,
1055
+ style: { ...vars, transform: "translate(40px, -50%)", zIndex, ...style },
1056
+ "aria-hidden": true,
1057
+ children: /* @__PURE__ */ jsxs4("div", { className: "clip-pal-anchor", children: [
1058
+ speak && /* @__PURE__ */ jsx4(
1059
+ "div",
1060
+ {
1061
+ className: "clip-pal-bubble-wrap",
1062
+ style: leftGutter ? { right: 0 } : { left: 0 },
1063
+ children: /* @__PURE__ */ jsx4("div", { children: /* @__PURE__ */ jsx4(
1064
+ Bubble,
1065
+ {
1066
+ side: bubbleSide,
1067
+ hidden: !bubbleOn,
1068
+ typing,
1069
+ line,
1070
+ box: bubSize,
1071
+ contentRef,
1072
+ contentStyle: {
1073
+ maxWidth: bubbleW,
1074
+ // never narrower than the pal — keeps the tail well inside
1075
+ // the bubble even with just the typing dots
1076
+ minWidth: palW + 8
1077
+ },
1078
+ tailInset: Math.max(10, palW / 2 - 7)
1079
+ }
1080
+ ) }, arrivalId)
1081
+ }
1082
+ ),
1083
+ /* @__PURE__ */ jsx4(
1084
+ "div",
1085
+ {
1086
+ ref: palRef,
1087
+ onPointerDown,
1088
+ onPointerMove,
1089
+ onPointerUp: endDrag,
1090
+ onPointerCancel: endDrag,
1091
+ className: `clip-pal-grab${grabbed ? " is-grabbed" : ""}`,
1092
+ style: draggable ? void 0 : { cursor: "default" },
1093
+ children: /* @__PURE__ */ jsx4("div", { className: walking ? "pal-walk" : "pal-idle", children: /* @__PURE__ */ jsx4(
1094
+ PalSvg,
1095
+ {
1096
+ width: palW,
1097
+ walking,
1098
+ emote: emote?.name ?? null,
1099
+ emoteOut: emote?.out ?? false,
1100
+ shape,
1101
+ strokeWidth,
1102
+ eyes
1103
+ }
1104
+ ) })
1105
+ }
1106
+ )
1107
+ ] })
1108
+ }
1109
+ );
1110
+ });
1111
+ var ScrollPal_default = ScrollPal;
1112
+ export {
1113
+ Bubble,
1114
+ HOVER_ATTR,
1115
+ HOVER_EMOTE_ATTR,
1116
+ PAL_EMOTES,
1117
+ PAL_GESTURES,
1118
+ PAL_POSES,
1119
+ PAL_POSE_IN_MS,
1120
+ PAL_POSE_OUT_MS,
1121
+ PAL_SHAPES,
1122
+ PalCompanion_default as PalCompanion,
1123
+ PalSvg,
1124
+ ScrollPal_default as ScrollPal,
1125
+ gestureMs,
1126
+ isMorphable,
1127
+ isPose,
1128
+ palVars,
1129
+ resolveShape,
1130
+ useHoverSay
1131
+ };
1132
+ //# sourceMappingURL=index.js.map