Working with an AI assistant? Copy this prompt into Claude Code, Cursor or Copilot — it carries the install steps, a no-CLI fallback and a verification checklist.
Add the "motion" item from the logic2b ui registry (https://ui.logic2b.com) to this project. Follow every step and verify at the end.
## Steps
1. If the project has no `components.json` yet, set up logic2b ui first:
```bash
npx logic2b@latest init
```
2. Install the item (the CLI resolves registry dependencies automatically):
```bash
npx logic2b@latest add motion
```
3. Install any npm dependencies the command prints.
## If the CLI is not available
Do it manually from the registry payload:
1. Fetch https://ui.logic2b.com/r/motion.json.
2. Write every entry in `files[]` to its mapped project path (see below).
3. Repeat (recursively) for each name in `registryDependencies`.
4. Install every package listed in `dependencies` across those payloads.
## Registry reference
- Index of every item: https://ui.logic2b.com/r/index.json
- Item payload (full source): https://ui.logic2b.com/r/<name>.json
- Usage examples per item: https://ui.logic2b.com/r/demos/index.json → https://ui.logic2b.com/r/demos/<demo>.json
- Docs index for agents: https://ui.logic2b.com/llms.txt (full docs: https://ui.logic2b.com/llms-full.txt)
- Every docs page is Markdown when you append `.md` to its URL.
Registry file paths map to project paths like this:
- `ui/*` → `@/components/ui/*`
- `blocks/<name>/*` → `@/components/*`
- `charts/*` → `@/components/charts/*`
- `hooks/*` → `@/hooks/*`
- `lib/utils.ts` → `@/lib/utils.ts`
- `theme.css` → next to the project's Tailwind entry stylesheet
## Conventions (do not break these)
- Tailwind CSS v4 (CSS-first config; no tailwind.config.js needed) and React 19.
- All colors go through semantic tokens (`bg-primary`, `text-muted-foreground`,
`border-border`, …). Never hardcode hex/oklch values in components.
- Dark mode is class-based: toggle `.dark` on `<html>`.
- Icons come from lucide-react.
## Verify before finishing
1. The dev server starts and the app renders without console errors.
2. TypeScript compiles (`tsc --noEmit` or the framework's check).
3. Components use the theme: a `<Button>` shows the primary token color,
and toggling `.dark` on `<html>` switches the palette.
Install the dependencies:
npm install radix-ui
Copy into ui/motion.tsx:
"use client"import * as React from "react"import { Slot } from "radix-ui"import { cn } from "@/lib/utils"/** * Motion presets — enter/exit/hover recipes expressed as token-driven class * strings on top of `tw-animate-css` (zero runtime deps). The `<Motion>` * primitive plays an enter recipe once on mount; the exported maps and helpers * let you drop the same recipes onto anything else (a Radix `data-[state]` * element, a hand-rolled transition). * * Timing rides the CSS custom properties tw-animate-css reads: * - default duration/easing come from the static `duration-*`/`ease-*` classes * baked into `motionEnter`, * - the `duration`/`delay` props override them per-instance via * `--tw-animation-duration` / `--tw-animation-delay` (no arbitrary Tailwind * class is generated at runtime, so it works with any value). * * Everything degrades under `prefers-reduced-motion`: the enter animation is * dropped (content shows immediately) and hover transitions are disabled. */export type MotionPreset = | "fade" | "fade-up" | "fade-down" | "fade-left" | "fade-right" | "scale" | "blur"export type MotionHover = "lift" | "sink" | "scale" | "glow"/** Enter recipes: tw-animate-css `animate-in` sets the keyframe `from` state, * the element animates to its natural rendered state. */export const motionEnterPresets: Record<MotionPreset, string> = { fade: "fade-in", "fade-up": "fade-in slide-in-from-bottom-3", "fade-down": "fade-in slide-in-from-top-3", "fade-left": "fade-in slide-in-from-right-3", "fade-right": "fade-in slide-in-from-left-3", scale: "fade-in zoom-in-95", blur: "fade-in blur-in-8",}/** Exit recipes: the mirror of each enter preset, for `animate-out` on a * Radix `data-[state=closed]` element. */export const motionExitPresets: Record<MotionPreset, string> = { fade: "fade-out", "fade-up": "fade-out slide-out-to-top-3", "fade-down": "fade-out slide-out-to-bottom-3", "fade-left": "fade-out slide-out-to-left-3", "fade-right": "fade-out slide-out-to-right-3", scale: "fade-out zoom-out-95", blur: "fade-out blur-out-8",}/** Hover recipes: plain Tailwind transitions, no keyframes. */export const motionHoverPresets: Record<MotionHover, string> = { lift: "transition-transform duration-200 ease-out hover:-translate-y-1", sink: "transition-transform duration-200 ease-out hover:translate-y-0.5", scale: "transition-transform duration-200 ease-out hover:scale-[1.03]", glow: "transition-shadow duration-200 ease-out hover:shadow-lg",}/** Build the class string for an enter recipe (plays on mount). */export function motionEnter(preset: MotionPreset, className?: string) { return cn( "animate-in fill-mode-both duration-500 ease-out motion-reduce:animate-none", motionEnterPresets[preset], className )}/** Build the class string for an exit recipe (pair with `animate-out`). */export function motionExit(preset: MotionPreset, className?: string) { return cn( "animate-out fill-mode-both duration-300 ease-in motion-reduce:animate-none", motionExitPresets[preset], className )}/** Build the class string for a hover recipe. */export function motionHover(hover: MotionHover, className?: string) { return cn("motion-reduce:transition-none", motionHoverPresets[hover], className)}/** Per-instance timing overrides, applied through the custom properties * tw-animate-css reads so any numeric value works without a Tailwind class. */function motionVars(duration?: number, delay?: number): React.CSSProperties { const style: Record<string, string> = {} if (duration != null) style["--tw-animation-duration"] = `${duration}ms` if (delay != null) style["--tw-animation-delay"] = `${delay}ms` return style as React.CSSProperties}export interface MotionProps extends React.ComponentProps<"div"> { /** Enter recipe to play on mount. Default `"fade"`. */ preset?: MotionPreset /** Optional hover recipe applied to the same element. */ hover?: MotionHover /** Enter duration in milliseconds. Overrides the 500ms default. */ duration?: number /** Enter delay in milliseconds — the knob for staggering a list. */ delay?: number /** Merge props onto the single child instead of rendering a wrapper. */ asChild?: boolean}function Motion({ className, preset = "fade", hover, duration, delay, asChild = false, style, ...props}: MotionProps) { const Comp = asChild ? Slot.Root : "div" return ( <Comp data-slot="motion" className={cn(motionEnter(preset), hover && motionHover(hover), className)} style={{ ...motionVars(duration, delay), ...style }} {...props} /> )}export { Motion }
The motion engine ships the <Motion> primitive together with the recipe
maps and helpers. Each named preset is also installable on its own —
motion-fade,
motion-slide,
motion-scale and
motion-blur — and each one pulls in motion
automatically.
Usage
import { Motion } from "@/components/ui/motion"<Motion preset="fade-up"> <Card>…</Card></Motion>
<Motion> plays its enter recipe once, on mount, and then gets out of the way.
It renders a div by default; pass asChild to animate the child element
directly instead of adding a wrapper node:
<Motion> covers the mount case. For an element that also animates out — a
dialog, a popover, anything with a Radix data-[state] — reach for the recipe
maps and drive both states off data-state instead:
import { motionEnterPresets, motionExitPresets,} from "@/components/ui/motion"// Applied to a Radix content node:<DialogContent className={cn( "data-[state=open]:animate-in data-[state=closed]:animate-out", "data-[state=open]:fade-in data-[state=open]:zoom-in-95", "data-[state=closed]:fade-out data-[state=closed]:zoom-out-95", )}/>
motionEnterPresets / motionExitPresets hold the exact class strings for
every preset if you’d rather look them up than hand-write the pair. The
motionEnter, motionExit and motionHover helpers build the full class
string (animation utility + timing + reduced-motion guard) for you.
Reduced motion
Every recipe degrades under prefers-reduced-motion: the enter animation is
dropped so content appears immediately (motion-reduce:animate-none) and hover
transitions are disabled (motion-reduce:transition-none). No extra wiring
needed.
The Framer Motion flavor
The presets above are deliberately CSS-only: they cost nothing at runtime and
handle the enter/hover cases most interfaces need. When you want spring physics,
shared-layout transitions or exit animations on plain (non-Radix) elements, drop
in the optional Framer Motion flavor of the same recipe. Install
framer-motion, then copy this alongside the CSS engine: