{v.benefit}
} + {frame > fps * 18 && {v.proof}} + +{v.benefit}
++ {lines.map((line, i) => { + const enter = spring({ frame: frame - startAt - i * stagger, fps, + config: { damping: 200 } }); // 0→1, no overshoot + return ( + + {line} + + ); + })} ++ ); +}; +``` + +Keep one motion language: same easing, same 14px rise, every line. Speed reads as confidence only when it's consistent. See `references/quote-card.md` for the full composition. + +## Kinetic emphasis on the key phrase + +Pick the single phrase that carries the testimonial ("saved us 10 hours a week") and let it arrive *after* its line has settled — a highlight sweep, a weight shift, or a color pop. Emphasis competing with the reveal cancels both. + +```jsx +const sweep = interpolate(frame, [hlStart, hlStart + 10], [0, 100], + { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); +// a highlighter wiping in behind the phrase, left→right +{phrase} +``` + +Emphasize at most one span per quote. Two highlights is no highlight. + +## Animated star rating (fractional) + +Render the real score, including halves. Use one full row of star glyphs as a mask, then wipe a filled layer to `rating / max` width — this handles 4.5 as cleanly as 5.0, with no half-star sprite. + +```jsx +const StarRating = ({ rating, max = 5, fillStart = 30, fillDur = 14 }) => { + const frame = useCurrentFrame(); + const target = (rating / max) * 100; + const w = interpolate(frame, [fillStart, fillStart + fillDur], [0, target], + { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); + return ( +
+ {lines.map((line, i) => { + const enter = spring({ frame: frame - (8 + i * 6), fps, + config: { damping: 200 } }); + return ( + ++ + {/* Stars */} ++ + ); + })} +
+ We shipped in days, not months. + Onboarding saved us 10 hours a week. ++
+