Velora
DocsInstallation · five steps

There is no package to install and no version to track. Set up the token layer once, then copy components into your project one file at a time.

  1. Install the dependencies

    Motion drives every animation, clsx and tailwind-merge back the cn() helper, and lucide-react draws the one glyph the loaders use — the check when a pass lands.

    terminal
    npm i motion clsx tailwind-merge lucide-react
  2. Add the class helper

    Every component merges its own classes with whatever you pass in, so this file has to exist before anything else compiles.

    src/lib/utils.ts
    // src/lib/utils.tsimport { clsx, type ClassValue } from "clsx";import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) {  return twMerge(clsx(inputs));}
  3. Paste the token layer

    Both themes declare the same names, so nothing downstream branches on theme. The loaders read --accent for their signal colour; each also takes its own tint property, so you can retheme one instance without touching the others.

    src/app/globals.css
    /* src/app/globals.css */@import "tailwindcss";@custom-variant dark (&:where(.dark, .dark *)); :root {  --bg: oklch(1 0 0);  --bg-sub: oklch(0.978 0 0);  --fg: oklch(0.16 0 0);  --fg-muted: oklch(0.46 0 0);  --fg-faint: oklch(0.62 0 0);  --line: oklch(0 0 0 / 0.085);  --accent: oklch(0.16 0 0);   /* the loaders' signal colour */} .dark {  --bg: oklch(0.145 0 0);  --bg-sub: oklch(0.168 0 0);  --fg: oklch(0.95 0 0);  --fg-muted: oklch(0.71 0 0);  --fg-faint: oklch(0.52 0 0);  --line: oklch(1 0 0 / 0.085);  --accent: oklch(0.98 0 0);} @theme inline {  --color-bg: var(--bg);  --color-bg-sub: var(--bg-sub);  --color-fg: var(--fg);  --color-fg-muted: var(--fg-muted);  --color-fg-faint: var(--fg-faint);  --color-line: var(--line);  --color-accent: var(--accent);} /* Each component names the utilities it needs in its   Source line — copy those blocks from globals.css too. */
  4. Add the motion presets

    Components import these by name. Keeping the numbers in one file is what stops a codebase drifting into forty slightly different springs.

    src/lib/motion.ts
    // src/lib/motion.ts// Named for what they are for, never for their numbers.export const spring = {  press:  { type: "spring", stiffness: 620, damping: 30, mass: 0.6 },  snappy: { type: "spring", stiffness: 420, damping: 32, mass: 0.7 },  smooth: { type: "spring", stiffness: 260, damping: 30, mass: 0.9 },  liquid: { type: "spring", stiffness: 170, damping: 21, mass: 1.1 },  gentle: { type: "spring", stiffness: 110, damping: 20, mass: 1 },} as const;
  5. Copy a component and use it

    Each component is a single self-contained file. Leave value off and it runs its own pass; pass your job's progress and it follows. Children are the finished image, revealed when the pass lands.

    src/components/result.tsx
    import { ImageGeneration } from "@/components/ui/image"; export function Result({ job }: { job: GenerationJob }) {  return (    <ImageGeneration      value={job.status === "queued" ? undefined : job.progress}      label={job.stage}      onComplete={() => track("image_ready", job.id)}    >      <img src={job.url} alt={job.prompt} />    </ImageGeneration>  );}