# Ai Prompt Flow

> Timeline-driven ai prompt flow.

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

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

## Install

```bash
npx shadcn@latest add https://framecn.vercel.app/r/ai-prompt-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/button`, `@framecn/input`, `@framecn/skeleton`, `@framecn/skeleton-block`, `@framecn/toast`

## Files

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

```tsx
"use client";

import { mixOklch, useFramecnTheme } from "@/lib/framecn-ui";
import type { FramecnTheme } from "@/lib/framecn-ui";
import { Button } from "@/components/framecn/button";
import { useButtonTransition } from "@/components/framecn/use-button-transition";
import { Input } from "@/components/framecn/input";
import { useInputTransition } from "@/components/framecn/use-input-transition";
import { Skeleton } from "@/components/framecn/skeleton";
import { SkeletonBlock } from "@/components/framecn/skeleton-block";
import { useSkeletonTransition } from "@/components/framecn/use-skeleton-transition";
import { Toast } from "@/components/framecn/toast";
import { useToastTransition } from "@/components/framecn/use-toast-transition";

export interface AiPromptFlowProps {
  prompt?: string;
  buttonLabel?: string;
  answerLines?: string[];
  toastTitle?: string;
  theme?: Partial<FramecnTheme>;
}
const STAGE_W = 1280;
const PROMPT_W = 520;
const PROMPT_LEFT = (STAGE_W - PROMPT_W) / 2;
const PROMPT_TOP = 168;
const PROMPT_H = 92;
const BTN_W = 200;
const BTN_LEFT = (STAGE_W - BTN_W) / 2;
const BTN_TOP = 278;
const BTN_H = 64;
const ANSWER_W = 560;
const ANSWER_LEFT = (STAGE_W - ANSWER_W) / 2;
const ANSWER_TOP = 380;
const DEFAULT_ANSWER = [
  "The thread debates the Q3 roadmap: ship the editor first,",
  "defer billing to Q4, and pull the migration forward so",
  "infra is unblocked before the team scales next quarter.",
];
export const AiPromptFlow = ({
  prompt = "Summarize this thread",
  buttonLabel = "Generate",
  answerLines = DEFAULT_ANSWER,
  toastTitle = "Response ready",
  theme,
}: AiPromptFlowProps) => {
  const resolved = useFramecnTheme(theme);
  const opts = { theme };
  const inputStyle = useInputTransition(
    [
      { at: 0, duration: 1, state: "active" },
      { at: 0, duration: 50, state: "typing" },
    ],
    opts
  );
  const buttonStyle = useButtonTransition(
    [
      { at: 52, duration: 6, state: "hover" },
      { at: 58, duration: 4, state: "press" },
      { at: 62, duration: 4, state: "loading" },
    ],
    opts
  );
  const skeletonStyle = useSkeletonTransition(
    [
      { at: 64, duration: 1, state: "loading" },
      { at: 150, duration: 16, state: "loaded" },
    ],
    {}
  );
  const panelOpacity = buttonStyle.spinnerOpacity;
  const toastStyle = useToastTransition(
    [
      { at: 160, duration: 14, state: "visible" },
      { at: 220, duration: 14, state: "hidden" },
    ],
    {}
  );
  return (
    <div
      style={{
        background: "transparent",
        height: "100%",
        position: "relative",
        width: "100%",
      }}
    >
      <div
        style={{
          height: PROMPT_H,
          left: PROMPT_LEFT,
          position: "absolute",
          top: PROMPT_TOP,
          width: PROMPT_W,
        }}
      >
        <Input
          placeholder="Ask anything…"
          value={prompt}
          style={inputStyle}
          theme={theme}
        />
      </div>

      <div
        style={{
          height: BTN_H,
          left: BTN_LEFT,
          position: "absolute",
          top: BTN_TOP,
          width: BTN_W,
        }}
      >
        <Button label={buttonLabel} style={buttonStyle} theme={theme} />
      </div>

      <div
        style={{
          display: "flex",
          justifyContent: "center",
          left: ANSWER_LEFT,
          opacity: panelOpacity,
          position: "absolute",
          top: ANSWER_TOP,
          width: ANSWER_W,
        }}
      >
        <Skeleton
          style={skeletonStyle}
          theme={theme}
          placeholder={
            <div
              style={{
                display: "flex",
                flexDirection: "column",
                gap: 14,
                width: ANSWER_W,
              }}
            >
              {answerLines.map((_, i) => (
                <SkeletonBlock
                  key={i}
                  width={i === answerLines.length - 1 ? "70%" : "100%"}
                  height={18}
                  baseColor={resolved.accent}
                  highlightColor={mixOklch(
                    resolved.accent,
                    resolved.foreground,
                    0.13
                  )}
                  radius={Math.max(0, resolved.radius - 2)}
                />
              ))}
            </div>
          }
        >
          <div
            style={{
              color: resolved.foreground,
              display: "flex",
              flexDirection: "column",
              fontFamily:
                "var(--font-geist-sans), -apple-system, BlinkMacSystemFont, sans-serif",
              fontSize: 14,
              gap: 8,
              lineHeight: 1.5,
              width: ANSWER_W,
            }}
          >
            {answerLines.map((line, i) => (
              <span key={i}>{line}</span>
            ))}
          </div>
        </Skeleton>
      </div>

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