/** * — THE horizontal card-strip engine (single source of truth). * * Extracted verbatim from so every strip surface shares ONE * marquee/chevron/seam/measure implementation ("unify means delete * variations"). Two input modes, one engine: * * MODE A — render-prop (`items` + `itemKey` + `renderCard`): full control. * The card receives the strip context (controlled activation, clone flag, * shared player-mount gate) and owns its own clone a11y. Used by * . The engine adds NO cell wrapper in this mode — the * card's root is the track's direct child (`scrollByCard` measures * `track.firstElementChild.offsetWidth`, and the `flex items-stretch gap-4` * sizing contract depends on it). * * MODE B — children (organic): ANY card components as plain JSX children. * Each child is auto-wrapped in a managed cell (fixed width, hover/keyboard * marquee-pause, clone `aria-hidden` + focus-suppressed but still CLICKABLE, * row-height stretch). * Adding a new card type requires ZERO strip code. Contract: * (a) pass an ARRAY of children (`{rows.map(...)}`) — `Children.toArray` * does NOT flatten through a single child (it would become * one giant cell; a dev-only warning fires); * (b) when `autoScroll` is on, children must be ref-free / unique-DOM-id- * free / mount-effect-free — the clone copy mounts the same element a * second time (double-fired effects, duplicate ids, last-mounted ref). * * Activation invariant: hover/focus activation is wired in exactly ONE place — * the managed cell (children mode) OR the card itself (render-prop mode, * e.g. ) — never both. * * Marquee state model (explicit — do not "simplify" into one timer): * the rAF advances only when the pause-reason set {cardHovered, offViewport, * tabHidden, reducedMotion} is empty AND `now > max(chevronSuppressUntil, * userScrollSuppressUntil)`. cardHovered means the pointer is over a CARD * (incl. its overlay) — leaving the card resumes the marquee immediately. * Chevron clicks and manual wheel/touch each set their own suppress-until * timestamp; any card-level leave-grace is a separate concern that never * touches marquee state. */ import React from 'react'; /** Per-card strip context passed to `renderCard` (and used internally by the * children-mode managed cell). */ export interface CardStripRenderCtx { /** Item index (copy-independent — the clone copy repeats the same indices). */ index: number; /** `${itemKey}__${copyIndex}__${index}` — unique per rendered card, even for * duplicate items (the trailing index disambiguates). */ cardKey: string; isClone: boolean; isTouch: boolean; /** Engine's (max-width: 767px) media query. */ isMobile: boolean; /** activeKey === cardKey (controlled hover/focus activation). */ active: boolean; onActivate: (key: string) => void; onDeactivate: (key: string) => void; /** Shared-by-index 500px near-viewport gate (opt-in — only meaningful when * the card registers via `rootRef`). */ mounted: boolean; /** Registers the card root with the mount-gate IntersectionObserver. * May return a cleanup (React 19 ref contract). */ rootRef: (el: HTMLDivElement | null) => (() => void) | undefined; } interface CardsStripEngineProps { /** Section heading. Omit `title` (or set `showTitle={false}`) to hide. */ title?: string; showTitle?: boolean; /** Custom node rendered between the heading and the strip. */ headerSlot?: React.ReactNode; /** Marquee auto-scroll. Auto-disabled on no-overflow and * prefers-reduced-motion. Pass `autoScroll={false}` for chevron-style * entity strips — the default stays `true` for bites-contract parity. */ autoScroll?: boolean; /** Marquee speed in px/s. Default 60 — marquee libraries (e.g. GSAP marquee) * default around 100px/s; 60 keeps drift lively while cards stay easy to * hover-target. 60px/s ≈ 1px per 60Hz frame, the smoothest integer step. */ autoScrollSpeed?: number; /** Pause the marquee while a CARD is hovered (resumes as soon as the * pointer leaves the card — strip whitespace/heading never pauses). */ pauseOnHover?: boolean; /** Floating prev/next buttons. Hidden automatically when nothing overflows. */ showChevrons?: boolean; /** Chevron aria-labels. */ prevLabel?: string; nextLabel?: string; /** Merged into the engine's wrapper ref (e.g. `useVideoWarmup().ref`). */ rootRef?: (el: HTMLDivElement | null) => void; className?: string; } /** MODE A — render-prop (advanced): full ctx control, no cell wrapper. */ interface RenderCardMode { items: ReadonlyArray; itemKey: (item: T, index: number) => string; renderCard: (item: T, ctx: CardStripRenderCtx) => React.ReactNode; children?: never; cardWidthDesktop?: never; cardWidthMobile?: never; } /** MODE B — children (organic): ANY card components, zero registration. */ interface ChildrenMode { children: React.ReactNode; /** Managed cell width in px per breakpoint (Figma: 400 desktop). */ cardWidthDesktop?: number; cardWidthMobile?: number; items?: never; itemKey?: never; renderCard?: never; } export type CardsStripProps = CardsStripEngineProps & (RenderCardMode | ChildrenMode); /** Single source for the strip cell width cap (shared with ). */ export declare const STRIP_CELL_MAX_WIDTH = "90vw"; export declare function CardsStrip(props: CardsStripProps): React.ReactElement | null; export {}; //# sourceMappingURL=cards-strip.d.ts.map