Files
kit-ui/components/animate/PresetLibrary.tsx
Sebastian Krüger eeef3283c8 feat: add CSS Animation Editor tool
Comprehensive visual editor for CSS @keyframe animations:
- AnimationSettings: name, duration, delay, easing (incl. cubic-bezier), iteration, direction, fill-mode
- KeyframeTimeline: drag-to-reposition keyframe markers, click-track to add, delete selected
- KeyframeProperties: per-keyframe transform (translate/rotate/scale/skew), opacity, background-color, border-radius, blur, brightness via sliders
- AnimationPreview: live preview on box/circle/text element with play/pause/restart and speed control (0.25×–2×)
- PresetLibrary: 22 presets across Entrance/Exit/Attention/Special categories with animated thumbnails
- ExportPanel: plain CSS and Tailwind v4 @utility formats with copy and download

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 14:17:04 +01:00

105 lines
3.4 KiB
TypeScript

'use client';
import { useEffect, useRef } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { cn } from '@/lib/utils/cn';
import { PRESETS, PRESET_CATEGORIES } from '@/lib/animate/presets';
import { buildCSS } from '@/lib/animate/cssBuilder';
import type { AnimationConfig, AnimationPreset } from '@/types/animate';
interface Props {
currentName: string;
onSelect: (config: AnimationConfig) => void;
}
function PresetCard({ preset, isActive, onSelect }: {
preset: AnimationPreset;
isActive: boolean;
onSelect: () => void;
}) {
const styleRef = useRef<HTMLStyleElement | null>(null);
const key = `preset-${preset.id}`;
// Each card gets its own @keyframes injected with a unique name to avoid conflicts
useEffect(() => {
const renamedConfig = {
...preset.config,
name: key,
keyframes: preset.config.keyframes,
};
if (!styleRef.current) {
styleRef.current = document.createElement('style');
document.head.appendChild(styleRef.current);
}
styleRef.current.textContent = buildCSS(renamedConfig).replace(
/animation: \S+/,
`animation: ${key}`
);
return () => {
styleRef.current?.remove();
styleRef.current = null;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<button
onClick={onSelect}
className={cn(
'group relative flex flex-col items-center gap-2 p-3 rounded-xl border transition-all duration-200 text-left hover:border-primary/50 hover:bg-accent/30',
isActive ? 'border-primary bg-primary/5 shadow-sm shadow-primary/20' : 'border-border bg-card/50'
)}
>
{/* Mini preview */}
<div className="w-full h-14 flex items-center justify-center rounded-lg bg-muted/30 overflow-hidden">
<div
className="animated w-8 h-8 rounded-md bg-gradient-to-br from-violet-500 to-purple-600"
style={{ animationName: key, animationDuration: `${preset.config.duration}ms`, animationIterationCount: 'infinite', animationDirection: 'alternate' }}
/>
</div>
<span className={cn(
'text-[11px] font-medium text-center leading-tight',
isActive ? 'text-primary' : 'text-foreground/80'
)}>
{preset.name}
</span>
</button>
);
}
export function PresetLibrary({ currentName, onSelect }: Props) {
return (
<Card>
<CardHeader>
<CardTitle>Presets</CardTitle>
</CardHeader>
<CardContent>
<Tabs defaultValue="Entrance">
<TabsList className="mb-4">
{PRESET_CATEGORIES.map((cat) => (
<TabsTrigger key={cat} value={cat} className="text-xs">
{cat}
</TabsTrigger>
))}
</TabsList>
{PRESET_CATEGORIES.map((cat) => (
<TabsContent key={cat} value={cat}>
<div className="grid grid-cols-3 sm:grid-cols-4 lg:grid-cols-6 gap-2">
{PRESETS.filter((p) => p.category === cat).map((preset) => (
<PresetCard
key={preset.id}
preset={preset}
isActive={preset.id === currentName}
onSelect={() => onSelect(preset.config)}
/>
))}
</div>
</TabsContent>
))}
</Tabs>
</CardContent>
</Card>
);
}