diff options
Diffstat (limited to 'src/components/icons')
| -rw-r--r-- | src/components/icons/animated/check.tsx | 111 | ||||
| -rw-r--r-- | src/components/icons/animated/upload.tsx | 102 | ||||
| -rw-r--r-- | src/components/icons/icons.tsx | 131 |
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, +}; |
