diff --git a/.claude/skills/convert-interact-pattern/SKILL.md b/.claude/skills/convert-interact-pattern/SKILL.md new file mode 100644 index 0000000..e16eda1 --- /dev/null +++ b/.claude/skills/convert-interact-pattern/SKILL.md @@ -0,0 +1,128 @@ +--- +name: convert-interact-pattern +description: Convert a standalone HTML/CSS/JS @wix/interact animation demo into a structured prose pattern guideline — selector roles, required styles, adaptation notes, a selector contract, suggested controls, and an interact template. Handles both generic and Wix compositions. Returns the guideline inline in chat or writes it to a markdown file. Use when turning an interact demo into a reusable, adaptable animation pattern spec. +--- + +# Convert Interact Pattern + +Turn a standalone `@wix/interact` animation demo (HTML + CSS + JS) into a **plain-prose guideline** that +describes the animation as a reusable, adaptable pattern: which DOM roles matter, which styles are +structural, how to adapt it to a real composition, and the interact config to reproduce it. + +This skill is self-contained. It does not depend on any particular repository or build step. + +## Output + +When you finish, deliver the guideline in whichever way fits the request: + +- **Inline in chat** — print the full markdown as your answer (default when the user just wants the result). +- **As a file** — write it to `.guideline.md` in the current working directory, or to a path the + user specifies. + +If the user hasn't said which, ask briefly or default to inline. Do not assume any repo layout or run any +project build/typecheck commands. + +## Output Contract + +Read the bundled exemplar **`reference/example.guideline.md`** (in this skill's folder) and match its +structure exactly. The guideline MUST have, in this order: + +1. `# ` — the H1 display name. +2. A one-line tagline. +3. `## Summary` with these bullets: + - `` - **ID:** `kebab-case-id` `` — a stable id, backtick-wrapped. + - `- **Target shape:** ` — short structural fit guidance (what kind of section it suits). + - `- **Description:** ` +4. `## Demo HTML` — a fenced `html` block showing structure only. +5. `## Selector Contract` — numbered hard selector rules and invalid adaptations. +6. `## Role Guidance` — a `| Role | Guidance |` table of short role-level mapping signals. +7. `## Adaptation Notes` — a numbered list of formulas, edge cases, and force-fit behavior. +8. `## Required Elements` — a `| Key | Role | Demo Selector | Purpose |` table. Choose one of two patterns based on how items are structured in the demo: + - **Fixed items** (`card1`, `card2`, …) — use when the item count is known at authoring time and each element gets its own `data-interact-key`. See `reference/example-fixed.guideline.md`. + - **List container** (`listContainer`) — use when items are dynamic or grabbed at runtime from a parent. A single `listContainer` key points to the parent; a `customEffect` iterates over its children. See `reference/example-list.guideline.md`. +9. `## Required Styles` — one `### \`role\` — \`selector\`` block per style, each with a fenced `css` + block (real kebab-case CSS) and a `Reason:` line. +10. `## Suggested Controls` — see below; always present, never empty. +11. `## Interact Template` — the config as fenced `ts` blocks (helper functions + the interaction), + using the required-element keys, not raw demo selectors. + +Keep the `# ` H1, the `**ID:**` line, and the `**Target shape:**` line in exactly that format — they form +the machine-readable summary a catalog can parse out of the file. + +## Workflow + +1. **Determine target platform.** Ask the user whether the target composition is a Wix site, or infer + from context if it's already clear. This controls whether **Wix DOM Mapping** applies throughout the + rest of the steps. +2. Read the demo HTML, CSS, and JS, and read the bundled `reference/example.guideline.md` for format. +3. Identify the animation mechanism, not the decoration: scroll source, sticky or moving stages, repeated items, transforms, ranges, and any dynamic formulas. +4. Convert demo selectors into reusable roles. Prefer roles like `scrollSource`, `stickyStage`, `stickyFrame`, `collection`, `stackList`, `horizontalTrack`, and `repeatedCard`. If Wix, also note the corresponding Wix selector for each role (see **Wix DOM Mapping** below). +5. Write `## Selector Contract` before `## Role Guidance` for non-negotiable mapping rules and known invalid adaptations. If Wix, include Wix-specific selector constraints inline. +6. Keep `## Required Elements` to the minimum viable pattern. For repeated items, choose the right pattern: + - **Fixed items**: use when count is known. Require three items minimum; keys MUST end in a digit (`card1`, `card2`, …) so they compact into `card{n}`; instruct adaptation to extend `card4..cardN`; add a repeated-card-keys note after the table. + - **List container**: use when items are dynamic or the count is not fixed. A single `listContainer` key points to the parent; document how the `customEffect` selects children. + If Wix, add Wix selector guidance in the Purpose column. +7. Put structural CSS in `## Required Styles`; keep visual styling out unless it is required for animation correctness. +8. Convert imperative demo JS into a static `## Interact Template` with helper functions. Use illustrative values and tell the agent which values must be recomputed. +9. Always author `## Suggested Controls` (see rules below). +10. Keep the guidance lean. Prefer 3-5 high-signal bullets per section and do not restate the same constraint across Selector Contract, Role Guidance, and Adaptation Notes. +11. Output the finished guideline (inline or as a file, per **Output** above). + +## Wix DOM Mapping + +*Apply this section only when the target is a Wix composition.* + +Translate generic roles to Wix selectors using these rules: + +- **`scrollSource`** — the outermost section comp: `#comp-` (the element whose scroll drives the `viewProgress` trigger). +- **`stickyStage`** — internal-container-root: `#comp-` with `data-testid="internal-container-root"`. This is the sticky outer wrapper, not its content child. Use the rendered `#comp-...` id, not `DESKTOP--...`. +- **`collection`** — internal-container-content: `# [data-testid="internal-container-content"]`. This must always be a child of `stickyStage` and must resolve to a different element. +- **Repeated items** — use rendered `#comp-` ids. Never use `DESKTOP--...` ids; those are editor-internal and absent from the live DOM. + +Before generating the guideline, confirm in the DOM that `stickyStage` and `collection` resolve to different elements. If they collapse to the same selector, reject the pattern (see Extraction Rules). + +## Suggested Controls + +Every guideline must name **1-3 suggested controls** — the pattern's core user-facing knobs +(spread, spacing, intensity, scroll distance, perspective, scale, tilt, offset, duration), chosen +for quality over quantity. Describe each knob *conceptually* — what it changes and a sensible range. +How a knob becomes a valid binding is the generator's job, not this guideline's; do not specify +binding targets, transforms, or the interact binding model here. Author a one-line intro, then +one `### ` block per control with these bullets: + +- `**Label:** ` +- `**Group:** <Layout | Motion | ...>` +- `**Type:** range | select | color | toggle | text` +- `**Default:** <value>` +- `**Description:** <what the knob changes>` +- `**Constraints:** min / max / step / unit` (as applicable) +- `**Suggested variable:** <--kebab-name>` (optional) — a CSS custom property the pattern's styles or + keyframes could read, offered as a hint for the generator. This is a naming suggestion, not a binding. + +Pick knobs that are stable and safe to expose; avoid incidental demo numbers that would break the +pattern when changed. + +## Extraction Rules + +- Preserve role ownership. Scroll distance belongs to the scroll source; sticky pinning belongs to a shared frame/stage; collection sizing belongs to the collection/track; item transforms belong to repeated item roots. +- Treat literal demo numbers as examples. Translate them into formulas or adaptation notes when they depend on viewport, item count, card size, gap, or layout. +- Keep DOM roles distinct when the pattern depends on them. If a pattern needs `stickyStage` and `collection`, their selectors must not collapse to the same element. +- Use specific selectors for repeated items. Avoid broad selectors like `.image` unless the section truly has no better mapping. +- Target repeated item roots, not raw `img` descendants, unless the animation specifically operates on image content inside a stable card root. +- For repeated items, prefer formula-driven guidance over serializing one near-identical style entry per card when the placement can be derived from item index and count. +- Use `overflow: clip` for viewProgress clipping roles. Avoid `overflow: hidden` on viewProgress ancestors. +- Include forced-fit guidance only when CSS can safely create the missing local structure without moving DOM nodes. +- State invalid adaptations explicitly, but once is enough. The agent needs crisp negative examples, not repeated warnings. +- **Reject and report** when a pattern's structural invariant cannot be satisfied in the target composition (e.g., `stickyStage` and `collection` must remain distinct selectors). Do not produce a guideline for a broken mapping — tell the user which role collapsed and why. + +## Review Checklist + +- The guideline has all eleven sections and the parseable summary block (H1, `**ID:**`, `**Target shape:**`). +- Every Interact Template effect key appears in `## Required Elements`. +- Every structural role used in `## Required Styles` appears in `## Role Guidance`. +- Repeated keys end in a digit and a repeated-card-keys note is present; item-count changes are explained. +- Dynamic distances and ranges say how to recompute them. +- `## Suggested Controls` has 1-3 controls, each with a label, range, and what it changes (no binding spec). +- The guideline warns against the most likely broken mapping. +- *(Wix only)* Wix selector patterns (`#comp-...`, `data-testid`) are present in the Selector Contract and Required Elements Purpose column. +- The result is delivered inline or written to the requested file. diff --git a/.claude/skills/convert-interact-pattern/agents/openai.yaml b/.claude/skills/convert-interact-pattern/agents/openai.yaml new file mode 100644 index 0000000..0010944 --- /dev/null +++ b/.claude/skills/convert-interact-pattern/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Convert Interact Pattern" + short_description: "Convert HTML demos to reusable animation pattern guidelines." + default_prompt: "Convert this full HTML @wix/interact animation demo into a prose pattern guideline with selector roles, adaptation rules, required styles, suggested controls, and an interact template. Ask whether the target is a Wix composition to apply Wix DOM mapping. Return it inline or as a markdown file." diff --git a/.claude/skills/convert-interact-pattern/reference/example-fixed.guideline.md b/.claude/skills/convert-interact-pattern/reference/example-fixed.guideline.md new file mode 100644 index 0000000..7daadce --- /dev/null +++ b/.claude/skills/convert-interact-pattern/reference/example-fixed.guideline.md @@ -0,0 +1,194 @@ +# Card Spread + +Stacked cards fan out horizontally on scroll. + +## Summary + +- **ID:** `card-spread` +- **Target shape:** Best for 3–7 similarly sized sibling items inside a single sticky stage, including mixed-content wrappers where the cards can share one overlapped grid row. +- **Description:** Five cards stacked at the center of the viewport fan out left/right and shrink slightly as the section scrolls past. + +## Demo HTML + +```html +<section class="scroll-section"> + <div class="cards-container-wrapper"> + <div id="cards-collection"> + <h2 class="static-title">Title</h2> + <div id="card-1" class="card">1</div> + <div id="card-2" class="card">2</div> + <div id="card-3" class="card">3</div> + <div id="card-4" class="card">4</div> + <div id="card-5" class="card">5</div> + </div> + </div> +</section> +``` + +## Selector Contract + +1. Role ownership is strict: `scrollSection` owns the timeline, `stickyStage` owns sticky/clipping, `collection` owns the centered inner stage, and `repeatedCard` owns the overlapped card-stage layout plus spread transform. +2. `stickyStage` and `collection` must be different selectors. In Wix, `stickyStage` is the internal-container-root `#comp-...` with `data-testid="internal-container-root"` and `collection` is its `[data-testid="internal-container-content"]` child. +3. If `collection` also contains non-repeated siblings such as titles or copy, keep `collection` as a grid and overlap only the repeated cards in a shared card stage row. Do not convert the whole mixed wrapper to flex. +4. Keep card-spread layout styles on the repeated card roots, not on raw `img` descendants or broad selectors when concrete card component ids exist. +5. Repeated cards share one overlapped stage inside the collection, not sticky items. Use rendered `#comp-...` ids, not `DESKTOP--...` ids. + +## Role Guidance + +| Role | Guidance | +| --- | --- | +| `scrollSource` | The tall section that drives the `viewProgress` trigger. | +| `stickyStage` | A sticky viewport-height wrapper that keeps the cards pinned during scroll. | +| `collection` | The grid layout owner that can keep static siblings in flow while repeated cards share one overlapped card stage. | +| `repeatedCard` | Repeated sibling items that share one overlapped grid cell and then spread horizontally. | + +## Adaptation Notes + +1. Preserve the section root outer layout; the sticky stage and centered collection are inner roles, not section-root roles. +2. Use viewport units only for the outer timeline container and sticky stage. Size cards relative to the collection stage so their proportions stay close to the source composition. +3. If `collection` contains a title or other static siblings, leave them in their own normal grid row and place only the repeated cards into a shared lower grid row so the non-animated content stays untouched. +4. Animate spread with `translateX(...) scale(...)` on the card roots instead of resizing card height unless the real section truly depends on viewport-sized cards. +5. When item count changes, recompute width, spacing, and outer translation distances instead of copying the demo offsets literally. +6. If the outer cards land mostly outside the visible stage, reduce card width or spread distance before returning the result. + +## Required Elements + +| Key | Role | Demo Selector | Purpose | +| --- | --- | --- | --- | +| `scrollSection` | `scrollSource` | `.scroll-section` | The `viewProgress` source for the entire pattern. | +| `stickyStage` | `stickyStage` | `.cards-container-wrapper` | Sticky pin only: `position: sticky`, `100vh`, `overflow: clip`. Wix: `#comp-...` with `data-testid="internal-container-root"` — not the collection. | +| `collection` | `collection` | `#cards-collection` | Centered mixed-content stage for the spread. Wix: `#<stickyStageCompId> [data-testid="internal-container-content"]` — must differ from `stickyStage`. | +| `card1` | `repeatedCard` | `.scroll-section #card-1` | Minimum repeated spread card; extend outward for `card4..cardN`. | +| `card2` | `repeatedCard` | `.scroll-section #card-2` | Repeated spread card. | +| `card3` | `repeatedCard` | `.scroll-section #card-3` | Repeated spread card. | +| `card4` | `repeatedCard` | `.scroll-section #card-4` | Repeated spread card. | +| `card5` | `repeatedCard` | `.scroll-section #card-5` | Repeated spread card. | + +> Repeated card keys must keep their trailing index (`card1`, `card2`, …) so they compact into the `card{n}` group; extend the row as `card4..cardN` for more items. + +## Required Styles + +### `scrollSource` — `.scroll-section` + +```css +.scroll-section { + height: 400vh; +} +``` + +Reason: creates enough scroll distance for the full `viewProgress` spread to play out. + +### `stickyStage` — `.cards-container-wrapper` + +```css +.cards-container-wrapper { + position: sticky; + top: 0; + height: 100vh; + overflow: clip; +} +``` + +Reason: pins the stage to the viewport and clips the spreading cards while the source section scrolls. + +### `collection` — `#cards-collection` + +```css +#cards-collection { + position: relative; + display: grid; + grid-template-columns: 1fr; + grid-template-rows: auto 1fr; + width: 100%; + height: 100vh; + margin: 0 auto; + justify-items: center; +} +``` + +Reason: creates a mixed-content grid stage so static siblings stay in flow while repeated cards overlap in a shared card row. The collection owns the composition space; child card percentages resolve against this stage. + +### `repeatedCard` — `#cards-collection > .card` + +```css +#cards-collection > .card { + grid-column: 1; + grid-row: 2; + place-self: start center; + width: 20vw; + height: 55%; + transform-origin: center center; + will-change: transform; +} +``` + +Reason: overlaps repeated cards in one shared grid cell with top alignment and centered placement before the animation distributes them, preserving their proportion relative to the collection stage. + +### `repeatedCard` — `.card` + +```css +.card { + margin: 0; +} +``` + +Reason: prevents repeated cards from drifting apart because of default spacing. + +## Suggested Controls + +Always expose at least the spread distance and ending scale; add more only when the adapted experience introduces new stable knobs. + +### `spread` + +- **Label:** `Spread` +- **Group:** `Layout` +- **Type:** `range` +- **Default:** `40` +- **Description:** Controls how far the outer cards fan out from the center stage at the end of the scroll range. +- **Constraints:** `min: 20`, `max: 50`, `step: 2`, `unit: vw` +- **Suggested variable:** `--card-spread-unit` + +### `end-scale` + +- **Label:** `Card Scale` +- **Group:** `Layout` +- **Type:** `range` +- **Default:** `0.85` +- **Description:** Controls the ending scale of the cards at maximum spread. +- **Constraints:** `min: 0.7`, `max: 1`, `step: 0.01`, `unit: x` +- **Suggested variable:** `--card-end-scale` + +## Interact Template + +```ts +const RANGE = { + rangeStart: { name: 'cover', offset: { unit: 'percentage', value: 20 } }, + rangeEnd: { name: 'cover', offset: { unit: 'percentage', value: 80 } }, + easing: 'cubic-bezier(0.42, 0, 0.58, 1)', + fill: 'both' as const, +}; + +// Demo offsets for five cards — recompute from real item count and spread. +const SPREAD_TRANSLATIONS = ['-40vw', '-20vw', '0', '20vw', '40vw'] as const; + +// Combined per-card effect: translateX + scale shrink in a single keyframe pair. +const cardSpreadEffect = (key: string, endTranslate: string) => ({ + key, + keyframeEffect: { + name: `${key}-spread`, + keyframes: [ + { transform: 'translateX(0) scale(1)' }, + { transform: `${endTranslate} scale(0.85)` }, + ], + }, + ...RANGE, +}); + +const interaction = { + key: 'scrollSection', + trigger: 'viewProgress', + effects: SPREAD_TRANSLATIONS.map((translate, index) => + cardSpreadEffect(`card${index + 1}`, `translateX(${translate})`), + ), +}; +``` diff --git a/.claude/skills/convert-interact-pattern/reference/example-list.guideline.md b/.claude/skills/convert-interact-pattern/reference/example-list.guideline.md new file mode 100644 index 0000000..7e6ebbf --- /dev/null +++ b/.claude/skills/convert-interact-pattern/reference/example-list.guideline.md @@ -0,0 +1,137 @@ +# List Stagger Reveal + +List items slide up and fade in sequentially as the section enters the viewport. + +## Summary + +- **ID:** `list-stagger-reveal` +- **Target shape:** Best for any vertically stacked list, repeater, or feed of variable-count items where individual item count is not fixed at authoring time. +- **Description:** As the section scrolls into view, each list item staggers in from below with a fade, driven by the section's viewProgress. Item count is dynamic — the pattern reads children from the list container at runtime. + +## Demo HTML + +```html +<section class="list-section"> + <ul id="item-list"> + <li class="item">Item 1</li> + <li class="item">Item 2</li> + <li class="item">Item 3</li> + <!-- any number of additional items --> + </ul> +</section> +``` + +## Selector Contract + +1. `listContainer` and `scrollSource` must be different selectors — the container is a child of the section, not the section itself. +2. The `customEffect` selects direct children of `listContainer` at runtime; do not register individual item keys with `data-interact-key`. +3. Stagger delay is computed from each item's index and total child count — do not hardcode per-item offsets. +4. Do not use `listContainer` for fixed, named elements (titles, CTAs) that require their own independent effect; give those a dedicated key instead. + +## Role Guidance + +| Role | Guidance | +| --- | --- | +| `scrollSource` | The section that drives the `viewProgress` trigger. | +| `listContainer` | The direct parent of all repeated items. Children are selected at runtime via `customEffect`. Wix: use the repeater root `#comp-...` with `data-testid="repeater"`. | + +## Adaptation Notes + +1. The stagger formula is `index / total` — recompute `total` from the live child count, not a hardcoded number. +2. If the list is paginated or lazy-loaded, apply the effect only to the initially rendered children; re-running on new batches requires a separate trigger. +3. For horizontal lists or grids, change the stagger axis from `translateY` to `translateX` or omit the translate entirely and rely on opacity alone. +4. If items have variable height, use `opacity` + a fixed `translateY` offset rather than percentage-based movement to avoid layout shifts. + +## Required Elements + +| Key | Role | Demo Selector | Purpose | +| --- | --- | --- | --- | +| `scrollSection` | `scrollSource` | `.list-section` | The `viewProgress` source for the entire pattern. | +| `listContainer` | `listContainer` | `#item-list` | Parent of all repeated items. Children are iterated in the `customEffect` at runtime — no per-item `data-interact-key` needed. Wix: repeater root `#comp-...` with `data-testid="repeater"`. | + +> This pattern uses a single `listContainer` key instead of numbered item keys. The `customEffect` queries `listContainer.children` at runtime, so item count can be dynamic. + +## Required Styles + +### `scrollSource` — `.list-section` + +```css +.list-section { + min-height: 100vh; +} +``` + +Reason: ensures the section has enough height to produce a meaningful `viewProgress` range as it enters the viewport. + +### `listContainer` — `#item-list` + +```css +#item-list { + list-style: none; + margin: 0; + padding: 0; +} +``` + +Reason: removes default list chrome that would interfere with item positioning. + +### `item` — `#item-list > .item` + +```css +#item-list > .item { + will-change: transform, opacity; +} +``` + +Reason: promotes items to their own layer ahead of the animation to avoid paint during scroll. + +## Suggested Controls + +### `stagger` + +- **Label:** `Stagger` +- **Group:** `Motion` +- **Type:** `range` +- **Default:** `0.15` +- **Description:** Controls the delay between each item's entrance, as a fraction of the total scroll range. +- **Constraints:** `min: 0.05`, `max: 0.4`, `step: 0.05`, `unit: fraction` +- **Suggested variable:** `--list-stagger-fraction` + +### `slide-distance` + +- **Label:** `Slide Distance` +- **Group:** `Motion` +- **Type:** `range` +- **Default:** `32` +- **Description:** How far each item travels upward during its entrance. +- **Constraints:** `min: 0`, `max: 80`, `step: 4`, `unit: px` +- **Suggested variable:** `--list-slide-distance` + +## Interact Template + +```ts +// Stagger fraction: each item's window is offset by this amount of the total range. +const STAGGER = 0.15; + +const interaction = { + key: 'scrollSection', + trigger: 'viewProgress', + effects: [{ + key: 'listContainer', + customEffect: (container, progress) => { + const items = Array.from(container.children); + const total = items.length; + + items.forEach((item, index) => { + // Each item gets its own [start, end] window within [0, 1]. + const start = (index / total) * (1 - STAGGER); + const end = start + STAGGER + (1 - STAGGER) / total; + const itemProgress = Math.max(0, Math.min(1, (progress - start) / (end - start))); + + (item as HTMLElement).style.opacity = String(itemProgress); + (item as HTMLElement).style.transform = `translateY(${(1 - itemProgress) * 32}px)`; + }); + }, + }], +}; +``` diff --git a/.claude/skills/convert-interact-pattern/reference/example.guideline.md b/.claude/skills/convert-interact-pattern/reference/example.guideline.md new file mode 100644 index 0000000..7daadce --- /dev/null +++ b/.claude/skills/convert-interact-pattern/reference/example.guideline.md @@ -0,0 +1,194 @@ +# Card Spread + +Stacked cards fan out horizontally on scroll. + +## Summary + +- **ID:** `card-spread` +- **Target shape:** Best for 3–7 similarly sized sibling items inside a single sticky stage, including mixed-content wrappers where the cards can share one overlapped grid row. +- **Description:** Five cards stacked at the center of the viewport fan out left/right and shrink slightly as the section scrolls past. + +## Demo HTML + +```html +<section class="scroll-section"> + <div class="cards-container-wrapper"> + <div id="cards-collection"> + <h2 class="static-title">Title</h2> + <div id="card-1" class="card">1</div> + <div id="card-2" class="card">2</div> + <div id="card-3" class="card">3</div> + <div id="card-4" class="card">4</div> + <div id="card-5" class="card">5</div> + </div> + </div> +</section> +``` + +## Selector Contract + +1. Role ownership is strict: `scrollSection` owns the timeline, `stickyStage` owns sticky/clipping, `collection` owns the centered inner stage, and `repeatedCard` owns the overlapped card-stage layout plus spread transform. +2. `stickyStage` and `collection` must be different selectors. In Wix, `stickyStage` is the internal-container-root `#comp-...` with `data-testid="internal-container-root"` and `collection` is its `[data-testid="internal-container-content"]` child. +3. If `collection` also contains non-repeated siblings such as titles or copy, keep `collection` as a grid and overlap only the repeated cards in a shared card stage row. Do not convert the whole mixed wrapper to flex. +4. Keep card-spread layout styles on the repeated card roots, not on raw `img` descendants or broad selectors when concrete card component ids exist. +5. Repeated cards share one overlapped stage inside the collection, not sticky items. Use rendered `#comp-...` ids, not `DESKTOP--...` ids. + +## Role Guidance + +| Role | Guidance | +| --- | --- | +| `scrollSource` | The tall section that drives the `viewProgress` trigger. | +| `stickyStage` | A sticky viewport-height wrapper that keeps the cards pinned during scroll. | +| `collection` | The grid layout owner that can keep static siblings in flow while repeated cards share one overlapped card stage. | +| `repeatedCard` | Repeated sibling items that share one overlapped grid cell and then spread horizontally. | + +## Adaptation Notes + +1. Preserve the section root outer layout; the sticky stage and centered collection are inner roles, not section-root roles. +2. Use viewport units only for the outer timeline container and sticky stage. Size cards relative to the collection stage so their proportions stay close to the source composition. +3. If `collection` contains a title or other static siblings, leave them in their own normal grid row and place only the repeated cards into a shared lower grid row so the non-animated content stays untouched. +4. Animate spread with `translateX(...) scale(...)` on the card roots instead of resizing card height unless the real section truly depends on viewport-sized cards. +5. When item count changes, recompute width, spacing, and outer translation distances instead of copying the demo offsets literally. +6. If the outer cards land mostly outside the visible stage, reduce card width or spread distance before returning the result. + +## Required Elements + +| Key | Role | Demo Selector | Purpose | +| --- | --- | --- | --- | +| `scrollSection` | `scrollSource` | `.scroll-section` | The `viewProgress` source for the entire pattern. | +| `stickyStage` | `stickyStage` | `.cards-container-wrapper` | Sticky pin only: `position: sticky`, `100vh`, `overflow: clip`. Wix: `#comp-...` with `data-testid="internal-container-root"` — not the collection. | +| `collection` | `collection` | `#cards-collection` | Centered mixed-content stage for the spread. Wix: `#<stickyStageCompId> [data-testid="internal-container-content"]` — must differ from `stickyStage`. | +| `card1` | `repeatedCard` | `.scroll-section #card-1` | Minimum repeated spread card; extend outward for `card4..cardN`. | +| `card2` | `repeatedCard` | `.scroll-section #card-2` | Repeated spread card. | +| `card3` | `repeatedCard` | `.scroll-section #card-3` | Repeated spread card. | +| `card4` | `repeatedCard` | `.scroll-section #card-4` | Repeated spread card. | +| `card5` | `repeatedCard` | `.scroll-section #card-5` | Repeated spread card. | + +> Repeated card keys must keep their trailing index (`card1`, `card2`, …) so they compact into the `card{n}` group; extend the row as `card4..cardN` for more items. + +## Required Styles + +### `scrollSource` — `.scroll-section` + +```css +.scroll-section { + height: 400vh; +} +``` + +Reason: creates enough scroll distance for the full `viewProgress` spread to play out. + +### `stickyStage` — `.cards-container-wrapper` + +```css +.cards-container-wrapper { + position: sticky; + top: 0; + height: 100vh; + overflow: clip; +} +``` + +Reason: pins the stage to the viewport and clips the spreading cards while the source section scrolls. + +### `collection` — `#cards-collection` + +```css +#cards-collection { + position: relative; + display: grid; + grid-template-columns: 1fr; + grid-template-rows: auto 1fr; + width: 100%; + height: 100vh; + margin: 0 auto; + justify-items: center; +} +``` + +Reason: creates a mixed-content grid stage so static siblings stay in flow while repeated cards overlap in a shared card row. The collection owns the composition space; child card percentages resolve against this stage. + +### `repeatedCard` — `#cards-collection > .card` + +```css +#cards-collection > .card { + grid-column: 1; + grid-row: 2; + place-self: start center; + width: 20vw; + height: 55%; + transform-origin: center center; + will-change: transform; +} +``` + +Reason: overlaps repeated cards in one shared grid cell with top alignment and centered placement before the animation distributes them, preserving their proportion relative to the collection stage. + +### `repeatedCard` — `.card` + +```css +.card { + margin: 0; +} +``` + +Reason: prevents repeated cards from drifting apart because of default spacing. + +## Suggested Controls + +Always expose at least the spread distance and ending scale; add more only when the adapted experience introduces new stable knobs. + +### `spread` + +- **Label:** `Spread` +- **Group:** `Layout` +- **Type:** `range` +- **Default:** `40` +- **Description:** Controls how far the outer cards fan out from the center stage at the end of the scroll range. +- **Constraints:** `min: 20`, `max: 50`, `step: 2`, `unit: vw` +- **Suggested variable:** `--card-spread-unit` + +### `end-scale` + +- **Label:** `Card Scale` +- **Group:** `Layout` +- **Type:** `range` +- **Default:** `0.85` +- **Description:** Controls the ending scale of the cards at maximum spread. +- **Constraints:** `min: 0.7`, `max: 1`, `step: 0.01`, `unit: x` +- **Suggested variable:** `--card-end-scale` + +## Interact Template + +```ts +const RANGE = { + rangeStart: { name: 'cover', offset: { unit: 'percentage', value: 20 } }, + rangeEnd: { name: 'cover', offset: { unit: 'percentage', value: 80 } }, + easing: 'cubic-bezier(0.42, 0, 0.58, 1)', + fill: 'both' as const, +}; + +// Demo offsets for five cards — recompute from real item count and spread. +const SPREAD_TRANSLATIONS = ['-40vw', '-20vw', '0', '20vw', '40vw'] as const; + +// Combined per-card effect: translateX + scale shrink in a single keyframe pair. +const cardSpreadEffect = (key: string, endTranslate: string) => ({ + key, + keyframeEffect: { + name: `${key}-spread`, + keyframes: [ + { transform: 'translateX(0) scale(1)' }, + { transform: `${endTranslate} scale(0.85)` }, + ], + }, + ...RANGE, +}); + +const interaction = { + key: 'scrollSection', + trigger: 'viewProgress', + effects: SPREAD_TRANSLATIONS.map((translate, index) => + cardSpreadEffect(`card${index + 1}`, `translateX(${translate})`), + ), +}; +```