# Checkout Flow

> Timeline-driven checkout flow.

A shadcn/ui block from the [@framecn](https://framecn.vercel.app) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/framecn/checkout-flow).

- Page, with a live preview: https://shadcn.exchange/r/framecn/checkout-flow
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/framecn/checkout-flow-light-cc02876047765d86.png
- Registry item: https://framecn.vercel.app/r/checkout-flow.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://framecn.vercel.app/r/checkout-flow.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: `@framecn/framecn-ui`, `@framecn/blur-in`, `@framecn/button`, `@framecn/checkbox`, `@framecn/cursor`, `@framecn/field`, `@framecn/input`, `@framecn/toast`, `@framecn/toggle-group`

## Files

### `registry/bases/editframe/ui/checkout-flow/index.tsx`

```tsx
"use client";

import { useFramecnTheme } from "@/lib/framecn-ui";
import type { FramecnTheme } from "@/lib/framecn-ui";
import { BlurIn } from "@/components/framecn/blur-in";
import { useBlurInTransition } from "@/components/framecn/use-blur-in-transition";
import { Button } from "@/components/framecn/button";
import { useButtonTransition } from "@/components/framecn/use-button-transition";
import { Checkbox } from "@/components/framecn/checkbox";
import { useCheckboxTransition } from "@/components/framecn/use-checkbox-transition";
import { Cursor } from "@/components/framecn/cursor";
import { useCursorPath } from "@/components/framecn/use-cursor-path";
import {
  Field,
  FieldControl,
  FieldGroup,
  FieldLabel,
} from "@/components/framecn/field";
import { Input } from "@/components/framecn/input";
import { useInputTransition } from "@/components/framecn/use-input-transition";
import { Toast } from "@/components/framecn/toast";
import { useToastTransition } from "@/components/framecn/use-toast-transition";
import { ToggleGroup } from "@/components/framecn/toggle-group";
import type { ToggleGroupItem } from "@/components/framecn/toggle-group";
import { useToggleGroupTransition } from "@/components/framecn/use-toggle-group-transition";

const DEFAULT_PLANS: ToggleGroupItem[] = [
  { label: "Monthly", value: "monthly" },
  { label: "Yearly", value: "yearly" },
];
const STAGE_W = 1280;
const CARD_W = 420;
const CARD_PAD = 28;
const CARD_TOP = 96;
const CARD_LEFT = (STAGE_W - CARD_W) / 2;
const CENTER_X = STAGE_W / 2;
const CONTENT_LEFT = CARD_LEFT + CARD_PAD;
const CONTENT_RIGHT = CARD_LEFT + CARD_W - CARD_PAD;
const TOGGLE_Y = 222;
const YEARLY_X = CONTENT_LEFT + 4 + 88 + 44;
const CARD_X = CENTER_X;
const CARD_Y = 312;
const TERMS_X = CONTENT_LEFT + 12;
const TERMS_Y = 376;
const PAY_X = CONTENT_RIGHT - 48;
const PAY_Y = 442;
export interface CheckoutFlowProps {
  title?: string;
  description?: string;
  plans?: ToggleGroupItem[];
  cardLabel?: string;
  cardPlaceholder?: string;
  termsLabel?: string;
  payLabel?: string;
  toastTitle?: string;
  theme?: Partial<FramecnTheme>;
}

export const CheckoutFlow = ({
  title = "Upgrade your plan",
  description = "Complete your purchase to unlock every feature.",
  plans = DEFAULT_PLANS,
  cardLabel = "Card number",
  cardPlaceholder = "4242 4242 4242 4242",
  termsLabel = "I accept the terms and conditions",
  payLabel = "Pay $49",
  toastTitle = "Payment successful",
  theme,
}: CheckoutFlowProps) => {
  const resolved = useFramecnTheme(theme);
  const opts = { theme };
  const cardEnter = useBlurInTransition(
    [{ at: 0, duration: 18, state: "revealed" }],
    { distance: 0 }
  );
  const enterHeader = useBlurInTransition([
    { at: 18, duration: 16, state: "revealed" },
  ]);
  const enterToggle = useBlurInTransition([
    { at: 24, duration: 16, state: "revealed" },
  ]);
  const enterCard = useBlurInTransition([
    { at: 30, duration: 16, state: "revealed" },
  ]);
  const enterTerms = useBlurInTransition([
    { at: 36, duration: 16, state: "revealed" },
  ]);
  const enterButton = useBlurInTransition([
    { at: 42, duration: 16, state: "revealed" },
  ]);
  const cursorStyle = useCursorPath([
    { at: 0, x: 140, y: 90 },
    { at: 64, click: true, duration: 22, x: YEARLY_X, y: TOGGLE_Y },
    { at: 96, click: true, duration: 24, x: CARD_X, y: CARD_Y },
    { at: 150, click: true, duration: 30, x: TERMS_X, y: TERMS_Y },
    { at: 180, click: true, duration: 26, x: PAY_X, y: PAY_Y },
  ]);
  const toggleStyle = useToggleGroupTransition(
    [{ at: 64, duration: 16, state: plans[1]?.value ?? "yearly" }],
    { items: plans, ...opts }
  );
  const cardStyle = useInputTransition(
    [
      { at: 96, duration: 6, state: "active" },
      { at: 100, duration: 40, state: "typing" },
      { at: 150, duration: 8, state: "blur" },
    ],
    opts
  );
  const checkboxStyle = useCheckboxTransition(
    [{ at: 150, duration: 14, state: "checked" }],
    opts
  );
  const payStyle = useButtonTransition(
    [
      { at: 172, duration: 8, state: "hover" },
      { at: 180, duration: 6, state: "press" },
      { at: 186, duration: 6, state: "loading" },
      { at: 224, duration: 16, state: "success" },
    ],
    opts
  );
  const toastStyle = useToastTransition(
    [
      { at: 224, duration: 14, state: "visible" },
      { at: 286, duration: 14, state: "hidden" },
    ],
    {}
  );
  return (
    <div
      style={{
        background: "transparent",
        fontFamily:
          "var(--font-geist-sans), -apple-system, BlinkMacSystemFont, sans-serif",
        height: "100%",
        position: "relative",
        width: "100%",
      }}
    >
      <div
        style={{
          background: resolved.card,
          border: `1px solid ${resolved.border}`,
          borderRadius: resolved.radius + 4,
          boxShadow:
            "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
          boxSizing: "border-box",
          color: resolved.cardForeground,
          display: "flex",
          filter: cardEnter.blur > 0 ? `blur(${cardEnter.blur}px)` : "none",
          flexDirection: "column",
          gap: 24,
          height: 420,
          left: CARD_LEFT,
          opacity: cardEnter.opacity,
          padding: 28,
          position: "absolute",
          top: CARD_TOP,
          width: CARD_W,
        }}
      >
        <BlurIn display="block" style={enterHeader}>
          <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
            <div
              style={{
                color: resolved.cardForeground,
                fontSize: 16,
                fontWeight: 600,
                lineHeight: 1,
              }}
            >
              {title}
            </div>
            <div
              style={{
                color: resolved.mutedForeground,
                fontSize: 14,
                lineHeight: "20px",
              }}
            >
              {description}
            </div>
          </div>
        </BlurIn>

        <BlurIn display="block" style={enterToggle}>
          <FieldControl height={44}>
            <ToggleGroup
              style={toggleStyle}
              items={plans}
              align="start"
              theme={theme}
            />
          </FieldControl>
        </BlurIn>

        <FieldGroup gap={24}>
          <BlurIn display="block" style={enterCard}>
            <Field>
              <FieldLabel theme={theme}>{cardLabel}</FieldLabel>
              <FieldControl>
                <Input
                  placeholder={cardPlaceholder}
                  value={cardPlaceholder}
                  style={cardStyle}
                  fullWidth
                  theme={theme}
                />
              </FieldControl>
            </Field>
          </BlurIn>

          <BlurIn display="block" style={enterTerms}>
            <FieldControl>
              <Checkbox
                label={termsLabel}
                style={checkboxStyle}
                align="start"
                theme={theme}
              />
            </FieldControl>
          </BlurIn>
        </FieldGroup>

        <BlurIn display="block" style={enterButton}>
          <Field gap={10}>
            <FieldControl height={44}>
              <Button
                label={payLabel}
                style={payStyle}
                align="end"
                theme={theme}
              />
            </FieldControl>
          </Field>
        </BlurIn>
      </div>

      <div style={{ bottom: 32, position: "absolute", right: 32 }}>
        <Toast
          title={toastTitle}
          variant="success"
          style={toastStyle}
          theme={theme}
        />
      </div>

      <Cursor style={cursorStyle} variant="pointer" theme={theme} />
    </div>
  );
};
```
