sticky-card-stack 0.1.2 → 0.1.4

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/README.md CHANGED
@@ -20,8 +20,8 @@ npm install react react-dom gsap lenis
20
20
  2. Wrap your cards in **`StickCardStack`** and use **`StickCard`** for each card.
21
21
 
22
22
  ```tsx
23
- import { StickCardStack, StickCard, useScrollTrigger } from 'scs';
24
- import 'scs/style.css'; // optional: default card layout and demo styles
23
+ import { StickCardStack, StickCard, useScrollTrigger } from 'sticky-card-stack';
24
+ import 'sticky-card-stack/style.css';
25
25
 
26
26
  function App() {
27
27
  useScrollTrigger();
@@ -43,6 +43,8 @@ function App() {
43
43
  }
44
44
  ```
45
45
 
46
+ All library CSS classes use the **`scs-`** prefix (e.g. `scs-wrap`, `scs-stack`, `scs-card-demo`) so they won’t clash with your own styles.
47
+
46
48
  ## API
47
49
 
48
50
  ### `useScrollTrigger()`
package/dist/scs.css CHANGED
@@ -1 +1 @@
1
- .stick-card-stack-wrap{position:relative}.stick-card-stack{position:sticky;top:0;height:100svh;width:100%;background-color:#e3e3db}.stick-card-stack .stick-card-stack-inner{position:absolute;inset:0;perspective:850px}.stick-card-stack .stick-card{position:absolute;top:50%;left:50%;width:65%;height:60%;display:flex;justify-content:center;align-items:center;padding:2.5rem;border-radius:1rem;transform-origin:center bottom;will-change:transform}@media(max-width:1024px){.stick-card-stack .stick-card{width:calc(100% - 4rem);height:75%}}
1
+ .scs-wrap{position:relative}.scs-stack{position:sticky;top:0;height:100svh;width:100%;background-color:#e3e3db}.scs-stack .scs-stack-inner{position:absolute;inset:0;perspective:850px}.scs-stack .scs-card{position:absolute;top:50%;left:50%;width:65%;height:60%;display:flex;justify-content:center;align-items:center;padding:2.5rem;border-radius:1rem;transform-origin:center bottom;will-change:transform}@media(max-width:1024px){.scs-stack .scs-card{width:calc(100% - 4rem);height:75%}}.scs-card-demo{display:flex;width:100%;height:100%;color:#fff;gap:1rem}.scs-card-demo .scs-col{flex:1;height:100%}.scs-card-demo .scs-col.scs-col-leading{display:flex;flex-direction:column;height:100%;padding:.5rem}.scs-card-demo .scs-col-header{flex:0 0 20%;min-height:0}.scs-card-demo .scs-col-content{margin-top:auto;flex:0 0 auto;max-height:22%;min-height:0}.scs-card-demo .scs-col.scs-col-main{border-radius:.75rem;overflow:hidden}.scs-card-demo.scs-card-1{background-color:#3d2fa9;z-index:5}.scs-card-demo.scs-card-2{background-color:#f72;z-index:4}.scs-card-demo.scs-card-3{background-color:#ff3d33;z-index:3}.scs-card-demo.scs-card-4{background-color:#785f47;z-index:2}@media(max-width:1024px){.scs-card-demo{flex-direction:column}.scs-card-demo .scs-col{width:100%}}
package/dist/scs.js CHANGED
@@ -3,7 +3,7 @@ import { createContext as O, useRef as T, useEffect as A, Children as p, useCont
3
3
  import t from "gsap";
4
4
  import { ScrollTrigger as h } from "gsap/ScrollTrigger";
5
5
  import X from "lenis";
6
- const w = {
6
+ const y = {
7
7
  scrollHeight: 8,
8
8
  cardYOffset: 5,
9
9
  cardScaleStep: 0.075
@@ -11,50 +11,50 @@ const w = {
11
11
  t.registerPlugin(h);
12
12
  function M({
13
13
  children: e,
14
- scrollHeight: n = w.scrollHeight,
15
- cardYOffset: c = w.cardYOffset,
16
- cardScaleStep: i = w.cardScaleStep,
17
- className: o,
14
+ scrollHeight: n = y.scrollHeight,
15
+ cardYOffset: c = y.cardYOffset,
16
+ cardScaleStep: l = y.cardScaleStep,
17
+ className: s,
18
18
  colorVariants: m
19
19
  }) {
20
- const y = T(null), x = T(null);
20
+ const x = T(null), P = T(null);
21
21
  A(() => {
22
- const g = y.current, l = x.current;
23
- if (!g || !l) return;
24
- const s = l.querySelectorAll(".stick-card"), a = s.length;
22
+ const g = x.current, o = P.current;
23
+ if (!g || !o) return;
24
+ const i = o.querySelectorAll(".scs-card"), a = i.length;
25
25
  if (a === 0) return;
26
26
  const d = 1 / a;
27
- s.forEach((v, u) => {
28
- t.set(v, {
27
+ i.forEach((C, u) => {
28
+ t.set(C, {
29
29
  xPercent: -50,
30
30
  yPercent: -50 + u * c,
31
- scale: 1 - u * i
31
+ scale: 1 - u * l
32
32
  });
33
33
  });
34
34
  const b = window.innerHeight * n, E = h.create({
35
35
  trigger: g,
36
36
  start: "top top",
37
37
  end: `+=${b}px`,
38
- onUpdate(v) {
39
- const u = v.progress, f = Math.min(
38
+ onUpdate(C) {
39
+ const u = C.progress, f = Math.min(
40
40
  Math.floor(u / d),
41
41
  a - 1
42
- ), k = (u - f * d) / d;
43
- s.forEach((C, S) => {
44
- if (S < f)
45
- t.set(C, {
42
+ ), v = (u - f * d) / d;
43
+ i.forEach((S, w) => {
44
+ if (w < f)
45
+ t.set(S, {
46
46
  yPercent: -250,
47
47
  rotationX: 35
48
48
  });
49
- else if (S === f)
50
- t.set(C, {
51
- yPercent: t.utils.interpolate(-50, -200, k),
52
- rotationX: t.utils.interpolate(0, 35, k),
49
+ else if (w === f)
50
+ t.set(S, {
51
+ yPercent: t.utils.interpolate(-50, -200, v),
52
+ rotationX: t.utils.interpolate(0, 35, v),
53
53
  scale: 1
54
54
  });
55
55
  else {
56
- const P = S - f, _ = (P - k) * c, $ = 1 - (P - k) * i;
57
- t.set(C, {
56
+ const k = w - f, _ = (k - v) * c, $ = 1 - (k - v) * l;
57
+ t.set(S, {
58
58
  yPercent: -50 + _,
59
59
  rotationX: 0,
60
60
  scale: $
@@ -66,23 +66,23 @@ function M({
66
66
  return () => {
67
67
  E.kill();
68
68
  };
69
- }, [n, c, i]);
69
+ }, [n, c, l]);
70
70
  const I = {
71
71
  height: `calc(100svh * ${n})`
72
72
  };
73
73
  return /* @__PURE__ */ r(
74
74
  "div",
75
75
  {
76
- ref: y,
77
- className: o ? `stick-card-stack-wrap ${o}` : "stick-card-stack-wrap",
76
+ ref: x,
77
+ className: s ? `scs-wrap ${s}` : "scs-wrap",
78
78
  style: I,
79
- children: /* @__PURE__ */ r("section", { className: "stick-card-stack", children: /* @__PURE__ */ r("div", { ref: x, className: "stick-card-stack-inner", children: p.map(e, (g, l) => {
80
- const s = p.count(e), a = l % j + 1, d = m && m.length > 0 ? m[l % m.length] : null;
79
+ children: /* @__PURE__ */ r("section", { className: "scs-stack", children: /* @__PURE__ */ r("div", { ref: P, className: "scs-stack-inner", children: p.map(e, (g, o) => {
80
+ const i = p.count(e), a = o % j + 1, d = m && m.length > 0 ? m[o % m.length] : null;
81
81
  return /* @__PURE__ */ r(R.Provider, { value: { variant: a, color: d }, children: /* @__PURE__ */ r(
82
82
  "div",
83
83
  {
84
- className: "stick-card",
85
- style: { zIndex: s - l },
84
+ className: "scs-card",
85
+ style: { zIndex: i - o },
86
86
  children: g
87
87
  }
88
88
  ) });
@@ -91,18 +91,18 @@ function M({
91
91
  );
92
92
  }
93
93
  function q(e) {
94
- const { colorOverride: n } = e, c = U(R), i = c?.variant ?? 1, o = n ?? c?.color ?? null;
94
+ const { colorOverride: n } = e, c = U(R), l = c?.variant ?? 1, s = n ?? c?.color ?? null;
95
95
  return /* @__PURE__ */ r(
96
96
  "div",
97
97
  {
98
- className: o ? "card-demo" : `card-demo card-${i}`,
99
- style: o ? { backgroundColor: o } : void 0,
98
+ className: s ? "scs-card-demo" : `scs-card-demo scs-card-${l}`,
99
+ style: s ? { backgroundColor: s } : void 0,
100
100
  children: "children" in e && e.children != null ? e.children : /* @__PURE__ */ N(H, { children: [
101
- /* @__PURE__ */ N("div", { className: "col col-leading", children: [
102
- /* @__PURE__ */ r("div", { className: "col-header", children: "header" in e ? e.header : null }),
103
- /* @__PURE__ */ r("div", { className: "col-content", children: "content" in e ? e.content : null })
101
+ /* @__PURE__ */ N("div", { className: "scs-col scs-col-leading", children: [
102
+ /* @__PURE__ */ r("div", { className: "scs-col-header", children: "header" in e ? e.header : null }),
103
+ /* @__PURE__ */ r("div", { className: "scs-col-content", children: "content" in e ? e.content : null })
104
104
  ] }),
105
- /* @__PURE__ */ r("div", { className: "col col-main", children: "main" in e ? e.main : null })
105
+ /* @__PURE__ */ r("div", { className: "scs-col scs-col-main", children: "main" in e ? e.main : null })
106
106
  ] })
107
107
  }
108
108
  );
@@ -131,7 +131,7 @@ function B() {
131
131
  }, []);
132
132
  }
133
133
  export {
134
- w as STICK_CARD_STACK_DEFAULTS,
134
+ y as STICK_CARD_STACK_DEFAULTS,
135
135
  q as StickCard,
136
136
  M as StickCardStack,
137
137
  B as useScrollTrigger
package/dist/scs.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"scs.js","sources":["../src/lib/types.ts","../src/lib/StickCardStack.tsx","../src/lib/StickCard.tsx","../src/lib/useScrollTrigger.ts"],"sourcesContent":["import type { ReactNode } from 'react';\n\n/** Props for the StickCardStack root component */\nexport interface StickCardStackProps {\n /** Card elements; each child is rendered as one stacked card */\n children: ReactNode;\n /**\n * Height of the scroll area as a multiple of viewport height.\n * @default 8\n */\n scrollHeight?: number;\n /**\n * Vertical offset between stacked cards (in percent).\n * @default 5\n */\n cardYOffset?: number;\n /**\n * Scale reduction per card in the stack (e.g. 0.075 = 7.5% smaller per card).\n * @default 0.075\n */\n cardScaleStep?: number;\n /** Optional class name for the wrapper element */\n className?: string;\n /**\n * Optional list of background colors (e.g. ['#3d2fa9', '#ff7722']).\n * Applied per card by index (cycles if fewer colors than cards).\n * When not provided, cards use the base CSS variables (.card-1 … .card-4).\n */\n colorVariants?: string[];\n}\n\n/** Common props for StickCard */\nexport interface StickCardPropsBase {\n /**\n * Custom background color for this card (e.g. '#3d2fa9').\n * When set, overrides stack context (variant/colorVariants) and uses this color only.\n */\n colorOverride?: string | null;\n}\n\n/** Structured card: header (20% top), content (22% at bottom of left column), and main column. */\nexport interface StickCardPropsStructured extends StickCardPropsBase {\n /** Optional header slot at top of left column (e.g. \"Join us\"). */\n header: ReactNode;\n /** Content slot: primary text in left column (e.g. \"Football\"). */\n content: ReactNode;\n /** Main slot: trailing column (e.g. image, media). */\n main: ReactNode;\n}\n\n/** Custom layout: you supply children and structure the card yourself. */\nexport interface StickCardPropsChildren extends StickCardPropsBase {\n children: ReactNode;\n}\n\n/** Props for a single StickCard: either structured (header, content, main) or custom (children). */\nexport type StickCardProps = StickCardPropsStructured | StickCardPropsChildren;\n\n/** Default values for StickCardStack animation options */\nexport const STICK_CARD_STACK_DEFAULTS = {\n scrollHeight: 8,\n cardYOffset: 5,\n cardScaleStep: 0.075,\n} as const;\n","import { createContext, useEffect, useRef, Children } from 'react';\nimport gsap from 'gsap';\nimport { ScrollTrigger } from 'gsap/ScrollTrigger';\nimport type { StickCardStackProps } from './types';\nimport { STICK_CARD_STACK_DEFAULTS } from './types';\nimport './StickCardStack.css';\n\n/** Injected by StickCardStack: variant (1–4) and optional per-card color from colorVariants. */\nexport interface StickCardContextValue {\n variant: number;\n color: string | null;\n}\nexport const StickCardVariantContext = createContext<StickCardContextValue | null>(null);\n\nconst VARIANT_COUNT = 4;\n\ngsap.registerPlugin(ScrollTrigger);\n\n/**\n * A sticky stack of cards that animate on scroll (GSAP + ScrollTrigger).\n * Use with useScrollTrigger() at app root for smooth scrolling.\n */\nexport function StickCardStack({\n children,\n scrollHeight = STICK_CARD_STACK_DEFAULTS.scrollHeight,\n cardYOffset = STICK_CARD_STACK_DEFAULTS.cardYOffset,\n cardScaleStep = STICK_CARD_STACK_DEFAULTS.cardScaleStep,\n className,\n colorVariants,\n}: StickCardStackProps) {\n const wrapRef = useRef<HTMLDivElement>(null);\n const innerRef = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n const wrap = wrapRef.current;\n const inner = innerRef.current;\n if (!wrap || !inner) return;\n\n const cards = inner.querySelectorAll<HTMLElement>('.stick-card');\n const totalCards = cards.length;\n if (totalCards === 0) return;\n\n const segmentSize = 1 / totalCards;\n\n cards.forEach((card, i) => {\n gsap.set(card, {\n xPercent: -50,\n yPercent: -50 + i * cardYOffset,\n scale: 1 - i * cardScaleStep,\n });\n });\n\n const scrollHeightPx = window.innerHeight * scrollHeight;\n\n const st = ScrollTrigger.create({\n trigger: wrap,\n start: 'top top',\n end: `+=${scrollHeightPx}px`,\n onUpdate(self) {\n const progress = self.progress;\n const activeIndex = Math.min(\n Math.floor(progress / segmentSize),\n totalCards - 1\n );\n const segmentProgress =\n (progress - activeIndex * segmentSize) / segmentSize;\n\n cards.forEach((card, i) => {\n if (i < activeIndex) {\n gsap.set(card, {\n yPercent: -250,\n rotationX: 35,\n });\n } else if (i === activeIndex) {\n gsap.set(card, {\n yPercent: gsap.utils.interpolate(-50, -200, segmentProgress),\n rotationX: gsap.utils.interpolate(0, 35, segmentProgress),\n scale: 1,\n });\n } else {\n const behindIndex = i - activeIndex;\n const currentYOffset = (behindIndex - segmentProgress) * cardYOffset;\n const currentScale =\n 1 - (behindIndex - segmentProgress) * cardScaleStep;\n gsap.set(card, {\n yPercent: -50 + currentYOffset,\n rotationX: 0,\n scale: currentScale,\n });\n }\n });\n },\n });\n\n return () => {\n st.kill();\n };\n }, [scrollHeight, cardYOffset, cardScaleStep]);\n\n const wrapStyle = {\n height: `calc(100svh * ${scrollHeight})`,\n };\n\n return (\n <div\n ref={wrapRef}\n className={className ? `stick-card-stack-wrap ${className}` : 'stick-card-stack-wrap'}\n style={wrapStyle}\n >\n <section className=\"stick-card-stack\">\n <div ref={innerRef} className=\"stick-card-stack-inner\">\n {Children.map(children, (child, index) => {\n const totalCards = Children.count(children);\n const variant = (index % VARIANT_COUNT) + 1;\n const color =\n colorVariants && colorVariants.length > 0\n ? colorVariants[index % colorVariants.length]\n : null;\n return (\n <StickCardVariantContext.Provider value={{ variant, color }}>\n <div\n className=\"stick-card\"\n style={{ zIndex: totalCards - index }}\n >\n {child}\n </div>\n </StickCardVariantContext.Provider>\n );\n })}\n </div>\n </section>\n </div>\n );\n}\n","import { useContext, type ReactElement } from 'react';\nimport { StickCardVariantContext } from './StickCardStack';\nimport type { StickCardProps, StickCardPropsChildren, StickCardPropsStructured } from './types';\n\n/**\n * A single card for use inside StickCardStack.\n * Either use structured props (header, content, main) for the default two-column layout,\n * or pass children and structure the card yourself.\n * Variant (1–4) is derived from position in the stack when used inside StickCardStack.\n */\nexport function StickCard(props: StickCardPropsStructured): ReactElement;\nexport function StickCard(props: StickCardPropsChildren): ReactElement;\nexport function StickCard(props: StickCardProps): ReactElement {\n const { colorOverride: colorProp } = props;\n const context = useContext(StickCardVariantContext);\n const variant = context?.variant ?? 1;\n const color = colorProp ?? context?.color ?? null;\n\n return (\n <div\n className={color ? 'card-demo' : `card-demo card-${variant}`}\n style={color ? { backgroundColor: color } : undefined}\n >\n {'children' in props && props.children != null ? (\n props.children\n ) : (\n <>\n <div className=\"col col-leading\">\n <div className=\"col-header\">{'header' in props ? props.header : null}</div>\n <div className=\"col-content\">{'content' in props ? props.content : null}</div>\n </div>\n <div className=\"col col-main\">{'main' in props ? props.main : null}</div>\n </>\n )}\n </div>\n );\n}\n","import { useEffect } from 'react';\nimport gsap from 'gsap';\nimport { ScrollTrigger } from 'gsap/ScrollTrigger';\nimport Lenis from 'lenis';\n\ngsap.registerPlugin(ScrollTrigger);\n\n/**\n * Sets up Lenis smooth scroll and syncs it with GSAP ScrollTrigger.\n * Call once at app root (e.g. in your root layout or App) when using StickCardStack.\n */\nexport function useScrollTrigger(): void {\n useEffect(() => {\n const lenis = new Lenis();\n\n lenis.on('scroll', ScrollTrigger.update);\n\n gsap.ticker.add((time) => {\n lenis.raf(time * 1000);\n });\n gsap.ticker.lagSmoothing(0);\n\n ScrollTrigger.scrollerProxy(document.body, {\n scrollTop(value) {\n if (arguments.length && typeof value === 'number') {\n lenis.scrollTo(value, { immediate: true });\n }\n return lenis.scroll;\n },\n getBoundingClientRect() {\n return {\n top: 0,\n left: 0,\n width: window.innerWidth,\n height: window.innerHeight,\n };\n },\n });\n\n return () => {\n gsap.ticker.remove(lenis.raf);\n lenis.destroy();\n };\n }, []);\n}\n"],"names":["STICK_CARD_STACK_DEFAULTS","StickCardVariantContext","createContext","VARIANT_COUNT","gsap","ScrollTrigger","StickCardStack","children","scrollHeight","cardYOffset","cardScaleStep","className","colorVariants","wrapRef","useRef","innerRef","useEffect","wrap","inner","cards","totalCards","segmentSize","card","i","scrollHeightPx","st","self","progress","activeIndex","segmentProgress","behindIndex","currentYOffset","currentScale","wrapStyle","jsx","Children","child","index","variant","color","StickCard","props","colorProp","context","useContext","jsxs","Fragment","useScrollTrigger","lenis","Lenis","time","value"],"mappings":"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"}
1
+ {"version":3,"file":"scs.js","sources":["../src/lib/types.ts","../src/lib/StickCardStack.tsx","../src/lib/StickCard.tsx","../src/lib/useScrollTrigger.ts"],"sourcesContent":["import type { ReactNode } from 'react';\n\n/** Props for the StickCardStack root component */\nexport interface StickCardStackProps {\n /** Card elements; each child is rendered as one stacked card */\n children: ReactNode;\n /**\n * Height of the scroll area as a multiple of viewport height.\n * @default 8\n */\n scrollHeight?: number;\n /**\n * Vertical offset between stacked cards (in percent).\n * @default 5\n */\n cardYOffset?: number;\n /**\n * Scale reduction per card in the stack (e.g. 0.075 = 7.5% smaller per card).\n * @default 0.075\n */\n cardScaleStep?: number;\n /** Optional class name for the wrapper element */\n className?: string;\n /**\n * Optional list of background colors (e.g. ['#3d2fa9', '#ff7722']).\n * Applied per card by index (cycles if fewer colors than cards).\n * When not provided, cards use the base CSS variables (.card-1 … .card-4).\n */\n colorVariants?: string[];\n}\n\n/** Common props for StickCard */\nexport interface StickCardPropsBase {\n /**\n * Custom background color for this card (e.g. '#3d2fa9').\n * When set, overrides stack context (variant/colorVariants) and uses this color only.\n */\n colorOverride?: string | null;\n}\n\n/** Structured card: header (20% top), content (22% at bottom of left column), and main column. */\nexport interface StickCardPropsStructured extends StickCardPropsBase {\n /** Optional header slot at top of left column (e.g. \"Join us\"). */\n header: ReactNode;\n /** Content slot: primary text in left column (e.g. \"Football\"). */\n content: ReactNode;\n /** Main slot: trailing column (e.g. image, media). */\n main: ReactNode;\n}\n\n/** Custom layout: you supply children and structure the card yourself. */\nexport interface StickCardPropsChildren extends StickCardPropsBase {\n children: ReactNode;\n}\n\n/** Props for a single StickCard: either structured (header, content, main) or custom (children). */\nexport type StickCardProps = StickCardPropsStructured | StickCardPropsChildren;\n\n/** Default values for StickCardStack animation options */\nexport const STICK_CARD_STACK_DEFAULTS = {\n scrollHeight: 8,\n cardYOffset: 5,\n cardScaleStep: 0.075,\n} as const;\n","import { createContext, useEffect, useRef, Children } from 'react';\nimport gsap from 'gsap';\nimport { ScrollTrigger } from 'gsap/ScrollTrigger';\nimport type { StickCardStackProps } from './types';\nimport { STICK_CARD_STACK_DEFAULTS } from './types';\nimport './StickCardStack.css';\n\n/** Injected by StickCardStack: variant (1–4) and optional per-card color from colorVariants. */\nexport interface StickCardContextValue {\n variant: number;\n color: string | null;\n}\nexport const StickCardVariantContext = createContext<StickCardContextValue | null>(null);\n\nconst VARIANT_COUNT = 4;\n\ngsap.registerPlugin(ScrollTrigger);\n\n/**\n * A sticky stack of cards that animate on scroll (GSAP + ScrollTrigger).\n * Use with useScrollTrigger() at app root for smooth scrolling.\n */\nexport function StickCardStack({\n children,\n scrollHeight = STICK_CARD_STACK_DEFAULTS.scrollHeight,\n cardYOffset = STICK_CARD_STACK_DEFAULTS.cardYOffset,\n cardScaleStep = STICK_CARD_STACK_DEFAULTS.cardScaleStep,\n className,\n colorVariants,\n}: StickCardStackProps) {\n const wrapRef = useRef<HTMLDivElement>(null);\n const innerRef = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n const wrap = wrapRef.current;\n const inner = innerRef.current;\n if (!wrap || !inner) return;\n\n const cards = inner.querySelectorAll<HTMLElement>('.scs-card');\n const totalCards = cards.length;\n if (totalCards === 0) return;\n\n const segmentSize = 1 / totalCards;\n\n cards.forEach((card, i) => {\n gsap.set(card, {\n xPercent: -50,\n yPercent: -50 + i * cardYOffset,\n scale: 1 - i * cardScaleStep,\n });\n });\n\n const scrollHeightPx = window.innerHeight * scrollHeight;\n\n const st = ScrollTrigger.create({\n trigger: wrap,\n start: 'top top',\n end: `+=${scrollHeightPx}px`,\n onUpdate(self) {\n const progress = self.progress;\n const activeIndex = Math.min(\n Math.floor(progress / segmentSize),\n totalCards - 1\n );\n const segmentProgress =\n (progress - activeIndex * segmentSize) / segmentSize;\n\n cards.forEach((card, i) => {\n if (i < activeIndex) {\n gsap.set(card, {\n yPercent: -250,\n rotationX: 35,\n });\n } else if (i === activeIndex) {\n gsap.set(card, {\n yPercent: gsap.utils.interpolate(-50, -200, segmentProgress),\n rotationX: gsap.utils.interpolate(0, 35, segmentProgress),\n scale: 1,\n });\n } else {\n const behindIndex = i - activeIndex;\n const currentYOffset = (behindIndex - segmentProgress) * cardYOffset;\n const currentScale =\n 1 - (behindIndex - segmentProgress) * cardScaleStep;\n gsap.set(card, {\n yPercent: -50 + currentYOffset,\n rotationX: 0,\n scale: currentScale,\n });\n }\n });\n },\n });\n\n return () => {\n st.kill();\n };\n }, [scrollHeight, cardYOffset, cardScaleStep]);\n\n const wrapStyle = {\n height: `calc(100svh * ${scrollHeight})`,\n };\n\n return (\n <div\n ref={wrapRef}\n className={className ? `scs-wrap ${className}` : 'scs-wrap'}\n style={wrapStyle}\n >\n <section className=\"scs-stack\">\n <div ref={innerRef} className=\"scs-stack-inner\">\n {Children.map(children, (child, index) => {\n const totalCards = Children.count(children);\n const variant = (index % VARIANT_COUNT) + 1;\n const color =\n colorVariants && colorVariants.length > 0\n ? colorVariants[index % colorVariants.length]\n : null;\n return (\n <StickCardVariantContext.Provider value={{ variant, color }}>\n <div\n className=\"scs-card\"\n style={{ zIndex: totalCards - index }}\n >\n {child}\n </div>\n </StickCardVariantContext.Provider>\n );\n })}\n </div>\n </section>\n </div>\n );\n}\n","import { useContext, type ReactElement } from 'react';\nimport { StickCardVariantContext } from './StickCardStack';\nimport type { StickCardProps, StickCardPropsChildren, StickCardPropsStructured } from './types';\nimport './StickCard.css';\n\n/**\n * A single card for use inside StickCardStack.\n * Either use structured props (header, content, main) for the default two-column layout,\n * or pass children and structure the card yourself.\n * Variant (1–4) is derived from position in the stack when used inside StickCardStack.\n */\nexport function StickCard(props: StickCardPropsStructured): ReactElement;\nexport function StickCard(props: StickCardPropsChildren): ReactElement;\nexport function StickCard(props: StickCardProps): ReactElement {\n const { colorOverride: colorProp } = props;\n const context = useContext(StickCardVariantContext);\n const variant = context?.variant ?? 1;\n const color = colorProp ?? context?.color ?? null;\n\n return (\n <div\n className={color ? 'scs-card-demo' : `scs-card-demo scs-card-${variant}`}\n style={color ? { backgroundColor: color } : undefined}\n >\n {'children' in props && props.children != null ? (\n props.children\n ) : (\n <>\n <div className=\"scs-col scs-col-leading\">\n <div className=\"scs-col-header\">{'header' in props ? props.header : null}</div>\n <div className=\"scs-col-content\">{'content' in props ? props.content : null}</div>\n </div>\n <div className=\"scs-col scs-col-main\">{'main' in props ? props.main : null}</div>\n </>\n )}\n </div>\n );\n}\n","import { useEffect } from 'react';\nimport gsap from 'gsap';\nimport { ScrollTrigger } from 'gsap/ScrollTrigger';\nimport Lenis from 'lenis';\n\ngsap.registerPlugin(ScrollTrigger);\n\n/**\n * Sets up Lenis smooth scroll and syncs it with GSAP ScrollTrigger.\n * Call once at app root (e.g. in your root layout or App) when using StickCardStack.\n */\nexport function useScrollTrigger(): void {\n useEffect(() => {\n const lenis = new Lenis();\n\n lenis.on('scroll', ScrollTrigger.update);\n\n gsap.ticker.add((time) => {\n lenis.raf(time * 1000);\n });\n gsap.ticker.lagSmoothing(0);\n\n ScrollTrigger.scrollerProxy(document.body, {\n scrollTop(value) {\n if (arguments.length && typeof value === 'number') {\n lenis.scrollTo(value, { immediate: true });\n }\n return lenis.scroll;\n },\n getBoundingClientRect() {\n return {\n top: 0,\n left: 0,\n width: window.innerWidth,\n height: window.innerHeight,\n };\n },\n });\n\n return () => {\n gsap.ticker.remove(lenis.raf);\n lenis.destroy();\n };\n }, []);\n}\n"],"names":["STICK_CARD_STACK_DEFAULTS","StickCardVariantContext","createContext","VARIANT_COUNT","gsap","ScrollTrigger","StickCardStack","children","scrollHeight","cardYOffset","cardScaleStep","className","colorVariants","wrapRef","useRef","innerRef","useEffect","wrap","inner","cards","totalCards","segmentSize","card","i","scrollHeightPx","st","self","progress","activeIndex","segmentProgress","behindIndex","currentYOffset","currentScale","wrapStyle","jsx","Children","child","index","variant","color","StickCard","props","colorProp","context","useContext","jsxs","Fragment","useScrollTrigger","lenis","Lenis","time","value"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sticky-card-stack",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "React sticky card stack with GSAP ScrollTrigger and Lenis smooth scroll",
5
5
  "type": "module",
6
6
  "main": "./dist/scs.js",