diff --git a/src/components/home/CommerceCompareSlider.tsx b/src/components/home/CommerceCompareSlider.tsx
new file mode 100644
index 0000000..7c08527
--- /dev/null
+++ b/src/components/home/CommerceCompareSlider.tsx
@@ -0,0 +1,324 @@
+"use client";
+
+import { useCallback, useEffect, useRef, useState } from "react";
+import type { KeyboardEvent as ReactKeyboardEvent, MouseEvent as ReactMouseEvent, TouchEvent as ReactTouchEvent } from "react";
+import { motion, useInView, useReducedMotion } from "framer-motion";
+import { MaterialIcon } from "@/components/ui/MaterialIcon";
+
+// Divider position, in percent of card width. The top (Nearle) layer is clipped
+// from the left by this value, so 0 = all Nearle, 100 = all Traditional.
+const REST_POSITION = 50;
+
+// One-shot hint played on scroll-in: sweep out to 20, across to 80, settle back
+// at centre. Purely to teach the interaction — cancelled the moment the user
+// touches the card.
+const HINT_STOPS = [20, 80, REST_POSITION];
+const HINT_LEG_MS = 820;
+const HINT_DELAY_MS = 480;
+
+const TRADITIONAL = [
+ { icon: "hub", text: "Platform owns discovery" },
+ { icon: "visibility_off", text: "Merchant becomes invisible" },
+ { icon: "search", text: "Endless searching" },
+ { icon: "groups", text: "Customer belongs to platform" },
+];
+
+const NEARLE = [
+ { icon: "chat_bubble", text: "Just ask in your own words" },
+ { icon: "storefront", text: "The shop you trust stays at the centre" },
+ { icon: "near_me", text: "You discover the shops right around you" },
+ { icon: "handshake", text: "Local shops grow — they never get replaced" },
+];
+
+const clamp = (n: number) => Math.min(100, Math.max(0, n));
+const easeInOut = (t: number) => (t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2);
+
+type PanelProps = {
+ /** Renders the panel in normal flow, invisibly, purely to give the card its height. */
+ ghost?: boolean;
+};
+
+// The two layers sit on top of each other, so a horizontal wipe only reads as a
+// swap if each side keeps its content on its own side of the divider:
+// Traditional hugs the left edge, Nearle mirrors to the right. Rows never wrap —
+// the divider, not a line break, is what truncates them.
+const ROW_BASE = "flex items-center gap-3 overflow-hidden rounded-2xl p-3 sm:p-3.5";
+const TEXT_BASE = "min-w-0 flex-1 truncate text-[13px] font-medium leading-snug sm:text-[15px]";
+
+function TraditionalPanel({ ghost }: PanelProps) {
+ return (
+
+
+
+
+
+
+
Today
+
+ Traditional Commerce
+
+
+
+
+
+ {TRADITIONAL.map((item) => (
+ -
+
+
+
+ {item.text}
+
+ ))}
+
+
+ );
+}
+
+function NearlePanel({ ghost }: PanelProps) {
+ return (
+
+
+
+
+
+
+
Nearle
+
+ Nearle AI Commerce
+
+
+
+
+
+ {NEARLE.map((item) => (
+ -
+
+
+
+ {item.text}
+
+ ))}
+
+
+ );
+}
+
+export function CommerceCompareSlider() {
+ const reduce = useReducedMotion();
+ const wrapRef = useRef(null);
+ const cardRef = useRef(null);
+ const inView = useInView(wrapRef, { once: true, margin: "-60px" });
+
+ const [position, setPosition] = useState(REST_POSITION);
+ const [dragging, setDragging] = useState(false);
+
+ // Hint bookkeeping: a rAF id to cancel, and a latch so it never replays.
+ const hintFrame = useRef(null);
+ const hintTimer = useRef | null>(null);
+ const hintDone = useRef(false);
+
+ const stopHint = useCallback(() => {
+ hintDone.current = true;
+ if (hintFrame.current !== null) cancelAnimationFrame(hintFrame.current);
+ if (hintTimer.current !== null) clearTimeout(hintTimer.current);
+ hintFrame.current = null;
+ hintTimer.current = null;
+ }, []);
+
+ const setFromClientX = useCallback((clientX: number) => {
+ const rect = cardRef.current?.getBoundingClientRect();
+ if (!rect || rect.width === 0) return;
+ setPosition(clamp(((clientX - rect.left) / rect.width) * 100));
+ }, []);
+
+ // ── One-shot hint ──────────────────────────────────────────────────────
+ // Hand-rolled rAF tween rather than Framer Motion: this component reserves
+ // Framer Motion for the entrance only, and the divider is driven by the same
+ // numeric state the pointer handlers write to.
+ useEffect(() => {
+ if (!inView || reduce || hintDone.current) return;
+
+ let leg = 0;
+ let from = REST_POSITION;
+ let start = 0;
+
+ const step = (now: number) => {
+ if (!start) start = now;
+ const t = Math.min(1, (now - start) / HINT_LEG_MS);
+ const to = HINT_STOPS[leg];
+ setPosition(from + (to - from) * easeInOut(t));
+
+ if (t < 1) {
+ hintFrame.current = requestAnimationFrame(step);
+ return;
+ }
+ leg += 1;
+ if (leg >= HINT_STOPS.length) {
+ stopHint();
+ return;
+ }
+ from = to;
+ start = 0;
+ hintFrame.current = requestAnimationFrame(step);
+ };
+
+ hintTimer.current = setTimeout(() => {
+ hintFrame.current = requestAnimationFrame(step);
+ }, HINT_DELAY_MS);
+
+ return () => {
+ if (hintFrame.current !== null) cancelAnimationFrame(hintFrame.current);
+ if (hintTimer.current !== null) clearTimeout(hintTimer.current);
+ };
+ }, [inView, reduce, stopHint]);
+
+ // ── Mouse ──────────────────────────────────────────────────────────────
+ const onMouseDown = (e: ReactMouseEvent) => {
+ stopHint();
+ setDragging(true);
+ setFromClientX(e.clientX);
+ };
+
+ useEffect(() => {
+ if (!dragging) return;
+ const onMove = (e: MouseEvent) => setFromClientX(e.clientX);
+ const onUp = () => setDragging(false);
+ window.addEventListener("mousemove", onMove);
+ window.addEventListener("mouseup", onUp);
+ return () => {
+ window.removeEventListener("mousemove", onMove);
+ window.removeEventListener("mouseup", onUp);
+ };
+ }, [dragging, setFromClientX]);
+
+ // ── Touch ──────────────────────────────────────────────────────────────
+ // touch-action: pan-y keeps vertical page scrolling native; horizontal
+ // movement over the card is ours.
+ const onTouchStart = (e: ReactTouchEvent) => {
+ stopHint();
+ setDragging(true);
+ setFromClientX(e.touches[0].clientX);
+ };
+ const onTouchMove = (e: ReactTouchEvent) => setFromClientX(e.touches[0].clientX);
+ const onTouchEnd = () => setDragging(false);
+
+ // ── Keyboard ───────────────────────────────────────────────────────────
+ const onKeyDown = (e: ReactKeyboardEvent) => {
+ const step = e.shiftKey ? 10 : 4;
+ if (e.key === "ArrowLeft") {
+ stopHint();
+ setPosition((p) => clamp(p - step));
+ } else if (e.key === "ArrowRight") {
+ stopHint();
+ setPosition((p) => clamp(p + step));
+ } else if (e.key === "Home") {
+ stopHint();
+ setPosition(0);
+ } else if (e.key === "End") {
+ stopHint();
+ setPosition(100);
+ } else {
+ return;
+ }
+ e.preventDefault();
+ };
+
+ return (
+
+
+ {/* Invisible in-flow copy: gives the card a real height so neither
+ absolutely-positioned layer can ever be clipped on small screens. */}
+
+
+ {/* Bottom layer — the world as it works today. */}
+
+
+ {/* Top layer — clipped from the left by the divider. */}
+
+
+
+
+ {/* Seam: an opaque strip under the divider so the two halves never read
+ as one run-on sentence when the rows meet mid-word. */}
+
+
+ {/* Divider line */}
+
+
+ {/* Handle */}
+
+
+
+
+
+
+
+ Drag to compare
+
+
+ );
+}
diff --git a/src/components/home/CommerceFlipCard.tsx b/src/components/home/CommerceFlipCard.tsx
new file mode 100644
index 0000000..f30431d
--- /dev/null
+++ b/src/components/home/CommerceFlipCard.tsx
@@ -0,0 +1,117 @@
+"use client";
+
+import { useRef } from "react";
+import { motion, useInView } from "framer-motion";
+import { MaterialIcon } from "@/components/ui/MaterialIcon";
+
+interface DifferencePillar {
+ number: string;
+ icon: string;
+ title: string;
+ description: string;
+ traditional: string;
+ nearle: string;
+}
+
+const PILLARS: DifferencePillar[] = [
+ {
+ number: "01",
+ icon: "search",
+ title: "Ask in Natural Words",
+ description: "Type or speak like you're talking to a neighbour — 'Who has fresh sourdough near me?' to see live local store stock.",
+ traditional: "Paid ads & dark warehouses",
+ nearle: "Real local shop inventory",
+ },
+ {
+ number: "02",
+ icon: "storefront",
+ title: "Your Store at the Centre",
+ description: "Your favourite neighbourhood grocery, bakery, or chemist gets its own digital storefront and QR ordering.",
+ traditional: "Faceless dark warehouses",
+ nearle: "The shop you already trust",
+ },
+ {
+ number: "03",
+ icon: "sell",
+ title: "Direct Store Prices",
+ description: "Order directly from your local merchant with zero middleman app commission markups.",
+ traditional: "25–30% extra app markup",
+ nearle: "Direct prices & 1-tap reorders",
+ },
+ {
+ number: "04",
+ icon: "favorite",
+ title: "Local Stores Grow",
+ description: "Every purchase keeps local store owners thriving, preserving neighbourhood jobs and community culture.",
+ traditional: "Money leaves neighbourhood",
+ nearle: "100% supports local stores",
+ },
+];
+
+const EASE = [0.22, 1, 0.36, 1] as const;
+
+export function CommerceFlipCard() {
+ const wrapRef = useRef(null);
+ const inView = useInView(wrapRef, { once: true, margin: "-50px" });
+
+ return (
+
+ {/* ─── 4-Card Grid matching Nearle Identity ─── */}
+
+ {PILLARS.map((item) => (
+
+ {/* Top Row: Icon + Number */}
+
+
+
+
+
+
+ {item.number}
+
+
+
+ {/* Title & Description */}
+
+ {item.title}
+
+
+ {item.description}
+
+
+
+ {/* Bottom Comparison Pill */}
+
+
+
+ ✕
+
+ {item.traditional}
+
+
+
+ ✓
+
+ {item.nearle}
+
+
+
+ ))}
+
+
+ );
+}
+
+
+
+
+
diff --git a/src/components/home/WhyNearleDifferent.tsx b/src/components/home/WhyNearleDifferent.tsx
index 465e784..a1677ff 100644
--- a/src/components/home/WhyNearleDifferent.tsx
+++ b/src/components/home/WhyNearleDifferent.tsx
@@ -2,29 +2,8 @@
import { motion, useReducedMotion } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
-import { Spotlight } from "@/components/ui/Spotlight";
-import {
- fadeUp,
- slideInLeftSubtle,
- slideInRight,
- staggerContainer,
- staggerCards,
- viewport,
-} from "@/lib/animations";
-
-const TRADITIONAL = [
- { icon: "hub", text: "Platform owns discovery" },
- { icon: "visibility_off", text: "Merchant becomes invisible" },
- { icon: "search", text: "Endless searching" },
- { icon: "groups", text: "Customer belongs to platform" },
-];
-
-const NEARLE = [
- { icon: "psychology", text: "Just ask in your own words" },
- { icon: "storefront", text: "The shop you trust stays at the centre" },
- { icon: "near_me", text: "You discover the shops right around you" },
- { icon: "handshake", text: "Local shops grow — they never get replaced" },
-];
+import { CommerceFlipCard } from "@/components/home/CommerceFlipCard";
+import { fadeUp, staggerContainer, viewport } from "@/lib/animations";
// The gentle S-curve the transformation flow travels. Stretched full-bleed via
// preserveAspectRatio="none", so exact geometry matters less than the direction:
@@ -321,132 +300,13 @@ export function WhyNearleDifferent() {
- Two ways to bring shopping online. One puts the platform in the middle — the other keeps
- the shop you already trust there.
+ One puts big platforms in the middle — the other keeps the shop you already trust there.
- {/* Sequential reveal: heading (above) → left card → right card, staggered
- by the parent; rows stagger within each card. */}
-
- {/* Traditional commerce */}
-
-
-
-
-
-
-
-
- Today
-
-
- Traditional Commerce
-
-
-
-
-
- {TRADITIONAL.map((item) => (
-
-
-
-
-
- {item.text}
-
-
- ))}
-
-
-
-
- {/* Nearle AI Commerce — highlighted */}
-
- {/* soft border glow behind the highlighted card */}
-
-
-
-
-
-
-
-
-
-
- Nearle
-
-
- Nearle AI Commerce
-
-
-
-
-
- {NEARLE.map((item) => (
-
-
-
-
-
- {item.text}
-
-
- ))}
-
-
-
-
+
);