summaryrefslogtreecommitdiff
path: root/src/components/icons
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/icons')
-rw-r--r--src/components/icons/animated/check.tsx111
-rw-r--r--src/components/icons/animated/upload.tsx102
-rw-r--r--src/components/icons/icons.tsx131
3 files changed, 344 insertions, 0 deletions
diff --git a/src/components/icons/animated/check.tsx b/src/components/icons/animated/check.tsx
new file mode 100644
index 0000000..24a246c
--- /dev/null
+++ b/src/components/icons/animated/check.tsx
@@ -0,0 +1,111 @@
+'use client';
+
+import { cn } from '@/lib/utils';
+import type { Variants } from 'motion/react';
+import { motion, useAnimation } from 'motion/react';
+import type { HTMLAttributes } from 'react';
+import { forwardRef, useCallback, useImperativeHandle, useRef } from 'react';
+
+export interface CheckIconHandle {
+ startAnimation: () => void;
+ stopAnimation: () => void;
+}
+
+interface CheckIconProps extends HTMLAttributes<HTMLDivElement> {
+ size?: number;
+}
+
+const pathVariants: Variants = {
+ normal: {
+ opacity: 1,
+ pathLength: 1,
+ scale: 1,
+ transition: {
+ duration: 0.3,
+ opacity: { duration: 0.1 },
+ },
+ },
+ animate: {
+ opacity: [0, 1],
+ pathLength: [0, 1],
+ scale: [0.5, 1],
+ transition: {
+ duration: 0.4,
+ opacity: { duration: 0.1 },
+ },
+ },
+};
+
+const CheckIcon = forwardRef<CheckIconHandle, CheckIconProps>(
+ ({ onMouseEnter, onMouseLeave, className, size = 28, ...props }, ref) => {
+ const controls = useAnimation();
+ const isControlledRef = useRef(false);
+
+ useImperativeHandle(ref, () => {
+ isControlledRef.current = true;
+
+ return {
+ startAnimation: () => controls.start('animate'),
+ stopAnimation: () => controls.start('normal'),
+ };
+ });
+
+ const handleMouseEnter = useCallback(
+ (e: React.MouseEvent<HTMLDivElement>) => {
+ if (!isControlledRef.current) {
+ controls.start('animate');
+ } else {
+ onMouseEnter?.(e);
+ }
+ },
+ [controls, onMouseEnter],
+ );
+
+ const handleMouseLeave = useCallback(
+ (e: React.MouseEvent<HTMLDivElement>) => {
+ if (!isControlledRef.current) {
+ controls.start('normal');
+ } else {
+ onMouseLeave?.(e);
+ }
+ },
+ [controls, onMouseLeave],
+ );
+
+ return (
+ <div
+ className={cn(
+ 'flex cursor-pointer select-none items-center justify-center rounded-md p-2 transition-colors duration-200 hover:bg-accent',
+ className,
+ )}
+ onMouseEnter={handleMouseEnter}
+ onMouseLeave={handleMouseLeave}
+ {...props}
+ >
+ {/* biome-ignore lint/a11y/noSvgWithoutTitle: <explanation> */}
+ <svg
+ xmlns='http://www.w3.org/2000/svg'
+ width={size}
+ height={size}
+ viewBox='0 0 24 24'
+ fill='none'
+ stroke='currentColor'
+ strokeWidth='2'
+ strokeLinecap='round'
+ strokeLinejoin='round'
+ >
+ <motion.path
+ variants={pathVariants}
+ initial='normal'
+ animate={controls}
+ d='M4 12 9 17L20 6'
+ />
+ </svg>
+ </div>
+ );
+ },
+);
+
+CheckIcon.displayName = 'CheckIcon';
+
+export { CheckIcon };
diff --git a/src/components/icons/animated/upload.tsx b/src/components/icons/animated/upload.tsx
new file mode 100644
index 0000000..19ac51e
--- /dev/null
+++ b/src/components/icons/animated/upload.tsx
@@ -0,0 +1,102 @@
+'use client';
+
+import { cn } from '@/lib/utils';
+import type { Variants } from 'motion/react';
+import { motion, useAnimation } from 'motion/react';
+import type { HTMLAttributes } from 'react';
+import { forwardRef, useCallback, useImperativeHandle, useRef } from 'react';
+
+export interface UploadIconHandle {
+ startAnimation: () => void;
+ stopAnimation: () => void;
+}
+
+interface UploadIconProps extends HTMLAttributes<HTMLDivElement> {
+ size?: number;
+}
+
+const arrowVariants: Variants = {
+ normal: { y: 0 },
+ animate: {
+ y: -2,
+ transition: {
+ type: 'spring',
+ stiffness: 200,
+ damping: 10,
+ mass: 1,
+ },
+ },
+};
+
+const UploadIcon = forwardRef<UploadIconHandle, UploadIconProps>(
+ ({ onMouseEnter, onMouseLeave, className, size = 28, ...props }, ref) => {
+ const controls = useAnimation();
+ const isControlledRef = useRef(false);
+
+ useImperativeHandle(ref, () => {
+ isControlledRef.current = true;
+
+ return {
+ startAnimation: () => controls.start('animate'),
+ stopAnimation: () => controls.start('normal'),
+ };
+ });
+
+ const handleMouseEnter = useCallback(
+ (e: React.MouseEvent<HTMLDivElement>) => {
+ if (!isControlledRef.current) {
+ controls.start('animate');
+ } else {
+ onMouseEnter?.(e);
+ }
+ },
+ [controls, onMouseEnter],
+ );
+
+ const handleMouseLeave = useCallback(
+ (e: React.MouseEvent<HTMLDivElement>) => {
+ if (!isControlledRef.current) {
+ controls.start('normal');
+ } else {
+ onMouseLeave?.(e);
+ }
+ },
+ [controls, onMouseLeave],
+ );
+
+ return (
+ <div
+ className={cn(
+ 'flex cursor-pointer select-none items-center justify-center rounded-md p-2 transition-colors duration-200 hover:bg-accent',
+ className,
+ )}
+ onMouseEnter={handleMouseEnter}
+ onMouseLeave={handleMouseLeave}
+ {...props}
+ >
+ {/* biome-ignore lint/a11y/noSvgWithoutTitle: <explanation> */}
+ <svg
+ xmlns='http://www.w3.org/2000/svg'
+ width={size}
+ height={size}
+ viewBox='0 0 24 24'
+ fill='none'
+ stroke='currentColor'
+ strokeWidth='2'
+ strokeLinecap='round'
+ strokeLinejoin='round'
+ >
+ <path d='M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4' />
+ <motion.g variants={arrowVariants} animate={controls}>
+ <polyline points='17 8 12 3 7 8' />
+ <line x1='12' x2='12' y1='3' y2='15' />
+ </motion.g>
+ </svg>
+ </div>
+ );
+ },
+);
+
+UploadIcon.displayName = 'UploadIcon';
+
+export { UploadIcon };
diff --git a/src/components/icons/icons.tsx b/src/components/icons/icons.tsx
new file mode 100644
index 0000000..e861252
--- /dev/null
+++ b/src/components/icons/icons.tsx
@@ -0,0 +1,131 @@
+import type { Icon as LucideIcon, LucideProps } from 'lucide-react';
+import {
+ AlertTriangle,
+ ArrowRight,
+ ArrowUpRight,
+ Check,
+ CheckCircle,
+ ChevronDown,
+ ChevronLeft,
+ ChevronRight,
+ ClipboardCheck,
+ Code,
+ CreditCard,
+ File,
+ FileText,
+ HelpCircle,
+ Home,
+ Image,
+ Info,
+ Laptop,
+ Loader2,
+ LogIn,
+ LogOut,
+ Mail,
+ Menu,
+ Moon,
+ MoreVertical,
+ Newspaper,
+ Pizza,
+ Plus,
+ Rss,
+ SendHorizonal,
+ Settings,
+ ShareIcon,
+ SunMedium,
+ Tag,
+ Tags,
+ Trash,
+ User,
+ X,
+} from 'lucide-react';
+
+export type Icon = typeof LucideIcon;
+
+export const Icons = {
+ logo: Code,
+ close: X,
+ menu: Menu,
+ code: Code,
+ copied: ClipboardCheck,
+ success: CheckCircle,
+ spinner: Loader2,
+ chevronLeft: ChevronLeft,
+ chevronRight: ChevronRight,
+ trash: Trash,
+ tags: Tags,
+ tag: Tag,
+ share: ShareIcon,
+ posts: Newspaper,
+ post: FileText,
+ page: File,
+ media: Image,
+ settings: Settings,
+ billing: CreditCard,
+ ellipsis: MoreVertical,
+ add: Plus,
+ logIn: LogIn,
+ logOut: LogOut,
+ warning: AlertTriangle,
+ user: User,
+ arrowRight: ArrowRight,
+ help: HelpCircle,
+ pizza: Pizza,
+ sun: SunMedium,
+ moon: Moon,
+ laptop: Laptop,
+ home: Home,
+ info: Info,
+ arrowUpRight: ArrowUpRight,
+ chevronDown: ChevronDown,
+ mail: Mail,
+ send: SendHorizonal,
+ gitHub: ({ ...props }: LucideProps) => (
+ <svg
+ aria-hidden='true'
+ focusable='false'
+ data-prefix='fab'
+ data-icon='github'
+ role='img'
+ xmlns='http://www.w3.org/2000/svg'
+ viewBox='0 0 496 512'
+ {...props}
+ >
+ <path
+ fill='currentColor'
+ d='M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3 .3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5 .3-6.2 2.3zm44.2-1.7c-2.9 .7-4.9 2.6-4.6 4.9 .3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3 .7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3 .3 2.9 2.3 3.9 1.6 1 3.6 .7 4.3-.7 .7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3 .7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3 .7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z'
+ />
+ </svg>
+ ),
+ google: ({ ...props }: LucideProps) => (
+ <svg
+ aria-hidden='true'
+ focusable='false'
+ width='1em'
+ height='1em'
+ viewBox='0 0 256 262'
+ xmlns='http://www.w3.org/2000/svg'
+ preserveAspectRatio='xMidYMid'
+ {...props}
+ >
+ <path
+ d='M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622 38.755 30.023 2.685.268c24.659-22.774 38.875-56.282 38.875-96.027'
+ fill='#4285F4'
+ />
+ <path
+ d='M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055-34.523 0-63.824-22.773-74.269-54.25l-1.531.13-40.298 31.187-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1'
+ fill='#34A853'
+ />
+ <path
+ d='M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82 0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602l42.356-32.782'
+ fill='#FBBC05'
+ />
+ <path
+ d='M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0 79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251'
+ fill='#EB4335'
+ />
+ </svg>
+ ),
+ check: Check,
+ rss: Rss,
+};