Workspace / block / hero

Split Hero

Copy on the left, a live visual stage on the right, balanced on a strict two-column grid that collapses cleanly on mobile.

Workspaceblockhero

paxfx add blocks/block-hero-split-02local only

Tier: medium. Reduced motion: Both columns render settled; the stage shows its static frame.

"use client";

import { cn, useInView, useReducedMotion, type BlockCopy } from "@pax-sh/blocks";

export interface HeroSplitProps {
  copy?: BlockCopy;
  className?: string;
}

const DEFAULT_COPY: BlockCopy = {
  eyebrow: "Pax effects",
  title: "The stage is half the story.",
  description:
    "Copy earns attention, the visual keeps it. Pax pairs every layout with a live stage you can swap for canvas work, product shots, or motion, without touching the grid.",
  primaryCta: { label: "Install the CLI", href: "/docs/installation" },
  secondaryCta: { label: "See the stages", href: "/blocks" },
};

/**
 * Copy column on the left, a layered visual stage on the right: stacked
 * glass panels floating over a slowly drifting acid gradient, all CSS.
 * The two columns sit on a strict grid that collapses cleanly on mobile.
 * Reduced motion freezes the gradient and renders both columns settled.
 */
export function HeroSplit({ copy = DEFAULT_COPY, className }: HeroSplitProps) {
  const reduced = useReducedMotion();
  const [ref, inView] = useInView<HTMLDivElement>({ threshold: 0.2 });
  const settled = reduced || inView;

  const enter = (step: number) =>
    reduced
      ? undefined
      : ({
          opacity: settled ? 1 : 0,
          transform: settled ? "translateY(0)" : "translateY(14px)",
          transition: `opacity 0.55s ease ${step * 0.09}s, transform 0.6s cubic-bezier(0.22, 1, 0.36, 1) ${step * 0.09}s`,
        } as const);

  return (
    <section
      className={cn(
        "relative overflow-hidden bg-[var(--pax-paper,#f6f2ea)] px-6 py-16 text-[var(--pax-ink,#12110f)] md:px-10 md:py-24",
        className,
      )}
      data-pax-asset="block-hero-split-02"
    >
      <style>{`
        @keyframes pax-split-drift {
          0% { transform: translate3d(-6%, -4%, 0) scale(1); }
          50% { transform: translate3d(6%, 5%, 0) scale(1.12); }
          100% { transform: translate3d(-6%, -4%, 0) scale(1); }
        }
      `}</style>

      <div
        ref={ref}
        className="mx-auto grid max-w-6xl items-center gap-12 md:grid-cols-2 md:gap-10"
      >
        <div>
          {copy.eyebrow ? (
            <div className="flex items-center gap-4" style={enter(0)}>
              <span className="h-px w-10 bg-current opacity-40" aria-hidden="true" />
              <p className="m-0 text-xs font-medium uppercase tracking-[0.22em] text-[var(--pax-mute,#6b645c)]">
                {copy.eyebrow}
              </p>
            </div>
          ) : null}

          <h1
            className="mt-6 text-4xl font-semibold leading-[1.05] tracking-tight [text-wrap:balance] md:text-5xl"
            style={enter(1)}
          >
            {copy.title}
          </h1>

          {copy.description ? (
            <p
              className="mt-5 max-w-xl text-base leading-relaxed text-[var(--pax-mute,#6b645c)] md:text-lg"
              style={enter(2)}
            >
              {copy.description}
            </p>
          ) : null}

          {copy.primaryCta || copy.secondaryCta ? (
            <div className="mt-9 flex flex-wrap items-center gap-4" style={enter(3)}>
              {copy.primaryCta ? (
                <a
                  href={copy.primaryCta.href}
                  className="rounded-full bg-[var(--pax-ink,#12110f)] px-6 py-3 text-sm font-semibold text-[var(--pax-paper,#f6f2ea)] outline-offset-4 transition-colors hover:bg-[var(--pax-night,#0b0d10)] focus-visible:outline focus-visible:outline-2"
                  {...(copy.primaryCta.goal ? { "data-fast-goal": copy.primaryCta.goal } : {})}
                >
                  {copy.primaryCta.label}
                </a>
              ) : null}
              {copy.secondaryCta ? (
                <a
                  href={copy.secondaryCta.href}
                  className="text-sm font-semibold underline decoration-[var(--pax-acid,#c8ff00)] decoration-2 underline-offset-4 outline-offset-4 focus-visible:outline focus-visible:outline-2"
                  {...(copy.secondaryCta.goal ? { "data-fast-goal": copy.secondaryCta.goal } : {})}
                >
                  {copy.secondaryCta.label}
                </a>
              ) : null}
            </div>
          ) : null}
        </div>

        <div
          aria-hidden="true"
          className="relative isolate h-72 overflow-hidden rounded-2xl border border-[var(--pax-border,rgba(18,17,15,0.12))] bg-[var(--pax-night,#0b0d10)] md:h-96"
          style={enter(2)}
        >
          {/* Drifting acid gradient field behind the panels. */}
          <div
            className="absolute -inset-1/4"
            style={{
              background:
                "radial-gradient(42% 46% at 32% 36%, var(--pax-acid,#c8ff00) 0%, transparent 62%), radial-gradient(38% 42% at 70% 68%, var(--pax-mute,#6b645c) 0%, transparent 66%)",
              opacity: 0.85,
              filter: "blur(28px)",
              animation: reduced ? "none" : "pax-split-drift 14s ease-in-out infinite",
            }}
          />

          {/* Stacked glass panels. */}
          <div
            className="absolute left-[12%] top-[16%] h-[54%] w-[62%] rounded-xl border border-[rgba(255,255,255,0.22)] backdrop-blur-md"
            style={{
              background: "linear-gradient(160deg, rgba(255,255,255,0.16), rgba(255,255,255,0.05))",
              boxShadow: "inset 0 1px 0 rgba(255,255,255,0.35)",
            }}
          />
          <div
            className="absolute left-[30%] top-[38%] h-[46%] w-[56%] rounded-xl border border-[rgba(255,255,255,0.28)] backdrop-blur-md"
            style={{
              background: "linear-gradient(160deg, rgba(255,255,255,0.22), rgba(255,255,255,0.07))",
              boxShadow: "inset 0 1px 0 rgba(255,255,255,0.45)",
            }}
          >
            <span className="absolute left-4 top-4 h-2 w-2 rounded-full bg-[var(--pax-acid,#c8ff00)]" />
            <span className="absolute left-4 top-4 mt-5 block h-1.5 w-2/3 rounded-full bg-[rgba(255,255,255,0.3)]" />
            <span className="absolute left-4 top-4 mt-9 block h-1.5 w-1/2 rounded-full bg-[rgba(255,255,255,0.2)]" />
          </div>
          <div
            className="absolute left-[8%] top-[62%] h-[30%] w-[38%] rounded-xl border border-[rgba(255,255,255,0.18)] backdrop-blur-md"
            style={{
              background: "linear-gradient(160deg, rgba(255,255,255,0.12), rgba(255,255,255,0.04))",
              boxShadow: "inset 0 1px 0 rgba(255,255,255,0.3)",
            }}
          />
        </div>
      </div>
    </section>
  );
}