@struq/skill 0.2.2 → 0.4.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 (122) hide show
  1. package/README.md +23 -35
  2. package/package.json +3 -2
  3. package/skills/struq/SKILL.md +148 -212
  4. package/skills/struq/blocks/CATALOG.md +426 -0
  5. package/skills/struq/blocks/backgrounds/background-aurora.tsx +818 -0
  6. package/skills/struq/blocks/backgrounds/background-facets.tsx +1283 -0
  7. package/skills/struq/blocks/backgrounds/background-silk.tsx +906 -0
  8. package/skills/struq/blocks/catalog.json +1302 -0
  9. package/skills/struq/blocks/footers/footer-contact.tsx +789 -0
  10. package/skills/struq/blocks/footers/footer-reveal.tsx +313 -0
  11. package/skills/struq/blocks/headers/header-bar.tsx +652 -0
  12. package/skills/struq/blocks/headers/menu-curtain.tsx +790 -0
  13. package/skills/struq/blocks/heroes/hero-framed.tsx +627 -0
  14. package/skills/struq/blocks/heroes/hero-index.tsx +855 -0
  15. package/skills/struq/blocks/heroes/hero-poster.tsx +1086 -0
  16. package/skills/struq/blocks/heroes/hero-split.tsx +685 -0
  17. package/skills/struq/blocks/heroes/hero-still.tsx +862 -0
  18. package/skills/struq/blocks/kit/Cursor.tsx +117 -0
  19. package/skills/struq/blocks/kit/HeroCanvas.tsx +278 -0
  20. package/skills/struq/blocks/kit/HorizontalGallery.tsx +106 -0
  21. package/skills/struq/blocks/kit/MagneticButton.tsx +92 -0
  22. package/skills/struq/blocks/kit/MediaFrame.tsx +96 -0
  23. package/skills/struq/blocks/kit/PinnedZoom.tsx +89 -0
  24. package/skills/struq/blocks/kit/Reveal.tsx +64 -0
  25. package/skills/struq/blocks/kit/SmoothScroll.tsx +57 -0
  26. package/skills/struq/blocks/kit/SplitHeadline.tsx +68 -0
  27. package/skills/struq/blocks/kit/blocks.css +2102 -0
  28. package/skills/struq/blocks/kit/gsap.ts +21 -0
  29. package/skills/struq/blocks/kit/images.ts +17 -0
  30. package/skills/struq/blocks/kit/photo-read.ts +102 -0
  31. package/skills/struq/blocks/kit/scenes/contours.ts +161 -0
  32. package/skills/struq/blocks/kit/scenes/dots.ts +110 -0
  33. package/skills/struq/blocks/kit/scenes/ripple.ts +99 -0
  34. package/skills/struq/blocks/kit/scenes/types.ts +114 -0
  35. package/skills/struq/blocks/page-transitions/page-transition-gate.tsx +706 -0
  36. package/skills/struq/blocks/page-transitions/page-transition-slices.tsx +469 -0
  37. package/skills/struq/blocks/page-transitions/page-transition-stack.tsx +583 -0
  38. package/skills/struq/blocks/preloaders/preloader-gate.tsx +358 -0
  39. package/skills/struq/blocks/preloaders/preloader-slices-uncovered.ts +50 -0
  40. package/skills/struq/blocks/preloaders/preloader-slices.tsx +289 -0
  41. package/skills/struq/blocks/preloaders/preloader-stack.tsx +367 -0
  42. package/skills/struq/blocks/preloaders/preloader-wordmark.tsx +568 -0
  43. package/skills/struq/blocks/sections/about-studio.tsx +436 -0
  44. package/skills/struq/blocks/sections/approach.tsx +661 -0
  45. package/skills/struq/blocks/sections/contact-form.tsx +664 -0
  46. package/skills/struq/blocks/sections/cta-closing.tsx +763 -0
  47. package/skills/struq/blocks/sections/fanned-cards.tsx +547 -0
  48. package/skills/struq/blocks/sections/film-strip.tsx +385 -0
  49. package/skills/struq/blocks/sections/grid-assembly.tsx +616 -0
  50. package/skills/struq/blocks/sections/hover-list.tsx +467 -0
  51. package/skills/struq/blocks/sections/image-trail.tsx +432 -0
  52. package/skills/struq/blocks/sections/manifesto.tsx +691 -0
  53. package/skills/struq/blocks/sections/panel-accordion.tsx +380 -0
  54. package/skills/struq/blocks/sections/reveals.tsx +416 -0
  55. package/skills/struq/blocks/sections/split-pair.tsx +670 -0
  56. package/skills/struq/blocks/sections/sticky-chapters.tsx +256 -0
  57. package/skills/struq/blocks/sections/velocity-type.tsx +269 -0
  58. package/skills/struq/blocks/showpieces/fill-rows.tsx +74 -0
  59. package/skills/struq/blocks/showpieces/hero-opener.tsx +128 -0
  60. package/skills/struq/blocks/showpieces/image-to-fullbleed.tsx +89 -0
  61. package/skills/struq/blocks/showpieces/manifesto-scrub.tsx +48 -0
  62. package/skills/struq/blocks/showpieces/panel-wipe.tsx +89 -0
  63. package/skills/struq/blocks/showpieces/pixel-resolve.tsx +107 -0
  64. package/skills/struq/blocks/showpieces/project-stack.tsx +86 -0
  65. package/skills/struq/blocks/showpieces/scrub-connector.tsx +307 -0
  66. package/skills/struq/blocks/showpieces/wordmark-finale.tsx +105 -0
  67. package/skills/struq/blocks/transitions/zoom-through.tsx +613 -0
  68. package/skills/struq/canon/canon.json +627 -0
  69. package/skills/struq/cinematic/README.md +7 -18
  70. package/skills/struq/cinematic/examples/code/nextjs/app/components/LenisRoot.tsx +1 -1
  71. package/skills/struq/cinematic/examples/code/shared/styles.css +1 -1
  72. package/skills/struq/cinematic/examples/code/vite/src/LenisRoot.tsx +1 -1
  73. package/skills/struq/cinematic/references/01-core-mental-model.md +1 -1
  74. package/skills/struq/cinematic/references/02-vocabulary.md +1 -1
  75. package/skills/struq/cinematic/references/07-stack-integration.md +2 -2
  76. package/skills/struq/recipes/app/fonts.ts +12 -0
  77. package/skills/struq/recipes/app/globals.css +42 -0
  78. package/skills/struq/recipes/app/layout.tsx +30 -0
  79. package/skills/struq/references/craft.md +77 -0
  80. package/skills/struq/references/iconography.md +28 -230
  81. package/skills/struq/references/stack-targets.md +23 -74
  82. package/skills/struq/references/tells.md +52 -0
  83. package/skills/struq/references/triage.md +6 -7
  84. package/skills/struq/scripts/blocks.mjs +156 -0
  85. package/skills/struq/scripts/canon.mjs +134 -0
  86. package/skills/struq/scripts/check.mjs +513 -0
  87. package/skills/struq/scripts/images.mjs +360 -0
  88. package/skills/struq/scripts/log.mjs +75 -0
  89. package/skills/struq/scripts/packs.mjs +156 -0
  90. package/skills/struq/scripts/tokens.mjs +222 -0
  91. package/skills/struq/references/anti-patterns.md +0 -265
  92. package/skills/struq/references/baseline-prompt.md +0 -118
  93. package/skills/struq/references/brand.md +0 -143
  94. package/skills/struq/references/brief-router.md +0 -219
  95. package/skills/struq/references/composition.md +0 -191
  96. package/skills/struq/references/depth-parallax.md +0 -235
  97. package/skills/struq/references/direction.md +0 -188
  98. package/skills/struq/references/feel-layer.md +0 -107
  99. package/skills/struq/references/gsap-api.md +0 -293
  100. package/skills/struq/references/imagery.md +0 -144
  101. package/skills/struq/references/loaders.md +0 -287
  102. package/skills/struq/references/micro-interactions.md +0 -270
  103. package/skills/struq/references/mobile-composition.md +0 -70
  104. package/skills/struq/references/nav-header.md +0 -296
  105. package/skills/struq/references/page-architecture.md +0 -134
  106. package/skills/struq/references/page-entrances.md +0 -198
  107. package/skills/struq/references/performance.md +0 -235
  108. package/skills/struq/references/pinning-scrub.md +0 -341
  109. package/skills/struq/references/pitfalls.md +0 -290
  110. package/skills/struq/references/route-transitions.md +0 -281
  111. package/skills/struq/references/scaling.md +0 -151
  112. package/skills/struq/references/scroll-reveals.md +0 -280
  113. package/skills/struq/references/seo.md +0 -219
  114. package/skills/struq/references/setup.md +0 -397
  115. package/skills/struq/references/showcase.md +0 -87
  116. package/skills/struq/references/struq-imagery.md +0 -107
  117. package/skills/struq/references/svg-motion.md +0 -390
  118. package/skills/struq/references/transitions.md +0 -160
  119. package/skills/struq/references/typography.md +0 -720
  120. package/skills/struq/references/vibe-presets.md +0 -287
  121. package/skills/struq/references/webgl-r3f.md +0 -274
  122. package/skills/struq/references/webgl-scenes.md +0 -593
@@ -0,0 +1,436 @@
1
+ "use client";
2
+ // about-studio (Struq section recipe)
3
+ // Who the studio is: one short statement, a paragraph, a few honest facts and the place itself, given real space.
4
+ // Three compositions:
5
+ //
6
+ // - plaat: a full-bleed plate of the place leads. It arrives as a print inside the page margins and opens out to the
7
+ // edges as it climbs (a clip on a layer already at full size, never a scale), while the photograph drifts slowly
8
+ // inside its frame. Under it the statement stands large on the left, the paragraph and the facts on the right; the
9
+ // statement's lines rise through their masks and each fact's rule draws in from the left.
10
+ // - wandeling: a walk through the place. The statement, the paragraph and the facts hold still on the left (plain CSS
11
+ // sticky, nothing pins) while two columns of upright photographs pass on the right, the second one later and faster,
12
+ // so the walk has depth. Phones: the type first, then the photographs as a row you swipe.
13
+ // - doorkijk: a short pin. The statement is cut in two halves and set either side of a narrow slit of photograph. As
14
+ // the reader scrolls the slit opens into a window (its clip opens, the photo never moves) and pushes the halves apart
15
+ // to the page's edges. The paragraph and the facts follow under the pin. Phones: the halves above and below the
16
+ // frame, which opens as it passes, no pin.
17
+ //
18
+ // <AboutStudio variant="plaat" statement="Negen mensen aan één lange tafel." text="..." facts={[{ label: "Sinds",
19
+ // value: "2012" }]} images={[images.band[0]]} />
20
+ // <AboutStudio variant="doorkijk" halves={["Negen mensen,", "één lange tafel."]} ... images={[images.column[1]]} />
21
+ //
22
+ // Every move is scrubbed to the scroll with 0.4 s of smoothing (a fast flick never stacks states, going back
23
+ // reverses), transforms are written in whole pixels and every clip from plain numbers. Each photograph is decoded
24
+ // before it shows. The statement is cut into lines with the face it is read in (the cut waits for the fonts, capped).
25
+ //
26
+ // Start states wait behind the `.js` / `.sq-js` gate and a 3 s CSS fallback (blocks.css, "--- about-studio ---"), so
27
+ // a page whose scripts never run still shows the composed section. Reduced motion: the composed section, no travel.
28
+ import Image from "next/image";
29
+ import { useRef } from "react";
30
+ import { gsap, ScrollTrigger, SplitText, useGSAP } from "../kit/gsap";
31
+ import type { StruqImage } from "../kit/images";
32
+
33
+ export const ABOUT_STUDIO_VARIANTS = [
34
+ { id: "plaat", title: "Plaat" },
35
+ { id: "wandeling", title: "Wandeling" },
36
+ { id: "doorkijk", title: "Doorkijk" },
37
+ ] as const;
38
+
39
+ export type AboutStudioVariant = (typeof ABOUT_STUDIO_VARIANTS)[number]["id"];
40
+
41
+ type Fact = { label: string; value: string };
42
+
43
+ type Props = {
44
+ variant?: AboutStudioVariant;
45
+ /** The studio in one sentence. plaat and wandeling set it whole; doorkijk cuts it in two (or takes `halves`). */
46
+ statement: string;
47
+ /** doorkijk: the two halves either side of the window. Left out, the statement is cut at its middle word. */
48
+ halves?: [string, string];
49
+ /** Two or three sentences about the people and the place. */
50
+ text: string;
51
+ /** A few honest facts: a short label and its value. */
52
+ facts?: Fact[];
53
+ /** plaat and doorkijk take the first photograph (a wide plate, an upright window); wandeling takes up to four. */
54
+ images: StruqImage[];
55
+ className?: string;
56
+ };
57
+
58
+ /** Seconds of smoothing on every scrub: a camera settling, never a delay. */
59
+ const SMOOTH = 0.4;
60
+ /** The longest a line cut waits for the display face, in ms. */
61
+ const FONT_CAP = 1200;
62
+ /** A line travels this share of its own height to its place. */
63
+ const RISE = 1.12;
64
+ /** The soft rise of the paragraph and the facts, in px. */
65
+ const SOFT = 18;
66
+ /** plaat: the share of the frame's height the photo layer reaches past it, above and below (its drift). */
67
+ const DRIFT = 0.1;
68
+ /** plaat: the margin the plate starts inside, as a share of its width per side. */
69
+ const MARGIN = 0.06;
70
+ /** wandeling: how far the second column travels against the first over the section, in screens. */
71
+ const WALK = 0.24;
72
+ /** doorkijk: the pin, in screens, and the share of the window's width the slit starts at. */
73
+ const PIN = 1.3;
74
+ const SLIT = 0.16;
75
+
76
+ const clamp01 = (v: number) => Math.min(1, Math.max(0, v));
77
+ const within = (p: number, from: number, to: number) => clamp01((p - from) / (to - from));
78
+ const out2 = gsap.parseEase("power2.out");
79
+ const out3 = gsap.parseEase("power3.out");
80
+ const inOut2 = gsap.parseEase("power2.inOut");
81
+ const inOut3 = gsap.parseEase("power3.inOut");
82
+
83
+ /** Staggered windows for n items inside 0 to 1: each runs `span`, the last one ends at 1. */
84
+ function windows(n: number, span: number) {
85
+ if (n <= 1) return [[0, 1]];
86
+ const step = (1 - span) / (n - 1);
87
+ return Array.from({ length: n }, (_, i) => [i * step, i * step + span]);
88
+ }
89
+
90
+ function faceReady() {
91
+ const fonts = typeof document !== "undefined" ? document.fonts?.ready : undefined;
92
+ return Promise.race([fonts ?? Promise.resolve(), new Promise((res) => window.setTimeout(res, FONT_CAP))]);
93
+ }
94
+
95
+ function decoded(img: HTMLImageElement) {
96
+ const loaded = img.complete
97
+ ? Promise.resolve()
98
+ : new Promise<void>((res) => {
99
+ img.addEventListener("load", () => res(), { once: true });
100
+ img.addEventListener("error", () => res(), { once: true });
101
+ });
102
+ return loaded.then(() => img.decode()).catch(() => undefined);
103
+ }
104
+
105
+ const px = (n: number) => `${Math.round(n)}px`;
106
+
107
+ function run(el: HTMLElement, kind: AboutStudioVariant, wide: boolean, reduce: boolean) {
108
+ const heads = [...el.querySelectorAll<HTMLElement>("[data-as-head]")];
109
+ const copy = el.querySelector<HTMLElement>("[data-as-copy]");
110
+ const softs = [...el.querySelectorAll<HTMLElement>("[data-as-soft]")];
111
+ const rules = [...el.querySelectorAll<HTMLElement>("[data-as-rule]")];
112
+ const photos = [...el.querySelectorAll<HTMLElement>("[data-as-photo]")];
113
+ const frame = el.querySelector<HTMLElement>("[data-as-frame]");
114
+ const layer = el.querySelector<HTMLElement>("[data-as-layer]");
115
+ const stage = el.querySelector<HTMLElement>("[data-as-stage]");
116
+ const walk = el.querySelector<HTMLElement>("[data-as-walk]");
117
+ const late = el.querySelector<HTMLElement>('[data-as-col="1"]');
118
+ if (reduce) {
119
+ el.dataset.ready = "1";
120
+ return;
121
+ }
122
+
123
+ let alive = true;
124
+ const splits: SplitText[] = [];
125
+ const lines: HTMLElement[][] = heads.map(() => []);
126
+ const heights: number[][] = heads.map(() => []);
127
+ const prog = { type: 0, soft: 0, open: 0, move: 0 };
128
+ const shown = photos.map(() => ({ v: 0 }));
129
+ let vh = window.innerHeight;
130
+ let frameW = 0;
131
+ let frameH = 0;
132
+ const measure = () => {
133
+ vh = window.innerHeight;
134
+ frameW = frame?.offsetWidth ?? 0;
135
+ frameH = frame?.offsetHeight ?? 0;
136
+ };
137
+ measure();
138
+
139
+ const paint = () => {
140
+ if (!alive) return;
141
+ // The statement: every head's lines rise one after another on the same stretch.
142
+ heads.forEach((_, h) => {
143
+ const spans = windows(lines[h].length, 0.62);
144
+ lines[h].forEach((line, i) => {
145
+ const e = out3(within(prog.type, spans[i][0], spans[i][1]));
146
+ line.style.transform = `translate3d(0, ${px((1 - e) * (heights[h][i] ?? 0) * RISE)}, 0)`;
147
+ });
148
+ });
149
+ const softSpans = windows(softs.length, 0.72);
150
+ softs.forEach((s, i) => {
151
+ const e = out2(within(prog.soft, softSpans[i][0], softSpans[i][1]));
152
+ s.style.transform = `translate3d(0, ${px((1 - e) * SOFT)}, 0)`;
153
+ s.style.opacity = e.toFixed(3);
154
+ });
155
+ // Each fact's rule draws from the left a little behind its row.
156
+ const ruleSpans = windows(rules.length, 0.6);
157
+ rules.forEach((r, i) => {
158
+ const e = inOut2(within(prog.soft, 0.12 + ruleSpans[i][0] * 0.88, 0.12 + ruleSpans[i][1] * 0.88));
159
+ r.style.setProperty("--as-draw", e.toFixed(4));
160
+ });
161
+ photos.forEach((p, i) => {
162
+ p.style.opacity = shown[i].v.toFixed(3);
163
+ });
164
+
165
+ if (kind === "plaat" && frame && layer) {
166
+ const inset = (1 - inOut3(prog.open)) * MARGIN * frameW;
167
+ frame.style.clipPath = `inset(0px ${inset.toFixed(1)}px 0px ${inset.toFixed(1)}px)`;
168
+ layer.style.transform = `translate3d(0, ${px((1 - 2 * prog.move) * DRIFT * frameH)}, 0)`;
169
+ } else if (kind === "wandeling" && late) {
170
+ late.style.transform = wide ? `translate3d(0, ${px((0.5 - prog.move) * WALK * vh)}, 0)` : "";
171
+ } else if (kind === "doorkijk" && frame) {
172
+ const share = SLIT + (1 - SLIT) * inOut3(prog.open);
173
+ const side = ((1 - share) / 2) * frameW;
174
+ frame.style.clipPath = `inset(0px ${side.toFixed(1)}px 0px ${side.toFixed(1)}px)`;
175
+ // The halves stand tight against the slit and are pushed out with its edges (wide screens only).
176
+ heads.forEach((h, i) => {
177
+ h.style.transform = wide ? `translate3d(${px(i === 0 ? side : -side)}, 0, 0)` : "";
178
+ });
179
+ }
180
+ };
181
+
182
+ const scrub = (target: object, vars: gsap.TweenVars, trigger: Element | null, start: string, end: string) => {
183
+ if (!trigger) return;
184
+ gsap.to(target, { ...vars, ease: "none", onUpdate: paint, scrollTrigger: { trigger, start, end, scrub: SMOOTH } });
185
+ };
186
+
187
+ if (kind === "plaat") {
188
+ scrub(prog, { open: 1 }, frame, "top 96%", "top 30%");
189
+ scrub(prog, { move: 1 }, frame, "top bottom", "bottom top");
190
+ scrub(prog, { type: 1 }, heads[0] ?? null, "top 90%", "top 52%");
191
+ scrub(prog, { soft: 1 }, copy, "top 92%", "top 48%");
192
+ } else if (kind === "wandeling") {
193
+ scrub(prog, { type: 1 }, el, "top 85%", "top 35%");
194
+ // Wide, the copy is sticky, so its stretch is read off the section; on phones off the copy itself.
195
+ if (wide) scrub(prog, { soft: 1 }, el, "top 70%", "top 10%");
196
+ else scrub(prog, { soft: 1 }, copy, "top 85%", "bottom 85%");
197
+ scrub(prog, { move: 1 }, walk, "top bottom", "bottom top");
198
+ } else if (wide && stage) {
199
+ // doorkijk: the halves rise as the stage arrives, then the pin opens the window.
200
+ scrub(prog, { type: 1 }, stage, "top 80%", "top 25%");
201
+ gsap.to(prog, {
202
+ open: 1,
203
+ ease: "none",
204
+ onUpdate: paint,
205
+ scrollTrigger: { trigger: stage, start: "top top", end: () => `+=${Math.round(PIN * window.innerHeight)}`, pin: true, scrub: SMOOTH },
206
+ });
207
+ scrub(prog, { soft: 1 }, copy, "top 92%", "top 52%");
208
+ } else {
209
+ scrub(prog, { type: 1 }, stage, "top 85%", "top 40%");
210
+ scrub(prog, { open: 1 }, frame, "top 88%", "center 50%");
211
+ scrub(prog, { soft: 1 }, copy, "top 94%", "top 60%");
212
+ }
213
+
214
+ const remeasure = () => {
215
+ measure();
216
+ paint();
217
+ };
218
+ ScrollTrigger.addEventListener("refresh", remeasure);
219
+
220
+ // Every photograph is decoded at its display size before any of it shows; the layer then fades in over a moment.
221
+ photos.forEach((p, i) => {
222
+ const img = p.querySelector("img");
223
+ if (!img) {
224
+ shown[i].v = 1;
225
+ return;
226
+ }
227
+ img.loading = "eager";
228
+ decoded(img).then(() => {
229
+ if (!alive) return;
230
+ gsap.to(shown[i], { v: 1, duration: 0.45, ease: "power2.out", onUpdate: paint });
231
+ });
232
+ });
233
+
234
+ faceReady().then(() => {
235
+ if (!alive) return;
236
+ heads.forEach((head, h) =>
237
+ splits.push(
238
+ SplitText.create(head, {
239
+ type: "lines",
240
+ mask: "lines",
241
+ linesClass: "struq-line",
242
+ autoSplit: true,
243
+ onSplit(self) {
244
+ lines[h] = self.lines as HTMLElement[];
245
+ heights[h] = lines[h].map((l) => l.offsetHeight);
246
+ paint();
247
+ },
248
+ }),
249
+ ),
250
+ );
251
+ el.dataset.ready = "1";
252
+ });
253
+ paint();
254
+
255
+ return () => {
256
+ alive = false;
257
+ ScrollTrigger.removeEventListener("refresh", remeasure);
258
+ for (const s of shown) gsap.killTweensOf(s);
259
+ for (const s of splits) s.revert();
260
+ for (const n of [...softs, ...photos, ...heads]) {
261
+ n.style.transform = "";
262
+ n.style.opacity = "";
263
+ }
264
+ for (const r of rules) r.style.removeProperty("--as-draw");
265
+ if (frame) frame.style.clipPath = "";
266
+ if (layer) layer.style.transform = "";
267
+ if (late) late.style.transform = "";
268
+ };
269
+ }
270
+
271
+ function Facts({ facts, className = "" }: { facts?: Fact[]; className?: string }) {
272
+ if (!facts?.length) return null;
273
+ return (
274
+ <dl className={className}>
275
+ {facts.map((f, i) => (
276
+ <div key={i} data-as-soft="" data-as-rule="" className="relative grid grid-cols-[7rem_1fr] gap-x-4 py-4 text-small md:grid-cols-[8.5rem_1fr]">
277
+ <dt className="text-ink-muted">{f.label}</dt>
278
+ <dd>{f.value}</dd>
279
+ </div>
280
+ ))}
281
+ </dl>
282
+ );
283
+ }
284
+
285
+ function halvesOf(statement: string, halves?: [string, string]): [string, string] {
286
+ if (halves) return halves;
287
+ const words = statement.split(" ");
288
+ const cut = Math.max(1, Math.ceil(words.length / 2));
289
+ return [words.slice(0, cut).join(" "), words.slice(cut).join(" ")];
290
+ }
291
+
292
+ const DISPLAY = "font-display leading-[1.04] tracking-[-0.022em]";
293
+
294
+ export function AboutStudio({ variant = "plaat", statement, halves, text, facts, images, className = "" }: Props) {
295
+ const root = useRef<HTMLElement>(null);
296
+
297
+ useGSAP(
298
+ () => {
299
+ const el = root.current;
300
+ if (!el) return;
301
+ const mm = gsap.matchMedia();
302
+ // Every side of every query is named, so the moves run on phones as well as on wide screens.
303
+ mm.add(
304
+ {
305
+ wide: "(min-width: 768px)",
306
+ reduce: "(prefers-reduced-motion: reduce)",
307
+ motion: "(prefers-reduced-motion: no-preference)",
308
+ },
309
+ (ctx) => {
310
+ const { wide, reduce } = ctx.conditions as { wide: boolean; reduce: boolean };
311
+ return run(el, variant, wide, reduce);
312
+ },
313
+ );
314
+ return () => mm.revert();
315
+ },
316
+ { scope: root, dependencies: [variant] },
317
+ );
318
+
319
+ const first = images[0];
320
+
321
+ if (variant === "wandeling") {
322
+ const walk = images.slice(0, 4);
323
+ const half = Math.ceil(walk.length / 2);
324
+ const cols = [walk.slice(0, half), walk.slice(half)];
325
+ return (
326
+ <section
327
+ ref={root}
328
+ data-about-studio="wandeling"
329
+ className={`relative overflow-hidden bg-surface-0 px-6 py-[14svh] text-ink md:px-[4vw] md:py-[18svh] ${className}`}
330
+ >
331
+ <div className="grid gap-y-14 md:grid-cols-12 md:gap-x-[2vw]">
332
+ <div className="md:col-span-5">
333
+ <div data-as-copy="" className="md:sticky md:top-[16svh]">
334
+ <h2 data-as-head="" className={`${DISPLAY} max-w-[13ch] text-[clamp(2.25rem,1rem+3.2vw,5rem)]`}>
335
+ {statement}
336
+ </h2>
337
+ <p data-as-soft="" className="mt-8 max-w-[40ch] text-lead text-ink-muted md:mt-10">
338
+ {text}
339
+ </p>
340
+ <Facts facts={facts} className="mt-10 max-w-[30rem] md:mt-12" />
341
+ </div>
342
+ </div>
343
+ {/* Phones: one row of photographs you swipe, so it is a scroll region keyboards can reach. */}
344
+ <div
345
+ data-as-walk=""
346
+ role="region"
347
+ aria-label="De studio in beeld"
348
+ tabIndex={0}
349
+ className="-mx-6 flex snap-x snap-mandatory gap-4 overflow-x-auto px-6 pb-2 outline-offset-4 md:col-span-6 md:col-start-7 md:mx-0 md:grid md:grid-cols-2 md:gap-x-[2vw] md:overflow-visible md:px-0 md:pb-0"
350
+ >
351
+ {cols.map((col, c) => (
352
+ <div key={c} data-as-col={c} className={`contents md:flex md:flex-col md:gap-[12svh] ${c === 1 ? "md:pt-[28svh]" : ""}`}>
353
+ {col.map((img) => (
354
+ <figure key={img.src} className="w-[74vw] shrink-0 snap-start md:w-auto">
355
+ <div className="relative aspect-[4/5] overflow-hidden">
356
+ <div data-as-photo="" className="absolute inset-0">
357
+ <Image src={img.src} alt={img.alt} fill sizes="(min-width: 768px) 25vw, 74vw" unoptimized={img.unoptimized} className="object-cover" />
358
+ </div>
359
+ </div>
360
+ <figcaption className="mt-3 text-small text-ink-muted">{img.alt}</figcaption>
361
+ </figure>
362
+ ))}
363
+ </div>
364
+ ))}
365
+ </div>
366
+ </div>
367
+ </section>
368
+ );
369
+ }
370
+
371
+ if (variant === "doorkijk") {
372
+ const [a, b] = halvesOf(statement, halves);
373
+ return (
374
+ <section ref={root} data-about-studio="doorkijk" className={`relative bg-surface-0 text-ink ${className}`}>
375
+ {/* One grid whose middle track is the window: the heading's two halves sit either side of it (the heading is
376
+ display: contents, so its halves are the grid's items), stacked above and below it on phones. */}
377
+ <div
378
+ data-as-stage=""
379
+ className="grid min-h-svh content-center justify-items-center gap-y-8 px-6 py-[12svh] md:h-svh md:min-h-[36rem] md:grid-cols-[1fr_var(--dk-w)_1fr] md:items-center md:gap-x-[3vw] md:gap-y-0 md:px-[4vw] md:py-0"
380
+ >
381
+ <h2 className="contents">
382
+ <span data-as-head="" className={`${DISPLAY} row-start-1 block max-w-[11ch] text-center text-[clamp(2.25rem,min(0.8rem+3.6vw,8.5svh),5.5rem)] md:col-start-1 md:justify-self-end md:text-right`}>
383
+ {a}
384
+ </span>{" "}
385
+ <span data-as-head="" className={`${DISPLAY} row-start-3 block max-w-[11ch] text-center text-[clamp(2.25rem,min(0.8rem+3.6vw,8.5svh),5.5rem)] md:col-start-3 md:row-start-1 md:justify-self-start md:text-left`}>
386
+ {b}
387
+ </span>
388
+ </h2>
389
+ {first ? (
390
+ <figure className="relative row-start-2 w-[var(--dk-w)] md:col-start-2 md:row-start-1">
391
+ <div data-as-frame="" className="relative aspect-[4/5] overflow-hidden">
392
+ <div data-as-photo="" className="absolute inset-0">
393
+ <Image src={first.src} alt={first.alt} fill sizes="(min-width: 768px) 36vw, 64vw" unoptimized={first.unoptimized} className="object-cover" />
394
+ </div>
395
+ </div>
396
+ <figcaption className="absolute left-0 top-full mt-3 text-small text-ink-muted">{first.alt}</figcaption>
397
+ </figure>
398
+ ) : null}
399
+ </div>
400
+ <div data-as-copy="" className="grid gap-y-10 px-6 pb-[16svh] pt-[10svh] md:grid-cols-12 md:gap-x-[2vw] md:px-[4vw] md:pt-[14svh]">
401
+ <p data-as-soft="" className="max-w-[40ch] text-lead text-ink-muted md:col-span-5 md:col-start-2">
402
+ {text}
403
+ </p>
404
+ <Facts facts={facts} className="md:col-span-4 md:col-start-8" />
405
+ </div>
406
+ </section>
407
+ );
408
+ }
409
+
410
+ return (
411
+ <section ref={root} data-about-studio="plaat" className={`relative overflow-hidden bg-surface-0 pb-[16svh] pt-[10svh] text-ink ${className}`}>
412
+ {first ? (
413
+ <figure className="mb-[10svh] md:mb-[14svh]">
414
+ <div data-as-frame="" className="relative aspect-[4/3] w-full overflow-hidden md:aspect-[21/9]">
415
+ {/* The photograph sits on a layer larger than its frame, so its drift never shows an edge. */}
416
+ <div data-as-layer="" data-as-photo="" className="absolute inset-x-0 -bottom-[10%] -top-[10%]">
417
+ <Image src={first.src} alt={first.alt} fill sizes="100vw" unoptimized={first.unoptimized} className="object-cover" />
418
+ </div>
419
+ </div>
420
+ <figcaption className="mt-4 px-6 text-small text-ink-muted md:px-[4vw]">{first.alt}</figcaption>
421
+ </figure>
422
+ ) : null}
423
+ <div className="grid gap-y-12 px-6 md:grid-cols-12 md:gap-x-[2vw] md:px-[4vw]">
424
+ <h2 data-as-head="" className={`${DISPLAY} max-w-[15ch] text-[clamp(2.25rem,1rem+3.6vw,5.5rem)] md:col-span-7`}>
425
+ {statement}
426
+ </h2>
427
+ <div data-as-copy="" className="md:col-span-4 md:col-start-9 md:pt-3">
428
+ <p data-as-soft="" className="max-w-[40ch] text-lead text-ink-muted">
429
+ {text}
430
+ </p>
431
+ <Facts facts={facts} className="mt-10 md:mt-14" />
432
+ </div>
433
+ </div>
434
+ </section>
435
+ );
436
+ }