314 lines
12 KiB
TypeScript
314 lines
12 KiB
TypeScript
'use client';
|
|
|
|
import Fuse from 'fuse.js';
|
|
import { index } from '@/__registry__';
|
|
import { MotionHighlight } from '@/registry/effects/motion-highlight';
|
|
import { AnimateIcon, staticAnimations } from '@/registry/icons/icon';
|
|
import { X } from '@/registry/icons/x';
|
|
import { Input } from '@workspace/ui/components/ui/input';
|
|
import { cn } from '@workspace/ui/lib/utils';
|
|
import { useEffect, useMemo, useState } from 'react';
|
|
import { motion } from 'motion/react';
|
|
import { CodeTabs } from '@/registry/components/code-tabs';
|
|
import { DynamicCodeBlock } from './dynamic-codeblock';
|
|
import {
|
|
Tabs,
|
|
TabsList,
|
|
TabsTrigger,
|
|
TabsContent,
|
|
TabsContents,
|
|
} from '@/registry/components/tabs';
|
|
import ReactIcon from '@workspace/ui/components/icons/react-icon';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@workspace/ui/components/ui/select';
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipProvider,
|
|
TooltipTrigger,
|
|
} from '@/registry/components/tooltip';
|
|
import { Button } from '@workspace/ui/components/ui/button';
|
|
import { RotateCcw } from '@/registry/icons/rotate-ccw';
|
|
|
|
const staticAnimationsLength = Object.keys(staticAnimations).length;
|
|
|
|
export const Icons = () => {
|
|
const [search, setSearch] = useState('');
|
|
const [animationKey, setAnimationKey] = useState(0);
|
|
const [activeIcon, setActiveIcon] = useState<string | null>(null);
|
|
const [activeTab, setActiveTab] = useState<string>('cli');
|
|
const [isCopied, setIsCopied] = useState(false);
|
|
const [activeAnimation, setActiveAnimation] = useState<string>('default');
|
|
|
|
const icons = Object.values(index).filter((icon) =>
|
|
icon.name.endsWith('-icon'),
|
|
);
|
|
|
|
const fuse = useMemo(() => {
|
|
return new Fuse(icons, {
|
|
keys: ['name', 'keywords'],
|
|
threshold: 0.3,
|
|
ignoreLocation: true,
|
|
});
|
|
}, [icons]);
|
|
|
|
const filteredIcons = useMemo(() => {
|
|
const q = search.trim();
|
|
if (!q) return icons;
|
|
return fuse.search(q).map((result) => result.item);
|
|
}, [search, fuse, icons]);
|
|
|
|
const icon = useMemo(
|
|
() => icons.find((icon) => icon.name === activeIcon),
|
|
[activeIcon, icons],
|
|
);
|
|
const iconName = useMemo(
|
|
() =>
|
|
icon?.name
|
|
.replace('-icon', '')
|
|
.split('-')
|
|
.map((word: string) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
.join(''),
|
|
[icon],
|
|
);
|
|
|
|
useEffect(() => {
|
|
setActiveAnimation('default');
|
|
}, [activeIcon]);
|
|
|
|
return (
|
|
<div className="-mt-4.5 text-black dark:text-white">
|
|
<p className="text-sm text-muted-foreground">
|
|
{filteredIcons.length} icons {search.length ? 'found' : 'available'}
|
|
</p>
|
|
|
|
<Input
|
|
placeholder="Search icons"
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
/>
|
|
|
|
<div>
|
|
{filteredIcons.length ? (
|
|
<div className="grid lg:grid-cols-11 sm:grid-cols-9 xs:grid-cols-7 grid-cols-5 gap-4 mt-6">
|
|
<TooltipProvider>
|
|
<MotionHighlight
|
|
hover
|
|
className="ring-2 ring-blue-500 bg-transparent rounded-lg -z-1"
|
|
>
|
|
{filteredIcons.map((icon) => {
|
|
const animationsLength = Object.keys(
|
|
icon?.component?.animations ?? {},
|
|
).length;
|
|
return (
|
|
<Tooltip side="bottom" key={icon.name}>
|
|
<TooltipTrigger>
|
|
<div>
|
|
<AnimateIcon animateOnHover>
|
|
<button
|
|
data-value={icon.name}
|
|
onClick={() => setActiveIcon(icon.name)}
|
|
className="relative group flex items-center justify-center size-full aspect-square rounded-lg p-3.5"
|
|
>
|
|
{icon?.component && (
|
|
<icon.component className="text-current size-full" />
|
|
)}
|
|
<div
|
|
className={cn(
|
|
'absolute inset-0 bg-muted rounded-lg -z-2 transition-colors duration-200',
|
|
activeIcon === icon.name && 'bg-blue-500/20',
|
|
)}
|
|
/>
|
|
<div className="absolute -bottom-2.5 -right-2.5 flex items-center justify-center text-muted-foreground font-medium size-5 bg-background border group-hover:border-blue-500 group-hover:ring group-hover:ring-blue-500 transition-colors duration-200 rounded-full">
|
|
<span className="text-[11px] leading-none">
|
|
{staticAnimationsLength + animationsLength}
|
|
</span>
|
|
</div>
|
|
</button>
|
|
</AnimateIcon>
|
|
</div>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p>{icon.name.replace('-icon', '')}</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
);
|
|
})}
|
|
</MotionHighlight>
|
|
</TooltipProvider>
|
|
</div>
|
|
) : (
|
|
<div className="flex items-center justify-center h-[200px]">
|
|
<p className="text-sm text-muted-foreground">No icons found</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<motion.div
|
|
className="fixed z-50 w-[325px] right-0 inset-y-12 rounded-l-2xl border-l border-y bg-background shadow-sm p-4"
|
|
initial={{ opacity: 0, x: '100%' }}
|
|
animate={activeIcon ? { opacity: 1, x: 0 } : { opacity: 0, x: '100%' }}
|
|
exit={{ opacity: 0, x: '100%' }}
|
|
transition={{ type: 'spring', stiffness: 150, damping: 25 }}
|
|
>
|
|
<h2 className="text-lg font-medium mt-1.5">
|
|
{activeIcon?.replace('-icon', '')}
|
|
</h2>
|
|
<AnimateIcon animateOnHover>
|
|
<button
|
|
onClick={() => setActiveIcon(null)}
|
|
className="absolute cursor-pointer top-5 right-5 size-8 rounded-full flex items-center justify-center bg-background hover:bg-muted transition-colors duration-200"
|
|
>
|
|
<X className="size-5 text-neutral-500" />
|
|
</button>
|
|
</AnimateIcon>
|
|
|
|
<div className="h-[calc(100%-3.25rem)] overflow-y-auto">
|
|
<div className="h-full flex flex-col justify-between gap-y-4">
|
|
<div>
|
|
<Tabs
|
|
value={activeTab}
|
|
onValueChange={(value) => setActiveTab(value)}
|
|
className="gap-0"
|
|
>
|
|
<div className="w-full flex justify-between items-center mb-3">
|
|
<h3 className="text-base font-medium pt-0 pb-0 mt-0 mb-0">
|
|
Installation
|
|
</h3>
|
|
<TabsList>
|
|
<TabsTrigger value="cli" className="w-[70px]">
|
|
CLI
|
|
</TabsTrigger>
|
|
<TabsTrigger value="manual" className="w-[70px]">
|
|
Manual
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
</div>
|
|
|
|
<TabsContents>
|
|
<TabsContent value="cli">
|
|
<CodeTabs
|
|
codes={{
|
|
npm: `npx shadcn@latest add "https://animate-ui.com/r/${activeIcon}.json"`,
|
|
pnpm: `pnpm dlx shadcn@latest add "https://animate-ui.com/r/${activeIcon}.json"`,
|
|
yarn: `npx shadcn@latest add "https://animate-ui.com/r/${activeIcon}.json"`,
|
|
bun: `bun x --bun shadcn@latest add "https://animate-ui.com/r/${activeIcon}.json"`,
|
|
}}
|
|
/>
|
|
</TabsContent>
|
|
<TabsContent value="manual" className="relative group">
|
|
{activeIcon && (
|
|
<DynamicCodeBlock
|
|
code={icon?.files?.[0]?.content}
|
|
lang="jsx"
|
|
title={`${icon?.name}.tsx`}
|
|
icon={<ReactIcon />}
|
|
className="max-h-[92px]"
|
|
/>
|
|
)}
|
|
|
|
<div
|
|
role="button"
|
|
onClick={() => {
|
|
navigator.clipboard.writeText(
|
|
icon?.files?.[0]?.content ?? '',
|
|
);
|
|
setIsCopied(true);
|
|
setTimeout(() => {
|
|
setIsCopied(false);
|
|
}, 2000);
|
|
}}
|
|
className="absolute cursor-pointer inset-px top-[41px] rounded-b-[13px] bg-black/20 invisible group-hover:visible opacity-0 group-hover:opacity-100 transition-all duration-200 flex items-center justify-center"
|
|
>
|
|
<p className="text-sm font-medium text-white">
|
|
{isCopied ? 'Copied' : 'Copy'}
|
|
</p>
|
|
</div>
|
|
</TabsContent>
|
|
</TabsContents>
|
|
</Tabs>
|
|
|
|
<h3 className="text-base font-medium mt-4">Usage</h3>
|
|
{activeIcon && (
|
|
<DynamicCodeBlock
|
|
code={`<${iconName} animateOnHover />
|
|
// Or use with the AnimateIcon component
|
|
<AnimateIcon animateOnHover>
|
|
<${iconName} />
|
|
</AnimateIcon>`}
|
|
lang="jsx"
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
{activeIcon && (
|
|
<>
|
|
<div className="relative h-[200px] w-full mx-auto rounded-2xl aspect-square bg-muted/50 border flex items-center justify-center">
|
|
{icon?.component && (
|
|
<icon.component
|
|
key={`${activeAnimation}-${activeIcon}-${animationKey}`}
|
|
animate
|
|
animation={activeAnimation}
|
|
className="text-current size-[100px]"
|
|
/>
|
|
)}
|
|
|
|
<AnimateIcon animateOnHover>
|
|
<Button
|
|
size="icon-sm"
|
|
variant="ghost"
|
|
className="absolute right-2 top-2 z-[2] backdrop-blur-md bg-transparent hover:bg-black/5 dark:hover:bg-white/10 size-6"
|
|
onClick={() => setAnimationKey((prev) => prev + 1)}
|
|
>
|
|
<RotateCcw className="size-3.5" />
|
|
</Button>
|
|
</AnimateIcon>
|
|
</div>
|
|
|
|
<Select
|
|
value={activeAnimation}
|
|
onValueChange={(value) => setActiveAnimation(value)}
|
|
>
|
|
<SelectTrigger className="w-full !h-11 px-1.5 rounded-lg">
|
|
<SelectValue placeholder="Select an animation" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<div className="space-y-1.5 p-0.5">
|
|
{Object.keys({
|
|
...staticAnimations,
|
|
...(icon?.component?.animations ?? {}),
|
|
}).map((animation) => (
|
|
<SelectItem
|
|
key={animation}
|
|
value={animation}
|
|
className="!h-8 rounded-md px-0 focus:bg-muted"
|
|
>
|
|
<div className="gap-2 flex items-center">
|
|
<div className="size-8 rounded-md p-1.5 bg-muted">
|
|
{icon?.component && (
|
|
<icon.component className="text-current size-full" />
|
|
)}
|
|
</div>
|
|
<span>{animation}</span>
|
|
</div>
|
|
</SelectItem>
|
|
))}
|
|
</div>
|
|
</SelectContent>
|
|
</Select>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
</div>
|
|
);
|
|
};
|