├── .gitignore ├── .husky └── pre-commit ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── app ├── globals.css ├── icon.svg ├── layout.tsx └── page.tsx ├── components ├── sections │ ├── footer.tsx │ ├── header.tsx │ ├── showcase.tsx │ └── sticky-header.tsx └── ui │ ├── component-container.tsx │ ├── family-style-otp.tsx │ ├── image-preview.tsx │ ├── index.ts │ └── multi-step.tsx ├── eslint.config.mjs ├── hooks ├── use-measure.tsx ├── use-mounted.ts └── use-sticky.ts ├── lib └── utils.ts ├── next.config.ts ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public ├── dark-icon.svg ├── gyoza-shop.jpg ├── light-icon.svg └── profile.png └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm run lint-staged -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | .next 3 | node_modules -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "useTabs": false, 4 | "tabWidth": 2, 5 | "semi": true, 6 | "singleQuote": true, 7 | "plugins": ["prettier-plugin-sort-imports", "prettier-plugin-tailwindcss"] 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Smintfy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | Snippet Logo 5 | 6 |

7 | 8 | # Snippet 9 | 10 | Collections of ui components built by smintfy. Feel free to play with them. 11 | 12 | ## Tech Stack 13 | 14 | Snippet is primarily built with Next.js, React, TypeScript, TailwindCSS, and Motion. 15 | 16 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 17 | 18 | ## Motivation 19 | 20 | Snippet is built as a playground for learning and experiment. By open sourcing the code, I hope other people can learn from it as well. 21 | 22 | ### Inspiration 23 | 24 | These are some of the people that has been inspiring me to improve my design engineering skills. 25 | 26 | - [Emil Kowalski](https://x.com/emilkowalski_) 27 | - [Mariana Castilho](https://x.com/mrncst) 28 | - [Jakub Krehel](https://x.com/jakubkrehel) 29 | - More soon... 30 | 31 | And here are some apps that inspire me. 32 | 33 | - [Family](https://family.co/) 34 | - More soon... 35 | 36 | ## Contributing 37 | 38 | Contributions are welcomed, If you find a bug or have a feature request, feel free to create an [issue](https://github.com/Smintfy/ui-snippet/issues) or submit a [PR](https://github.com/Smintfy/ui-snippet/pulls). 39 | 40 | ### Getting started with development 41 | 42 | You can fork and clone the repository to your local machine if you want to contribute or you can just clone the repository if you want to play around. 43 | 44 | ```bash 45 | # clone your forked repo 46 | git clone https://github.com/your-username/ui-snippet.git 47 | 48 | # or just clone it without fork 49 | git clone https://github.com/smintfy/ui-snippet.git 50 | ``` 51 | 52 | Navigate to the project directory 53 | 54 | ```bash 55 | cd ui-snippet 56 | ``` 57 | 58 | Install dependencies 59 | 60 | ```bash 61 | npm install 62 | ``` 63 | 64 | Run the development server 65 | 66 | ```bash 67 | npm run dev 68 | ``` 69 | 70 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | 3 | /* 4 | The default border color has changed to `currentColor` in Tailwind CSS v4, 5 | so we've added these compatibility styles to make sure everything still 6 | looks the same as it did with Tailwind CSS v3. 7 | 8 | If we ever want to remove these styles, we need to add an explicit border 9 | color utility to any element that depends on these defaults. 10 | */ 11 | @layer base { 12 | *, 13 | ::after, 14 | ::before, 15 | ::backdrop, 16 | ::file-selector-button { 17 | border-color: var(--color-border, currentColor); 18 | } 19 | } 20 | 21 | @theme inline { 22 | --color-primary: var(--primary); 23 | --color-secondary: var(--secondary); 24 | --color-tertiary: var(--tertiary); 25 | --color-border: var(--border); 26 | --color-accent: var(--accent); 27 | } 28 | 29 | :root { 30 | --background: oklch(1 0 0); 31 | --primary: oklch(0.209 0 0); 32 | --secondary: oklch(0.442 0 0); 33 | --tertiary: oklch(0.556 0 0); 34 | --border: oklch(0.922 0 0); 35 | --accent: oklch(0.976 0 0); 36 | } 37 | 38 | body { 39 | background: var(--background); 40 | color: var(--primary); 41 | font-family: Arial, Helvetica, sans-serif; 42 | } 43 | 44 | @keyframes fastPulse { 45 | 0%, 100% { opacity: 1 } 46 | 50% { opacity: 0.4 } 47 | } 48 | 49 | .fast-pulse { 50 | animation: fastPulse 0.6s ease-in-out infinite; 51 | } -------------------------------------------------------------------------------- /app/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 9 | 13 | 17 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Inter } from 'next/font/google'; 2 | import type { Metadata } from 'next'; 3 | import { cn } from '@/lib/utils'; 4 | import './globals.css'; 5 | 6 | const inter = Inter({ 7 | variable: '--font-inter', 8 | subsets: ['latin'], 9 | }); 10 | 11 | export const metadata: Metadata = { 12 | title: 'uisnippet', 13 | description: 'Collections of ui components made by smintfy.', 14 | }; 15 | 16 | export default function RootLayout({ 17 | children, 18 | }: Readonly<{ 19 | children: React.ReactNode; 20 | }>) { 21 | return ( 22 | 23 | {children} 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Showcase from '@/components/sections/showcase'; 4 | import Header from '@/components/sections/header'; 5 | import Footer from '@/components/sections/footer'; 6 | import { cn } from '@/lib/utils'; 7 | import { Toaster } from 'sonner'; 8 | 9 | export default function Home() { 10 | return ( 11 |
16 | 17 |
18 | 19 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /components/sections/footer.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | 3 | export default function Footer() { 4 | return ( 5 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /components/sections/header.tsx: -------------------------------------------------------------------------------- 1 | import StickyHeader from './sticky-header'; 2 | 3 | export default function Header() { 4 | return ( 5 | <> 6 | 7 |

8 | Collections of ui components built by smintfy. 9 |

10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /components/sections/showcase.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentContainer, FamilyStyleOTP, ImagePreview, MultiStep } from '@/components/ui'; 2 | 3 | interface ComponentContainerProps { 4 | name: string; 5 | description: React.JSX.Element; 6 | tags: string[]; 7 | source: string; 8 | children: React.ReactNode; 9 | } 10 | 11 | const components: ComponentContainerProps[] = [ 12 | { 13 | name: 'Image preview', 14 | description: <>Interaction built using shared layout animations and Radix dialog primitive., 15 | tags: ['react', 'radix', 'motion', 'tailwind'], 16 | source: 'https://github.com/Smintfy/ui-snippet/blob/main/components/ui/image-preview.tsx', 17 | children: , 18 | }, 19 | { 20 | name: 'Multi-step flow', 21 | description: ( 22 | <>This is a multi-step component guiding users through a process or for an onboarding. 23 | ), 24 | tags: ['react', 'motion', 'tailwind'], 25 | source: 'https://github.com/Smintfy/ui-snippet/blob/main/components/ui/multi-step.tsx', 26 | children: , 27 | }, 28 | { 29 | name: 'Family style OTP', 30 | description: ( 31 | <> 32 | Recreating OTP component from{' '} 33 | 38 | Family 39 | {' '} 40 | built on top of{' '} 41 | 42 | otp-input 43 | {' '} 44 | by guilhermerodz. And by the way, the correct code is {'"'}123456{'"'}. 45 | 46 | ), 47 | tags: ['react', 'motion', 'tailwind'], 48 | source: 'https://github.com/Smintfy/ui-snippet/blob/main/components/ui/family-style-otp.tsx', 49 | children: , 50 | }, 51 | ]; 52 | 53 | export default function Showcase() { 54 | return ( 55 |
56 | {components.map((component) => ( 57 | 64 | {component.children} 65 | 66 | ))} 67 |
68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /components/sections/sticky-header.tsx: -------------------------------------------------------------------------------- 1 | // Credit to https://github.com/marcbouchenoire for the sticky header implementation 2 | 3 | 'use client'; 4 | 5 | import { useIsMounted } from '@/hooks/use-mounted'; 6 | import { useIsSticky } from '@/hooks/use-sticky'; 7 | import { cn } from '@/lib/utils'; 8 | import { useRef } from 'react'; 9 | 10 | export default function StickyHeader() { 11 | const stickyRef = useRef(null!); 12 | const isSticky = useIsSticky(stickyRef); 13 | const isMounted = useIsMounted(); 14 | 15 | return ( 16 | <> 17 |
23 |
29 |
30 |
31 |
32 | 72 | 73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /components/ui/component-container.tsx: -------------------------------------------------------------------------------- 1 | interface ComponentContainerProps { 2 | name: string; 3 | description: React.JSX.Element; 4 | tags: string[]; 5 | source: string; 6 | children: React.ReactNode; 7 | } 8 | 9 | export default function ComponentContainer({ 10 | name, 11 | description, 12 | tags, 13 | source, 14 | children, 15 | }: ComponentContainerProps) { 16 | return ( 17 |
18 |
19 |

{name}

20 |

21 | {description}{' '} 22 | 23 | View source 24 | 25 |

26 |
27 |
28 | {children} 29 |
30 |
31 | {tags.map((tag) => ( 32 | 36 | {tag} 37 | 38 | ))} 39 |
40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /components/ui/family-style-otp.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { motion, AnimatePresence } from 'motion/react'; 4 | import { useEffect, useRef, useState } from 'react'; 5 | import { OTPInput, SlotProps } from 'input-otp'; 6 | import { cn } from '@/lib/utils'; 7 | import { toast } from 'sonner'; 8 | 9 | interface AnimatedNumberProps { 10 | value: string | null; 11 | placeholder: string; 12 | } 13 | 14 | function Separator() { 15 | return
; 16 | } 17 | 18 | function AnimatedNumber({ value, placeholder }: AnimatedNumberProps) { 19 | return ( 20 |
21 | 22 | 30 | {value ?? placeholder} 31 | 32 | 33 |
34 | ); 35 | } 36 | 37 | function Slot(props: SlotProps & { isShaking?: boolean; isVerifying: boolean; delay: number; }) { 38 | const placeholderChar = '0'; 39 | 40 | return ( 41 | 51 | 52 | {props.isActive ? ( 53 | 62 | ) : null} 63 | 64 | ); 65 | } 66 | 67 | export default function FamilyStyleOTP() { 68 | const CORRECT_OTP = '123456'; 69 | const [value, setValue] = useState(''); 70 | const [disableSubmitButton, setDisableSubmitButton] = useState(true); 71 | const [isVerifying, setIsVerifying] = useState(false); 72 | const [isShaking, setIsShaking] = useState(false); 73 | const [errorMessage, setErrorMessage] = useState(''); 74 | 75 | const otpRef = useRef(null); 76 | 77 | useEffect(() => { 78 | setDisableSubmitButton(value.length !== 6); 79 | }, [value]); 80 | 81 | const handleSubmit = () => { 82 | // Fix for an issue where user can briefly click submit again after submitting, 83 | // prevent submission if one is already in progress. 84 | if (isVerifying) return; 85 | 86 | setIsVerifying(true); 87 | setDisableSubmitButton(true); 88 | setErrorMessage(''); 89 | 90 | setTimeout(() => { 91 | if (value === CORRECT_OTP) { 92 | toast.message('Successfully verified', { 93 | description: 'Your OTP has been verified.', 94 | }); 95 | } else { 96 | setIsShaking(true); 97 | setErrorMessage('Invalid validation code'); 98 | } 99 | 100 | setValue(''); 101 | setIsVerifying(false); 102 | 103 | // Focus back to the first slot after submitting. 104 | if (otpRef.current) { 105 | otpRef.current.focus(); 106 | otpRef.current.setSelectionRange(0, 0); 107 | } 108 | }, 2000); 109 | }; 110 | 111 | return ( 112 |
113 |
114 |

Verification Code

115 |

We{"'"}ve sent you a verification code.

116 |
117 | setIsShaking(false)} 121 | > 122 | { 128 | // Error if value doesn't contain number 129 | if (!/^\d*$/.test(newValue)) { 130 | setIsShaking(true); 131 | return; 132 | } 133 | setValue(newValue); 134 | if (errorMessage) setErrorMessage(''); 135 | }} 136 | onKeyDown={(e) => { 137 | if (e.key === 'Enter') { 138 | e.preventDefault(); 139 | if (value.length < 6) return; 140 | handleSubmit(); 141 | } 142 | }} 143 | render={({ slots }) => ( 144 | <> 145 |
146 | {slots.slice(0, 3).map((slot, idx) => ( 147 | 154 | ))} 155 |
156 | 157 |
158 | {slots.slice(3).map((slot, idx) => ( 159 | 166 | ))} 167 |
168 | 169 | )} 170 | /> 171 |
172 | 173 | Didn{"'"}t receive a code?{' '} 174 | 184 | 185 |
186 | 187 | {errorMessage && ( 188 | 196 | {errorMessage} 197 | 198 | )} 199 | 200 |
201 | 252 |
253 | ); 254 | } 255 | -------------------------------------------------------------------------------- /components/ui/image-preview.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { motion, AnimatePresence, MotionConfig } from 'framer-motion'; 4 | import { VisuallyHidden } from '@radix-ui/react-visually-hidden'; 5 | import { Dialog } from 'radix-ui'; 6 | import { X } from 'lucide-react'; 7 | import { useState } from 'react'; 8 | import Image from 'next/image'; 9 | 10 | const MotionImage = motion.create(Image); 11 | 12 | export default function ImagePreview() { 13 | const [isOpen, setIsOpen] = useState(false); 14 | 15 | return ( 16 |
17 | 20 | 21 | 22 | 27 | 36 | 37 | 38 | 39 | 40 | {isOpen && ( 41 | <> 42 | 43 | 49 | 50 |
51 | 52 | 53 | Image Preview 54 | 55 | Interaction built using shared layout animations and Radix dialog 56 | primitive. 57 | 58 | 59 | 63 | 71 | 72 | 80 | 81 | 82 | 83 |
84 | 85 | )} 86 |
87 |
88 |
89 |
90 |
91 | ); 92 | } 93 | -------------------------------------------------------------------------------- /components/ui/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ComponentContainer } from './component-container'; 2 | export { default as FamilyStyleOTP } from './family-style-otp'; 3 | export { default as ImagePreview } from './image-preview'; 4 | export { default as MultiStep } from './multi-step'; 5 | -------------------------------------------------------------------------------- /components/ui/multi-step.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { motion, MotionConfig, AnimatePresence } from 'motion/react'; 4 | import { useMemo, useRef, useState } from 'react'; 5 | import useMeasure from '@/hooks/use-measure'; 6 | import { cn } from '@/lib/utils'; 7 | import { toast } from 'sonner'; 8 | 9 | const PrimaryButton: React.FC> = ({ 10 | children, 11 | className, 12 | ...props 13 | }) => { 14 | return ( 15 | 26 | ); 27 | }; 28 | 29 | const SecondaryButton: React.FC> = ({ 30 | children, 31 | className, 32 | ...props 33 | }) => { 34 | return ( 35 | 45 | ); 46 | }; 47 | 48 | const StepOne = () => { 49 | return ( 50 | <> 51 |
52 |

This is step one

53 |

54 | This is a multi-step component. It{"'"}s a great way to guide users through a process. 55 |

56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | 64 | ); 65 | }; 66 | 67 | const StepTwo = () => { 68 | return ( 69 | <> 70 |
71 |

This is step two

72 |

73 | This is a multi-step component. It{"'"}s a great way to guide users through a process. 74 |

75 |
76 |
77 |
78 |
79 |
80 |
81 | 82 | ); 83 | }; 84 | 85 | const StepThree = () => { 86 | return ( 87 | <> 88 |
89 |

This is step three

90 |

91 | This is a multi-step component. It{"'"}s a great way to guide users through a process. 92 |

93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 | 102 | ); 103 | }; 104 | 105 | export default function MultiStep() { 106 | const [currentStep, setCurrentStep] = useState(0); 107 | const [clickDirection, setClickDirection] = useState(1); 108 | const ref = useRef(null); 109 | const { height } = useMeasure({ ref }); 110 | 111 | const steps = useMemo(() => { 112 | switch (currentStep) { 113 | case 0: 114 | return ; 115 | case 1: 116 | return ; 117 | case 2: 118 | return ; 119 | } 120 | }, [currentStep]); 121 | 122 | const variants = { 123 | initial: (custom: number) => ({ 124 | x: `${100 * custom}%`, 125 | filter: 'blur(2.5px)', 126 | opacity: 0, 127 | }), 128 | animate: () => ({ 129 | x: 0, 130 | filter: 'blur(0)', 131 | opacity: 1, 132 | }), 133 | exit: (custom: number) => ({ 134 | x: `${-100 * custom}%`, 135 | filter: 'blur(2.5px)', 136 | opacity: 0, 137 | }), 138 | }; 139 | 140 | return ( 141 |
142 | 143 | 150 |
151 |
152 | 156 | 157 | 166 | {steps} 167 | 168 | 169 | 170 | 171 | { 176 | if (currentStep === 0) { 177 | return; 178 | } 179 | setCurrentStep((prev) => prev - 1); 180 | setClickDirection(-1); 181 | }} 182 | > 183 | Back 184 | 185 | { 188 | if (currentStep === 2) { 189 | toast.message('Onboarding completed', { 190 | description: 'Now you know what to do.', 191 | }); 192 | setCurrentStep(0); 193 | return; 194 | } 195 | setCurrentStep((prev) => prev + 1); 196 | setClickDirection(1); 197 | }} 198 | > 199 | {currentStep === 2 ? 'Complete' : 'Continue'} 200 | 201 | 202 |
203 |
204 |
205 |
206 |
207 | ); 208 | } 209 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { FlatCompat } from '@eslint/eslintrc'; 2 | import { fileURLToPath } from 'url'; 3 | import { dirname } from 'path'; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [...compat.extends('next/core-web-vitals', 'next/typescript')]; 13 | 14 | export default eslintConfig; 15 | -------------------------------------------------------------------------------- /hooks/use-measure.tsx: -------------------------------------------------------------------------------- 1 | import { RefObject, useEffect, useState } from 'react'; 2 | 3 | const useMeasure = ({ ref }: { ref: RefObject }) => { 4 | const [height, setHeight] = useState(0); 5 | 6 | useEffect(() => { 7 | const observer = new ResizeObserver((entries) => { 8 | for (const entry of entries) { 9 | const rect = entry.target.getBoundingClientRect(); 10 | 11 | setHeight(rect.height); 12 | } 13 | }); 14 | 15 | if (ref.current) { 16 | observer.observe(ref.current); 17 | } 18 | 19 | return () => observer.disconnect(); 20 | }, [ref]); 21 | 22 | return { height }; 23 | }; 24 | 25 | export default useMeasure; 26 | -------------------------------------------------------------------------------- /hooks/use-mounted.ts: -------------------------------------------------------------------------------- 1 | import { useSyncExternalStore } from 'react'; 2 | 3 | const subscribe = () => () => {}; 4 | const getSnapshot = () => true; 5 | const getServerSnapshot = () => false; 6 | 7 | export function useIsMounted() { 8 | const isMounted = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); 9 | 10 | return isMounted; 11 | } 12 | -------------------------------------------------------------------------------- /hooks/use-sticky.ts: -------------------------------------------------------------------------------- 1 | import { type RefObject, useEffect, useState } from 'react'; 2 | 3 | export function useIsSticky(ref: RefObject) { 4 | const [isSticky, setIsSticky] = useState(false); 5 | 6 | useEffect(() => { 7 | const current = ref.current; 8 | if (!current) return; 9 | 10 | const observer = new IntersectionObserver( 11 | ([entry]) => setIsSticky((entry?.intersectionRatio ?? 1) < 1), 12 | { 13 | threshold: [1], 14 | }, 15 | ); 16 | 17 | observer.observe(current as T); 18 | 19 | return () => { 20 | observer.unobserve(current as T); 21 | }; 22 | }, []); // eslint-disable-line react-hooks/exhaustive-deps 23 | 24 | return isSticky; 25 | } 26 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { twMerge } from 'tailwind-merge'; 2 | import { ClassValue, clsx } from 'clsx'; 3 | 4 | export const cn = (...inputs: ClassValue[]) => { 5 | return twMerge(clsx(inputs)); 6 | }; 7 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from 'next'; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui-snippet", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "format": "prettier --write \"**/*.{ts,tsx}\"", 11 | "lint-staged": "prettier --write --ignore-unknown", 12 | "prepare": "npx husky install" 13 | }, 14 | "dependencies": { 15 | "@number-flow/react": "^0.5.8", 16 | "@radix-ui/react-visually-hidden": "^1.1.2", 17 | "clsx": "^2.1.1", 18 | "input-otp": "^1.4.2", 19 | "lucide-react": "^0.479.0", 20 | "motion": "^12.4.10", 21 | "next": "^15.2.4", 22 | "radix-ui": "^1.1.3", 23 | "react": "^19.0.0", 24 | "react-dom": "^19.0.0", 25 | "sonner": "^2.0.2", 26 | "tailwind-merge": "^3.0.2" 27 | }, 28 | "devDependencies": { 29 | "@eslint/eslintrc": "^3", 30 | "@tailwindcss/postcss": "^4", 31 | "@types/node": "^20", 32 | "@types/react": "^19", 33 | "@types/react-dom": "^19", 34 | "eslint": "^9", 35 | "eslint-config-next": "15.2.1", 36 | "lint-staged": "^15.5.0", 37 | "prettier": "^3.5.3", 38 | "prettier-plugin-sort-imports": "^1.8.6", 39 | "prettier-plugin-tailwindcss": "^0.6.11", 40 | "tailwindcss": "^4", 41 | "typescript": "^5" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ['@tailwindcss/postcss'], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /public/dark-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | 12 | -------------------------------------------------------------------------------- /public/gyoza-shop.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Smintfy/ui-snippet/eb6a867cebe3a7ae9fc490f4c49b1e6722d248ba/public/gyoza-shop.jpg -------------------------------------------------------------------------------- /public/light-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | 12 | -------------------------------------------------------------------------------- /public/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Smintfy/ui-snippet/eb6a867cebe3a7ae9fc490f4c49b1e6722d248ba/public/profile.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | --------------------------------------------------------------------------------