# Signup Flow

> Timeline-driven signup flow.

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

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

## Install

```bash
npx shadcn@latest add https://framecn.vercel.app/r/signup-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/cursor`, `@framecn/field`, `@framecn/input`, `@framecn/toast`

## Files

### `registry/bases/editframe/ui/signup-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 { Cursor } from "@/components/framecn/cursor";
import { useCursorPath } from "@/components/framecn/use-cursor-path";
import {
  Field,
  FieldControl,
  FieldDescription,
  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";

export interface SignupFlowProps {
  title?: string;
  description?: string;
  fullName?: string;
  email?: string;
  password?: string;
  createLabel?: string;
  googleLabel?: string;
  signinText?: string;
  toastTitle?: string;
  theme?: Partial<FramecnTheme>;
}
const STAGE_W = 1280;
const CARD_W = 376;
const CARD_TOP = 48;
const CARD_LEFT = (STAGE_W - CARD_W) / 2;
const CENTER_X = STAGE_W / 2;
const NAME_Y = 216;
const EMAIL_Y = 296;
const PASS_Y = 414;
const CONFIRM_Y = 530;
const CREATE_Y = 590;
export const SignupFlow = ({
  title = "Create an account",
  description = "Enter your information below to create your account",
  fullName = "John Doe",
  email = "m@example.com",
  password = "••••••••",
  createLabel = "Create account",
  toastTitle = "Account created",
  theme,
}: SignupFlowProps) => {
  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 enterName = useBlurInTransition([
    { at: 24, duration: 16, state: "revealed" },
  ]);
  const enterEmail = useBlurInTransition([
    { at: 30, duration: 16, state: "revealed" },
  ]);
  const enterPass = useBlurInTransition([
    { at: 36, duration: 16, state: "revealed" },
  ]);
  const enterConfirm = useBlurInTransition([
    { at: 42, duration: 16, state: "revealed" },
  ]);
  const enterButton = useBlurInTransition([
    { at: 48, duration: 16, state: "revealed" },
  ]);
  const DEMO = 48;
  const cursorStyle = useCursorPath([
    { at: 0, x: 160, y: 120 },
    { at: 18 + DEMO, click: true, duration: 18, x: CENTER_X, y: NAME_Y },
    { at: 52 + DEMO, click: true, duration: 30, x: CENTER_X, y: EMAIL_Y },
    { at: 96 + DEMO, click: true, duration: 40, x: CENTER_X, y: PASS_Y },
    { at: 134 + DEMO, click: true, duration: 32, x: CENTER_X, y: CONFIRM_Y },
    { at: 176 + DEMO, click: true, duration: 38, x: CENTER_X, y: CREATE_Y },
  ]);
  const nameStyle = useInputTransition(
    [
      { at: 18 + DEMO, duration: 6, state: "active" },
      { at: 20 + DEMO, duration: 20, state: "typing" },
      { at: 52 + DEMO, duration: 8, state: "blur" },
    ],
    opts
  );
  const emailStyle = useInputTransition(
    [
      { at: 52 + DEMO, duration: 6, state: "active" },
      { at: 54 + DEMO, duration: 28, state: "typing" },
      { at: 96 + DEMO, duration: 8, state: "blur" },
    ],
    opts
  );
  const passStyle = useInputTransition(
    [
      { at: 96 + DEMO, duration: 6, state: "active" },
      { at: 98 + DEMO, duration: 22, state: "typing" },
      { at: 134 + DEMO, duration: 8, state: "blur" },
    ],
    opts
  );
  const confirmStyle = useInputTransition(
    [
      { at: 134 + DEMO, duration: 6, state: "active" },
      { at: 136 + DEMO, duration: 22, state: "typing" },
      { at: 176 + DEMO, duration: 8, state: "blur" },
    ],
    opts
  );
  const buttonStyle = useButtonTransition(
    [
      { at: 176 + DEMO, duration: 8, state: "hover" },
      { at: 186 + DEMO, duration: 6, state: "press" },
      { at: 192 + DEMO, duration: 6, state: "loading" },
      { at: 234 + DEMO, duration: 16, state: "success" },
    ],
    opts
  );
  const toastStyle = useToastTransition(
    [
      { at: 234 + DEMO, duration: 14, state: "visible" },
      { at: 300 + DEMO, 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: 580,
          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>

        <FieldGroup gap={16}>
          <BlurIn display="block" style={enterName}>
            <Field>
              <FieldLabel theme={theme}>Full Name</FieldLabel>
              <FieldControl>
                <Input
                  placeholder={fullName}
                  value={fullName}
                  style={nameStyle}
                  theme={theme}
                />
              </FieldControl>
            </Field>
          </BlurIn>

          <BlurIn display="block" style={enterEmail}>
            <Field>
              <FieldLabel theme={theme}>Email</FieldLabel>
              <FieldControl>
                <Input
                  placeholder={email}
                  value={email}
                  style={emailStyle}
                  theme={theme}
                />
              </FieldControl>
              <FieldDescription theme={theme}>
                We'll use this to contact you.
              </FieldDescription>
            </Field>
          </BlurIn>

          <BlurIn display="block" style={enterPass}>
            <Field>
              <FieldLabel theme={theme}>Password</FieldLabel>
              <FieldControl>
                <Input
                  placeholder={password}
                  value={password}
                  style={passStyle}
                  theme={theme}
                />
              </FieldControl>
              <FieldDescription theme={theme}>
                Must be at least 8 characters long.
              </FieldDescription>
            </Field>
          </BlurIn>

          <BlurIn display="block" style={enterConfirm}>
            <Field>
              <FieldLabel theme={theme}>Confirm Password</FieldLabel>
              <FieldControl>
                <Input
                  placeholder={password}
                  value={password}
                  style={confirmStyle}
                  theme={theme}
                />
              </FieldControl>
            </Field>
          </BlurIn>
        </FieldGroup>

        <BlurIn display="block" style={enterButton}>
          <Field gap={10}>
            <FieldControl>
              <Button label={createLabel} style={buttonStyle} 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>
  );
};
```
