diff --git a/package.json b/package.json
index cd05a9c..506d057 100644
--- a/package.json
+++ b/package.json
@@ -47,6 +47,7 @@
"date-fns": "^4.4.0",
"dotenv": "^17.4.2",
"drizzle-orm": "^0.45.2",
+ "gsap": "^3.15.0",
"lucide-react": "^1.17.0",
"motion": "^12.42.0",
"next": "16.2.6",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d84dda6..6248bf5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -80,6 +80,9 @@ importers:
drizzle-orm:
specifier: ^0.45.2
version: 0.45.2(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.29.2)(pg@8.21.0)
+ gsap:
+ specifier: ^3.15.0
+ version: 3.15.0
lucide-react:
specifier: ^1.17.0
version: 1.20.0(react@19.2.4)
@@ -4139,6 +4142,9 @@ packages:
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+ gsap@3.15.0:
+ resolution: {integrity: sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A==}
+
has-bigints@1.1.0:
resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
engines: {node: '>= 0.4'}
@@ -9987,6 +9993,8 @@ snapshots:
graceful-fs@4.2.11: {}
+ gsap@3.15.0: {}
+
has-bigints@1.1.0: {}
has-flag@4.0.0: {}
diff --git a/src/features/marketing/components/empowerment-section.tsx b/src/features/marketing/components/empowerment-section.tsx
index b97fc93..a8b685a 100644
--- a/src/features/marketing/components/empowerment-section.tsx
+++ b/src/features/marketing/components/empowerment-section.tsx
@@ -2,6 +2,7 @@
import { Avatar, AvatarFallback, AvatarImage } from "@/shared/ui/avatar"
import { ChevronDown, Gift, Star, Stethoscope } from "lucide-react"
+import { motion } from "motion/react"
import Image from "next/image"
import { useEffect, useState } from "react"
@@ -13,7 +14,7 @@ export function EmpowermentSection() {
Empowering People to Move Forward
-
+
@@ -34,16 +35,43 @@ export function RestoreHopeCard() {
-
-
Get the support you need
+
+
+ Get the support you need
+
-
-
+
+
+
+
+
+
-
+
)
}
@@ -61,70 +89,105 @@ export function NeverNavigateAloneCard() {
-
-
Connect to a specialist
+
+
+ Connect to a specialist
+
-
-
-
- MA
-
-
-
-
- Dr. M. Anderson
-
-
- Washington D.C, USA
-
-
-
-
-
-
{" "}
- 5.0
+
+
+
+
+ MA
+
+
+
+
+ Dr. M. Anderson
+
+
+ Washington D.C, USA
+
-
-
80 Surgeries/yr
+
+
+
+ {" "}
+ 5.0
+
+
+ 80
+ Surgeries/yr
+
-
-
-
-
- AR
-
-
-
-
- Dr. A. Rivera
-
-
- Washington D.C, USA
-
-
-
-
-
-
{" "}
- 5.0
+
+
+
+
+
+ AR
+
+
+
+
+ Dr. A. Rivera
+
+
+ Washington D.C, USA
+
-
-
80 Surgeries/yr
+
+
+
+ {" "}
+ 5.0
+
+
+ 80
+ Surgeries/yr
+
-
+
-
+
)
}
@@ -141,16 +204,29 @@ export function BuildIndependenceCard() {
strengthen financial resilience
-
+
-
+
Week 2: AI, Economic Mobility
@@ -161,9 +237,15 @@ export function BuildIndependenceCard() {
-
+
-
+
Week 2: AI Productivity and Prompting
@@ -174,8 +256,8 @@ export function BuildIndependenceCard() {
-
-
+
+
)
}
diff --git a/src/features/marketing/components/practical-pathways-section.tsx b/src/features/marketing/components/practical-pathways-section.tsx
index 7a32a69..86b224c 100644
--- a/src/features/marketing/components/practical-pathways-section.tsx
+++ b/src/features/marketing/components/practical-pathways-section.tsx
@@ -1,43 +1,256 @@
-import Image from "next/image"
-
-const PracticalPathwaysSection = () => {
- return (
-
-
-
-
- Practical pathways to improved health outcomes
-
-
- Our shared values
-
-
-
-
-
-
- Restoring Hope
-
-
- We believe every person deserves hope, dignity, and the
- opportunity to move forward, regardless of disability, chronic
- illness, or financial hardship.
-
-
-
-
-
-
-
- )
-}
-
-export default PracticalPathwaysSection
+"use client"
+
+import { useLayoutEffect, useRef } from "react"
+import gsap from "gsap"
+import { ScrollTrigger } from "gsap/ScrollTrigger"
+
+gsap.registerPlugin(ScrollTrigger)
+
+type Item = {
+ id: number
+ color: string
+ label: string
+ description: string
+}
+
+const items: Item[] = [
+ {
+ id: 1,
+ color: "var(--hue-1)",
+ label: "Restoring Hope",
+ description:
+ "We believe every person deserves hope, dignity, and the opportunity to move forward, regardless of disability, chronic illness, or financial hardship.",
+ },
+ {
+ id: 2,
+ color: "var(--hue-2)",
+ label: "Empowering Independence",
+ description:
+ "Our mission is to help people transform life’s greatest challenges into opportunities for greater independence through education, technology, and practical support.",
+ },
+ {
+ id: 3,
+ color: "var(--hue-3)",
+ label: "AI Education",
+ description:
+ "We help patients navigate complex medical conditions—especially endometriosis—by promoting education, organization, informed decision-making, and access to specialized care.",
+ },
+ {
+ id: 4,
+ color: "var(--hue-4)",
+ label: "Community",
+ description:
+ "We believe lives are changed when people encourage, educate, and support one another. Together, we can accomplish more than any individual alone.",
+ },
+ {
+ id: 5,
+ color: "var(--hue-5)",
+ label: "Innovation with Purpose",
+ description:
+ "We embrace technology not for its own sake, but as a tool to make education, healthcare navigation, and economic opportunity more accessible to everyone.",
+ },
+]
+
+export default function ScrollStory() {
+ const sectionRef = useRef(null)
+ const textContainerRef = useRef(null)
+
+ useLayoutEffect(() => {
+ const section = sectionRef.current
+ const textContainer = textContainerRef.current
+
+ if (!section || !textContainer) return
+
+ const ctx = gsap.context(() => {
+ /*
+ * Each text block occupies one full viewport-sized
+ * "slot" in the vertical text track.
+ *
+ * We then move the entire track upward.
+ */
+
+ const blocks = gsap.utils.toArray(
+ ".scroll-story-block",
+ textContainer
+ )
+
+ if (!blocks.length) return
+
+ /*
+ * --------------------------------------------------
+ * INITIAL POSITION
+ * --------------------------------------------------
+ *
+ * Block 1 starts in the viewport.
+ *
+ * Blocks 2–5 are underneath it in the vertical
+ * track and therefore aren't visible.
+ */
+
+ gsap.set(textContainer, {
+ y: 0,
+ })
+
+ /*
+ * --------------------------------------------------
+ * SCROLL DISTANCE
+ * --------------------------------------------------
+ *
+ * There are 4 transitions for 5 items.
+ *
+ * Each transition gets 80% of a viewport.
+ *
+ * You can change this to:
+ *
+ * 1 = slower
+ * 0.8 = current
+ * 0.6 = faster
+ */
+
+ const transitionDistance = window.innerHeight * 1
+
+ const timeline = gsap.timeline({
+ scrollTrigger: {
+ trigger: section,
+
+ /*
+ * Pin when the section reaches the top.
+ */
+ start: "200px top",
+
+ /*
+ * Four transitions.
+ */
+ end: `+=${transitionDistance * (items.length - 1)}`,
+
+ pin: true,
+
+ /*
+ * Direct relationship between scroll and movement.
+ */
+ scrub: 0.4,
+
+ anticipatePin: 1,
+
+ invalidateOnRefresh: true,
+
+ markers: true,
+ },
+ })
+
+ /*
+ * --------------------------------------------------
+ * MOVE THE TEXT TRACK
+ * --------------------------------------------------
+ *
+ * Instead of fading individual blocks, we move
+ * the entire stack.
+ *
+ * Each block has the same height.
+ *
+ * 0% = Item 1
+ * -100% = Item 2
+ * -200% = Item 3
+ * -300% = Item 4
+ * -400% = Item 5
+ */
+
+ timeline.to(textContainer, {
+ y: () => {
+ /*
+ * The container needs to move by exactly one
+ * block height for every transition.
+ *
+ * Using the first block's height makes this
+ * independent of viewport dimensions.
+ */
+
+ const blockHeight = blocks[0].offsetHeight
+
+ return -(blockHeight * (items.length - 1))
+ },
+
+ ease: "none",
+
+ duration: items.length - 1,
+ })
+ }, section)
+
+ return () => {
+ ctx.revert()
+ }
+ }, [])
+
+ return (
+
+
+
+
+ Practical pathways to improved health outcomes
+
+
+
+ Our shared values
+
+
+
+
+ {/* ==================================================
+ LEFT — TEXT
+ ================================================== */}
+
+
+ {/*
+ * This is the viewport through which the text
+ * track moves.
+ *
+ * It sits BELOW the heading.
+ */}
+
+
+ {/*
+ * TEXT TRACK
+ *
+ * This is the element GSAP moves vertically.
+ */}
+
+
+ {items.map((item) => (
+
+
+ {item.label}
+
+
+
+ {item.description}
+
+
+ ))}
+
+
+
+
+ {/* ==================================================
+ RIGHT — STATIC IMAGE
+ ================================================== */}
+
+
+
+

+
+
+
+
+
+ )
+}
diff --git a/src/features/marketing/components/stories-from-our-community-section.tsx b/src/features/marketing/components/stories-from-our-community-section.tsx
index 52f11c5..3bc2a17 100644
--- a/src/features/marketing/components/stories-from-our-community-section.tsx
+++ b/src/features/marketing/components/stories-from-our-community-section.tsx
@@ -3,6 +3,7 @@
import Image from "next/image"
import { useState } from "react"
import { ArrowLeft, ArrowRight } from "lucide-react"
+import { AnimatePresence, motion } from "motion/react"
import { cn } from "@/shared/utils"
@@ -55,19 +56,21 @@ const stories = [
]
export const StoriesSection = () => {
- const [activeIndex, setActiveIndex] = useState(0)
+ const [[activeIndex, direction], setActiveIndex] = useState([0, 0])
const activeStory = stories[activeIndex]
function showPreviousStory() {
- setActiveIndex((currentIndex) =>
- currentIndex === 0 ? stories.length - 1 : currentIndex - 1
- )
+ setActiveIndex(([currentIndex]) => [
+ currentIndex === 0 ? stories.length - 1 : currentIndex - 1,
+ -1,
+ ])
}
function showNextStory() {
- setActiveIndex((currentIndex) =>
- currentIndex === stories.length - 1 ? 0 : currentIndex + 1
- )
+ setActiveIndex(([currentIndex]) => [
+ currentIndex === stories.length - 1 ? 0 : currentIndex + 1,
+ 1,
+ ])
}
return (
@@ -85,55 +88,74 @@ export const StoriesSection = () => {
-
-
-
+
+
+
+
+
-
-
- {activeStory.quote}
-
+
+
+
+ {activeStory.quote}
+
-
-
-
- {activeStory.name}
-
-
- {activeStory.role}
-
-
+
+
+
+ {activeStory.name}
+
+
+ {activeStory.role}
+
+
-
-
-
+
+