# Throughline Feature

> A feature section for an AI product-photo studio: one product shot in the middle of a stage, and every crop and format it becomes riding out from under it along five dotted spokes (marketplace square, feed, story, banner, alt text, WebP). Copy and three numbers sit beside it. Hold a tile to stop its spoke and read it.

A shadcn/ui block from the [@keyframeui](https://keyframeui.com/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/keyframeui/throughline-feature).

- Page, with a live preview: https://shadcn.exchange/r/keyframeui/throughline-feature
- Screenshot: https://shadcn.exchange/shot/keyframeui/throughline-feature/light.png
- Registry item: https://keyframeui.com/r/throughline-feature.json
- Categories: blocks, feature-sections
- Listed: 2026-11-02, source last changed 2026-11-02

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/keyframeui/throughline-feature.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `https://keyframeui.com/r/throughline.json`

## Notes from the registry

Live preview and the components it's built with: https://keyframeui.com/blocks/feature-sections/throughline-feature

## Files

### `registry/blocks/throughline-feature.tsx`

```tsx
"use client";

import * as React from "react";

import { Throughline, type ThroughlineItem, type ThroughlineLine } from "@/components/ui/throughline";

/* ── Content ─────────────────────────────────────────────────
 * Everything you'd change to make the section yours: the
 * palette, the copy, the product shot and the formats. Every
 * line starts under the shot, so each format slides out from
 * behind it and rides off to its station.
 * ─────────────────────────────────────────────────────────── */

const PALETTE = {
  light: { paper: "#ffffff", ink: "#0b0b10", accent: "#5b2bff", stage: "#f4f2fb" },
  dark: { paper: "#07070c", ink: "#f5f5f7", accent: "#9b83ff", stage: "#0f0e16" },
};

const EYEBROW = "Every crop";
const HEADLINE = "One shot in. Every size your store needs out.";
const BODY =
  "Loupe reframes the same photo for each place it's going, keeps the product centered and the shadow where the light says it falls, and writes the alt text while it's there.";
const STATS = [
  { value: "38", label: "crops and formats from one upload" },
  { value: "84 KB", label: "average listing image, WebP" },
  { value: "9", label: "languages of alt text" },
];
const LINK = { label: "See every format", href: "#formats" };

const SHOT = {
  src: "https://images.unsplash.com/photo-1505740420928-5e560c06d30e?auto=format&fit=crop&w=720&h=720&q=80",
  alt: "Over-ear headphones on a yellow backdrop",
  caption: "studio-headphones.jpg",
};

/** An aspect-ratio frame: the format itself, drawn to scale. */
function Frame({ w, h }: { w: number; h: number }) {
  const s = 16 / Math.max(w, h);
  return (
    <svg viewBox="0 0 24 24" fill="none" aria-hidden>
      <rect x={12 - (w * s) / 2} y={12 - (h * s) / 2} width={w * s} height={h * s} rx="1.6" stroke="var(--accent)" strokeWidth="2" />
    </svg>
  );
}

const FORMATS: ThroughlineItem[] = [
  { label: "1:1 · Marketplace listing", line: 0, icon: <Frame w={1} h={1} /> },
  { label: "4:5 · Feed post", line: 1, icon: <Frame w={4} h={5} /> },
  { label: "9:16 · Stories and Reels", line: 2, icon: <Frame w={9} h={16} /> },
  { label: "16:9 · Site banner", line: 3, icon: <Frame w={16} h={9} /> },
  { label: "3:4 · Lookbook page", line: 4, icon: <Frame w={3} h={4} /> },
  {
    label: "Alt text · “Black over-ear headphones…”",
    line: 0,
    icon: (
      <svg viewBox="0 0 24 24" fill="none" aria-hidden>
        <path d="m4.5 18 4.5-12 4.5 12M6.2 14h5.6M16.5 9.5v8.5M16.5 6v.01" stroke="var(--accent)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
      </svg>
    ),
  },
  {
    label: "WebP · 84 KB, 2400px",
    line: 2,
    icon: (
      <svg viewBox="0 0 24 24" fill="none" aria-hidden>
        <path d="M7 4.5h7l4 4v11H7Z M14 4.5v4h4" stroke="var(--accent)" strokeWidth="2" strokeLinejoin="round" />
      </svg>
    ),
  },
  {
    label: "2:3 · Pinterest pin",
    line: 1,
    icon: <Frame w={2} h={3} />,
  },
  {
    label: "Shadow · soft, light from 30° left",
    line: 3,
    icon: (
      <svg viewBox="0 0 24 24" fill="none" aria-hidden>
        <circle cx="12" cy="10" r="4.5" stroke="var(--accent)" strokeWidth="2" />
        <ellipse cx="13.5" cy="19" rx="5.5" ry="1.4" fill="var(--accent)" opacity="0.45" />
      </svg>
    ),
  },
  {
    label: "Cutout · PNG with transparency",
    line: 4,
    icon: (
      <svg viewBox="0 0 24 24" fill="none" aria-hidden>
        <rect x="4.5" y="4.5" width="15" height="15" rx="2" stroke="var(--accent)" strokeWidth="2" strokeDasharray="2.6 2.4" />
      </svg>
    ),
  },
];

/** Five spokes out from under the shot at [50, 50]. */
const LINES: ThroughlineLine[] = [
  { from: [50, 50], to: [8, 6], bend: 0.1, behind: true },
  { from: [50, 50], to: [94, 8], bend: -0.12, behind: true },
  { from: [50, 50], to: [98, 54], bend: 0.08, behind: true },
  { from: [50, 50], to: [90, 96], bend: 0.12, behind: true },
  { from: [50, 50], to: [6, 90], bend: -0.1, behind: true },
];

/* ── Section ─────────────────────────────────────────────── */

export function ThroughlineFeature() {
  return (
    <section
      style={
        {
          "--paper-light": PALETTE.light.paper,
          "--ink-light": PALETTE.light.ink,
          "--accent-light": PALETTE.light.accent,
          "--stage-light": PALETTE.light.stage,
          "--paper-dark": PALETTE.dark.paper,
          "--ink-dark": PALETTE.dark.ink,
          "--accent-dark": PALETTE.dark.accent,
          "--stage-dark": PALETTE.dark.stage,
        } as React.CSSProperties
      }
      className="relative overflow-hidden bg-(--paper) text-(--ink) [--accent:var(--accent-light)] [--ink:var(--ink-light)] [--paper:var(--paper-light)] [--stage:var(--stage-light)] dark:[--accent:var(--accent-dark)] dark:[--ink:var(--ink-dark)] dark:[--paper:var(--paper-dark)] dark:[--stage:var(--stage-dark)]"
    >
      <div className="mx-auto grid max-w-6xl items-center gap-12 px-6 py-24 sm:py-28 lg:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)] lg:gap-16">
        <div>
          <p className="text-sm font-semibold text-(--accent)">{EYEBROW}</p>
          <h2 className="mt-3 text-4xl font-semibold tracking-[-0.04em] text-balance sm:text-5xl">{HEADLINE}</h2>
          <p className="mt-5 max-w-md text-[16px] leading-relaxed text-pretty opacity-70">{BODY}</p>
          <dl className="mt-9 grid max-w-md grid-cols-3 gap-5 border-t border-current/10 pt-6">
            {STATS.map((stat) => (
              <div key={stat.label}>
                <dt className="sr-only">{stat.label}</dt>
                <dd className="text-2xl font-semibold tracking-[-0.03em] tabular-nums">{stat.value}</dd>
                <dd className="mt-1 text-[13px] leading-snug opacity-60">{stat.label}</dd>
              </div>
            ))}
          </dl>
          <a
            href={LINK.href}
            className="group mt-9 inline-flex items-center gap-1.5 rounded-full text-[15px] font-semibold text-(--accent) focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-(--accent)"
          >
            {LINK.label}
            <svg viewBox="0 0 16 16" fill="none" aria-hidden className="size-3.5 transition-transform duration-150 ease-out group-hover:translate-x-0.5">
              <path d="M3 8h10m-4-4 4 4-4 4" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" />
            </svg>
          </a>
        </div>

        <Throughline lines={LINES} items={FORMATS} duration={18} className="rounded-[32px] bg-(--stage) text-(--accent) [--throughline-tile:clamp(2.5rem,8.5cqi,3.5rem)]">
          <div className="flex h-[clamp(22rem,42vw,30rem)] items-center justify-center">
            <figure className="w-[min(15rem,46%)] rounded-[22px] bg-(--paper) p-2 shadow-[0_30px_60px_-30px_rgb(30_20_80/0.45),0_0_0_1px_rgb(0_0_0/0.04)] dark:shadow-[0_30px_60px_-30px_rgb(0_0_0/0.9),0_0_0_1px_rgb(255_255_255/0.06)]">
              {/* eslint-disable-next-line @next/next/no-img-element */}
              <img src={SHOT.src} alt={SHOT.alt} width={720} height={720} className="aspect-square w-full rounded-[16px] object-cover" />
              <figcaption className="flex items-center justify-between px-1.5 pt-2 pb-0.5 text-[11px] text-(--ink)">
                <span className="truncate font-medium">{SHOT.caption}</span>
                <span className="shrink-0 pl-2 whitespace-nowrap opacity-50">38 out</span>
              </figcaption>
            </figure>
          </div>
        </Throughline>
      </div>
    </section>
  );
}
```
