# TrustStrip Redesign — Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Replace the minimal icon+text trust strip with a bold stats section featuring large animated count-up numbers, icon circles, vertical dividers, and a gradient background.

**Architecture:** 3-file change — new `useCountUp` hook for the animation, rewritten `TrustStrip.tsx` component, barrel file update. Uses existing `useInView` hook and codebase patterns (cn utility, Tailwind, lucide-react). No new dependencies.

**Tech Stack:** React 19, Tailwind CSS, lucide-react, existing `useInView` hook

---

### Task 1: Create the useCountUp hook

**Files:**

- Create: `apps/portal/src/features/landing-page/hooks/useCountUp.ts`

**Step 1: Write the hook**

Create `apps/portal/src/features/landing-page/hooks/useCountUp.ts`:

```tsx
import { useEffect, useState } from "react";

/**
 * useCountUp — Animates a number from 0 to target over a duration.
 * Respects prefers-reduced-motion. Only runs when enabled is true.
 */
export function useCountUp(target: number, duration = 1500, enabled = false): number {
  const [value, setValue] = useState(0);

  useEffect(() => {
    if (!enabled) return;

    // Respect prefers-reduced-motion
    const prefersReduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    if (prefersReduced) {
      setValue(target);
      return;
    }

    let startTime: number | null = null;
    let rafId: number;

    const animate = (timestamp: number) => {
      if (!startTime) startTime = timestamp;
      const elapsed = timestamp - startTime;
      const progress = Math.min(elapsed / duration, 1);

      // Ease-out cubic for smooth deceleration
      const eased = 1 - Math.pow(1 - progress, 3);
      setValue(Math.round(eased * target));

      if (progress < 1) {
        rafId = requestAnimationFrame(animate);
      }
    };

    rafId = requestAnimationFrame(animate);
    return () => cancelAnimationFrame(rafId);
  }, [target, duration, enabled]);

  return value;
}
```

**Step 2: Export from barrel file**

Modify `apps/portal/src/features/landing-page/hooks/index.ts` — add this line:

```ts
export { useCountUp } from "./useCountUp";
```

**Step 3: Verify it builds**

Run: `pnpm type-check`
Expected: No errors

**Step 4: Commit**

```bash
git add apps/portal/src/features/landing-page/hooks/useCountUp.ts apps/portal/src/features/landing-page/hooks/index.ts
git commit -m "feat: add useCountUp hook for animated number transitions"
```

---

### Task 2: Rewrite TrustStrip component

**Files:**

- Modify: `apps/portal/src/features/landing-page/components/TrustStrip.tsx` (complete rewrite)

**Step 1: Replace TrustStrip.tsx with new implementation**

Rewrite `apps/portal/src/features/landing-page/components/TrustStrip.tsx`:

```tsx
"use client";

import { Clock, CreditCard, Globe, Users } from "lucide-react";
import type { LucideIcon } from "lucide-react";
import { cn } from "@/shared/utils";
import { useInView, useCountUp } from "@/features/landing-page/hooks";

interface StatItem {
  icon: LucideIcon;
  value: number;
  suffix: string;
  label: string;
  delay: number;
  formatter?: (n: number) => string;
}

const stats: StatItem[] = [
  { icon: Clock, value: 20, suffix: "+", label: "Years in Japan", delay: 0 },
  { icon: Globe, value: 100, suffix: "%", label: "English Support", delay: 100 },
  {
    icon: Users,
    value: 10000,
    suffix: "+",
    label: "Customers Served",
    delay: 200,
    formatter: (n: number) => n.toLocaleString(),
  },
];

function AnimatedStat({ stat, inView }: { stat: StatItem; inView: boolean }) {
  const count = useCountUp(stat.value, 1500, inView);

  return (
    <span className="text-3xl sm:text-4xl font-extrabold text-primary tabular-nums">
      {stat.formatter ? stat.formatter(count) : count}
      {stat.suffix}
    </span>
  );
}

export function TrustStrip() {
  const [ref, inView] = useInView();

  return (
    <section
      ref={ref as React.RefObject<HTMLElement>}
      aria-label="Company statistics"
      className={cn(
        "relative left-1/2 right-1/2 w-screen -translate-x-1/2 py-10 sm:py-12 overflow-hidden transition-all duration-700",
        inView ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
      )}
    >
      {/* Gradient background */}
      <div className="absolute inset-0 bg-gradient-to-r from-surface-sunken via-background to-info-bg/30" />

      <div className="relative mx-auto max-w-6xl px-6 sm:px-10 lg:px-14">
        <div className="grid grid-cols-2 gap-8 sm:flex sm:justify-between sm:items-center">
          {/* Animated stats */}
          {stats.map((stat, i) => (
            <div
              key={stat.label}
              className={cn(
                "flex flex-col items-center text-center gap-3 sm:flex-1",
                i < stats.length - 1 && "sm:border-r sm:border-border/30"
              )}
            >
              <div className="flex items-center justify-center w-10 h-10 rounded-full bg-primary/10">
                <stat.icon className="h-5 w-5 text-primary" />
              </div>
              <div className="flex flex-col items-center gap-1">
                <AnimatedStat stat={stat} inView={inView} />
                <span className="text-sm text-muted-foreground font-medium">{stat.label}</span>
              </div>
            </div>
          ))}

          {/* Static stat — Foreign Cards */}
          <div className={cn("flex flex-col items-center text-center gap-3 sm:flex-1")}>
            <div className="flex items-center justify-center w-10 h-10 rounded-full bg-primary/10">
              <CreditCard className="h-5 w-5 text-primary" />
            </div>
            <div className="flex flex-col items-center gap-1">
              <span className="text-xl sm:text-2xl font-extrabold text-primary">Foreign Cards</span>
              <span className="text-sm text-muted-foreground font-medium">Accepted</span>
            </div>
          </div>
        </div>
      </div>
    </section>
  );
}
```

Key decisions:

- `AnimatedStat` is a separate component so each stat has its own `useCountUp` instance
- The 4th stat (Foreign Cards) is rendered separately since it has no numeric animation
- Desktop: flex row with `border-r` dividers on first 3 items. Mobile: 2-col grid
- `tabular-nums` prevents layout jitter during count-up animation
- `formatter` on the 10,000 stat adds comma separators

**Step 2: Verify it builds**

Run: `pnpm type-check`
Expected: No errors

**Step 3: Visual check**

Open `localhost:3000` and verify:

- 4 stats displayed in a row on desktop, 2x2 grid on mobile
- Numbers animate from 0 to target when scrolling into view
- Vertical dividers between first 3 stats on desktop
- "Foreign Cards / Accepted" shows as static bold text
- Gradient background blends with hero above
- Each stat has a primary-tinted circular icon above it

**Step 4: Commit**

```bash
git add apps/portal/src/features/landing-page/components/TrustStrip.tsx
git commit -m "feat: redesign TrustStrip with bold animated stats"
```

---

### Task 3: Stagger the count-up animations

The current implementation starts all counters simultaneously. Add stagger delays.

**Files:**

- Modify: `apps/portal/src/features/landing-page/hooks/useCountUp.ts`
- Modify: `apps/portal/src/features/landing-page/components/TrustStrip.tsx`

**Step 1: Add delay parameter to useCountUp**

In `useCountUp.ts`, change the signature and add a delay before animation starts:

```tsx
export function useCountUp(target: number, duration = 1500, enabled = false, delay = 0): number {
```

Update the effect body — wrap the animation start in a `setTimeout`:

```tsx
useEffect(() => {
  if (!enabled) return;

  const prefersReduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
  if (prefersReduced) {
    setValue(target);
    return;
  }

  let startTime: number | null = null;
  let rafId: number;
  let timeoutId: ReturnType<typeof setTimeout>;

  const animate = (timestamp: number) => {
    if (!startTime) startTime = timestamp;
    const elapsed = timestamp - startTime;
    const progress = Math.min(elapsed / duration, 1);
    const eased = 1 - Math.pow(1 - progress, 3);
    setValue(Math.round(eased * target));

    if (progress < 1) {
      rafId = requestAnimationFrame(animate);
    }
  };

  timeoutId = setTimeout(() => {
    rafId = requestAnimationFrame(animate);
  }, delay);

  return () => {
    clearTimeout(timeoutId);
    cancelAnimationFrame(rafId);
  };
}, [target, duration, enabled, delay]);
```

**Step 2: Pass delay from TrustStrip**

In `AnimatedStat`, update the `useCountUp` call:

```tsx
function AnimatedStat({ stat, inView }: { stat: StatItem; inView: boolean }) {
  const count = useCountUp(stat.value, 1500, inView, stat.delay);
```

**Step 3: Verify it builds**

Run: `pnpm type-check`
Expected: No errors

**Step 4: Visual check**

Open `localhost:3000`, scroll to trust strip. The 3 animated numbers should start counting up in sequence: "20+" first, "100%" ~100ms later, "10,000+" ~200ms later. The stagger should feel subtle but add polish.

**Step 5: Commit**

```bash
git add apps/portal/src/features/landing-page/hooks/useCountUp.ts apps/portal/src/features/landing-page/components/TrustStrip.tsx
git commit -m "feat: add staggered delay to count-up animations"
```

---

### Task 4: Final verification

**Step 1: Run type check and lint**

Run: `pnpm type-check`
Expected: No errors

**Step 2: Visual smoke test**

Check `localhost:3000` at:

- Desktop (1280px): 4 stats in a row, dividers, gradient bg, count-up animation
- Tablet (768px): same row, slightly smaller numbers
- Mobile (375px): 2x2 grid, no dividers, animation still works

**Step 3: Accessibility check**

- Tab through the page — stats section should be announced as "Company statistics"
- Set `prefers-reduced-motion: reduce` in browser DevTools — numbers should show final values immediately without animation
