<SYSTEM>This document contains comprehensive information about Minh Trịnh's professional profile, portfolio, and blog content. It includes personal details, work experience, projects, achievements, certifications, and all published blog posts. This data is formatted for consumption by Large Language Models (LLMs) to provide accurate and up-to-date information about Minh Trịnh's background, skills, and expertise as a Design Engineer.</SYSTEM>

# chanhdai.com

> A minimal, pixel-perfect dev portfolio, component registry, and blog to showcase my work as a Design Engineer.

## About

- **Frontend Engineer** with **4+ years of experience**, known for pixel-perfect execution and strong attention to small details.
- Skilled in **Next.js**, **React**, **TypeScript**, and modern front-end technologies; building high-quality, user-centric web and mobile applications.
- Passionate about exploring new technologies and turning ideas into reality through polished, thoughtfully crafted personal projects.

### Personal Information

- First Name: Minh
- Last Name: Trịnh
- Display Name: Minh Trịnh
- Location: Ho Chi Minh City, Viet Nam
- Website: https://mitr.dev

### Social Links

- [X (formerly Twitter)](https://x.com/minhtrinh2611)
- [GitHub](https://github.com/trinhvanminh)
- [LinkedIn](https://www.linkedin.com/in/minhtrinh26)
- [Telegram](https://t.me/tvminh2611)

### Tech Stack

- [TypeScript](https://www.typescriptlang.org/)
- [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript)
- [Python](https://www.python.org/)
- [Node.js](https://nodejs.org/)
- [Vite](https://vite.dev/)
- [React](https://react.dev/)
- [Next.js](https://nextjs.org/)
- [Tailwind CSS](https://tailwindcss.com/)
- [shadcn/ui](https://ui.shadcn.com/)
- [Radix UI](https://www.radix-ui.com/)
- [Motion](https://motion.dev/)
- [TanStack](https://tanstack.com/)
- [Redux](https://redux.js.org/)
- [Ant Design](https://ant.design/)
- [React Router](https://reactrouter.com/)
- [Git](https://git-scm.com/)
- [Docker](https://www.docker.com/)
- [MySQL](https://www.mysql.com/)
- [MongoDB](https://www.mongodb.com/)
- [Redis](https://redis.io/)
- [Figma](https://www.figma.com/)
- [ChatGPT](https://chatgpt.com/)

## Experience

### Full-stack Developer | Freelance

Duration: 2025 - Present

Skills: React, Next.js, Google Maps API, Better Auth, Strapi, WordPress

Projects:
- Cruise Booking: developed landing page with CMS system using Strapi and Stripe for payments.
- University statistics system: designed dashboard and admin portal using Python Django.
- Senla Truck: truck management website with real-time delivery tracking.
- Company langing page: designed a customizable WordPress landing page.

### Middle Frontend Engineer | Logix Technology

Duration: 05.2025 - 03.2026

Skills: React.js, Next.js, Typescript, Zustand, Vite, TailwindCSS, Shadcn, Agile

- Lead frontend engineer for FMCG project.
- Architecting frontend structure using Next.js, Shadcn, TailwindCSS, and Turborepo.
- Leading frontend architecture and component standardization.

### Junior Frontend Engineer | Logix Technology

Duration: 09.2022 - 05.2025

Skills: React.js, React Native, Next.js, Typescript, Python, Redux Toolkit, Vite, Agile

- Maintained and improved multiple production systems including Car Bidding App and FMCG project.
- Upgraded MUI version, standardized components, and migrated project from JS to TS.
- Researched and built AI-powered Retailer Assistant using LangChain, Python, and vector database.
- Migrated UI from MUI to veeamUI for [Veeam](https://www.veeam.com/) data portability project.
- Led and mentored 2 fresher frontend developers.

### Fresher Software Engineer | UNI Customs Consulting

Duration: 10.2021 - 08.2022

Skills: React.js, Antd, MUI, Solr, Python, Auth0, nginx, UI/UX Design, UX Writing, Design System, Figma, Redux

- Developed [Eximhow Web App](https://www.eximhow.com/) and [UNI Customs Consulting Landing Page](https://unicustomsconsulting.com/en/home/) using React, Antd, and MUI.
- Built data extraction and processing pipelines using Python, BeautifulSoup, NumPy, and Pandas.
- Designed and implemented [HS Search System](https://uni-solr.eximhow.com/solr) using Solr.
- Worked on multi-language support, SEO and content writing.

### University of Science — VNUHCM | Education

Duration: 08.2017 - 2021

Skills: C++, Python, SQL, NLP, Data Structures, Algorithms, Advanced Databases, Systems Design, Distributed Systems, Software Engineering, Self-learning, Teamwork, Presentation

undefined

### Trung Phu High School | Education

Duration: 08.2014 - 06.2017

Skills: Algorithms, Pascal, Self-learning, Mathematics, World, Excel, PowerPoint

undefined

### Hoa Phu Secondary School | Education

Duration: 08.2010 - 06.2014

Skills: Self-learning, Mathematics

undefined

## Projects

### FMCG Project 2.0

Project URL: undefined

Skills: Next.js, shadcn, TailwindCSS, Turborepo, Agile

Leading frontend architecture and component standardization.

### Veeam - Data Portability and Resilience

Project URL: https://www.veeam.com/

Skills: React, Vite, RxJS, Microfrontend, veeamUI

- Migrated UI from MUI to veeamUI
- Developed complex directory tree for backup services (Teams, Outlook, etc.)
- Led 2 fresher frontend developers, mentoring and guiding them on code quality and best practices

### Retailer Assistant - AI Chat Bot

Project URL: https://github.com/trinhvanminh/rag-and-sql-agent

Skills: Vite, Lit, LangChain, Python, Prompt design, Streamlit, Flask, MongoDB

Supported retailers in obtaining data via SQL Agent and company information via RAG.

### FMCG Project 1.0

Project URL: undefined

Skills: React, Redux, MUI, PWA, NX

- Upgraded MUI version for multi-apps
- Standardized components across apps and applied monorepo using NX
- Migrated project from JS to TS

### Jesicar - Car Bidding App

Project URL: undefined

Skills: React Native, Expo

Maintaining and improving existing codebase.

### Eximhow Web App

Project URL: https://www.eximhow.com/

Skills: React, MUI, Solr, Figma, Google Login

- Integrated with HS search API and extracted data
- Multi-language support, UI design using Figma, Google login integration

### HS Search System

Project URL: https://uni-solr.eximhow.com/solr

Skills: Solr, Search Engine

Researched and implemented search engine using Solr.

### Data Engineer Project

Project URL: undefined

Skills: Python, BeautifulSoup, NumPy, Pandas

- Extracted and structured data from Excel, PDFs, and web sources
- Source: [Thuvienphapluat](https://thuvienphapluat.vn/phap-luat/ho-tro-phap-luat/bieu-thue-xuat-nhap-khau-2025-da-cap-nhat-theo-nghi-dinh-1742025ndcp-file-excel-bieu-thue-xuat-nhap-225003.html)

### UNI Landing Page

Project URL: https://unicustomsconsulting.com/en/home/

Skills: React, Antd, MUI, SEO

- SEO optimization (JSON-LD, Google Search Console)
- Multi-language support and content writing

## Awards

### 1st Prize | Can Tho City Young Informatics Contest 2014

- Field: Creative Software
- Project: Website Hành Trình Khám Phá Miền Tây

### Consolation Prize | National Young Informatics Contest 2014

- Organized in Hanoi
- Field: Creative Software
- Project: Website Hành Trình Khám Phá Miền Tây

### 3rd Prize | Can Tho City Young Informatics Contest 2015

- Field: Creative Software
- Project: VietnamStudy Learning Application

### 3rd Prize | Can Tho City Young Informatics Contest 2015

- Field: Informatics Knowledge and Skills
- Programming Language: Pascal

### Consolation Prize | National Young Informatics Contest 2015

- Organized in Binh Duong
- Field: Informatics Knowledge and Skills
- Programming Language: Pascal

### Outstanding Student | Most Outstanding Student of the District

Recognized by the District for exceptional academic and personal achievements as the Outstanding Student of the locality.

### 2nd Prize | Can Tho City Youth and Children's Creativity Contest 2015

- Field: Software
- Project: VietnamStudy Learning Application

### 3rd Prize | Can Tho City Science and Engineering Fair 2015

- Field: Computer Science
- Project: Distance Calculation System for Provinces, Cities, and Districts in the Mekong Delta

### Consolation Prize | Can Tho City Young Informatics Contest 2016

- Field: Creative Software
- Project: Study English

### 1st Prize | Can Tho City Youth and Children's Creativity Contest 2016

- Field: Software
- Project: Study English

### 3rd Prize | National Young Informatics Contest 2016

- Organized in Quang Binh
- Field: Hardware Programming
- Project: IoT Technology Application for Smart Home Control

### Consolation Prize | National Youth and Children's Creativity Contest 2016

- Organized in Hanoi
- Field: Software
- Project: Study English

### 2nd Prize | Can Tho City Outstanding Student Selection Exam 2016-2017

- Subject: Informatics (Programming)
- Programming Language: C++

### Consolation Prize | Can Tho City Young Informatics Contest 2017

- Field: Creative Software
- Project: Formula - Social network for sharing solutions and problem-solving methods

### 3rd Prize | Can Tho City Young Informatics Contest 2017

- Field: Informatics Knowledge and Skills
- Programming Language: C++

### 2nd Prize | Can Tho City Youth and Children's Creativity Contest 2017

- Field: Software
- Project: We Study All — Multi-Platform Learning Support Tool

### Creative Award | Binh Duong Hackathon 2017

- Completed the Binh Duong Smart Map product within 24 hours
- Youngest competing team to win an award

### 1st Prize | Can Tho City Science and Engineering Fair 2018

- Field: System Software
- Project: [UnlimitedStudy](https://muctim.tuoitre.vn/cong-cu-ho-tro-viec-day-va-hoc-55107.htm)

### 3rd Prize | Can Tho City Outstanding Student Selection Exam 2017-2018

- Subject: Informatics (Programming)
- Programming Language: C++

### 3rd Prize | National Science and Engineering Fair 2018 (ViSEF)

- Organized in Lam Dong
- Field: System Software
- Project: [UnlimitedStudy](https://muctim.tuoitre.vn/cong-cu-ho-tro-viec-day-va-hoc-55107.htm)

### 3rd Prize | Can Tho City Young Informatics Contest 2018

- Field: Creative Software
- Project: [UnlimitedStudy](https://muctim.tuoitre.vn/cong-cu-ho-tro-viec-day-va-hoc-55107.htm)

### 2nd Prize | Can Tho City Youth and Children's Creativity Contest 2018

- Field: Software
- Project: [UnlimitedStudy](https://muctim.tuoitre.vn/cong-cu-ho-tro-viec-day-va-hoc-55107.htm)

### 3rd Prize | National Young Informatics Contest 2018

- Organized in Ba Ria - Vung Tau
- Field: Creative Products
- Project: [UnlimitedStudy](https://muctim.tuoitre.vn/cong-cu-ho-tro-viec-day-va-hoc-55107.htm)

### 2nd Prize | Business Startup Competition 2019

- Organized by University of Economics and Law — VNUHCM
- Project: [Penphy](https://www.youtube.com/watch?v=EdU7rUO-UA4) — Self Development Social Network

### Bronze Medal | 10th Design, Manufacturing, and Application Award 2022

- Organized by Ho Chi Minh City Youth Union
- Field: Software
- Project: [ZaDark](https://zadark.com)

### Summer 2025 Cohort | ▲Vercel OSS Program

- Selected for [▲Vercel OSS Program](https://vercel.com/blog/summer-2025-oss-program#react-wheel-picker) summer 2025 cohort
- Received $3,600 in platform credits, OSS Starter Pack, and priority community support
- Project: [React Wheel Picker](https://react-wheel-picker.chanhdai.com)

## Certifications

- [Animations on the Web](https://animations.dev/certificate/dfb419c8-5ed7-43f2-b60c-446bc50a39b1)
- [Certificate of Trademark Registration No. 565092](https://drive.google.com/file/d/1NgRp81ZAAUKEtuI9RefG0e8yni6JkoE8/view?usp=sharing)
- [Certificate of Trademark Registration No. 543682](https://drive.google.com/file/d/1x7YzlK1kyz16h28ux9k3KAwnZFAabsvq/view?usp=sharing)
- [Next.js SEO Fundamentals](https://nextjs.org/learn/certificate?course=seo&user=47463&certId=seo-47463-1745634751873)
- [Next.js App Router Fundamentals](https://nextjs.org/learn/certificate?course=dashboard-app&user=47463&certId=dashboard-app-47463-1745633258744)
- [React Foundations for Next.js](https://nextjs.org/learn/certificate?course=react-foundations&user=47463&certId=react-foundations-47463-1745634245158)
- [Certificate of Copyright Registration No. 0040/2025/QTG](https://drive.google.com/file/d/1kVBByVLlYPyyUJvxKga670wvVPNmZ2nV/view?usp=sharing)
- [Vietnamese Standardized Test of English Proficiency (CEFR B1)](https://vbcc.hcmussh.edu.vn/?type=11&certificateType=7&no=QH58202305187&fullName=Nguy%E1%BB%85n+Ch%C3%A1nh+%C4%90%E1%BA%A1i&dob=2000-08-14)
- [Certificate of Copyright Registration No. 7994/2024/QTG](https://drive.google.com/file/d/1otjV4GNOLFj4JD2tHWLuZpUIVyITYNUy/view?usp=sharing)
- [Engaging in Persuasive and Credible Communication](https://coursera.org/verify/C6DHB9A5XQHV)
- [Introduction to Databases](https://coursera.org/verify/YV5VQ5MXZ5YH)
- [Solving Problems with Creative and Critical Thinking](https://coursera.org/verify/4UKZQJGM2932)
- [Digital Skills: Social Media](https://www.futurelearn.com/certificates/pc4345i)
- [The Fundamentals of Digital Marketing](https://drive.google.com/file/d/14laAMWmVKhCfgKwVEgJyWse7o0sZDD4N/view?usp=sharing)
- [Microsoft Office Specialist: Microsoft Office PowerPoint® 2013](https://drive.google.com/file/d/117DE6bMkHvRqXbED2tmSMNkbx9s7OahM/view?usp=sharing)
- [Google Code-in 2016](https://drive.google.com/file/d/162RXtAVIZEvfx6LvP3xeBj-cSI9ZpPUX/view?usp=sharing)
- [HSGS Olympiad 2016](https://drive.google.com/file/d/1eKv-2ldfw8-hF27sKfKWXL8MmwcSkzPq/view?usp=sharing)
- [Microsoft Office Specialist: Microsoft Office Word 2013](https://drive.google.com/file/d/1-NHhjKlQbhlcO7bpRue1XzDgDaudOf2N/view?usp=sharing)

## Blog

---
title: "Welcome to mitr.dev"
description: "A minimal, pixel-perfect dev portfolio, component registry, and blog to showcase my work as a Design Engineer."
last_updated: "November 27, 2025"
source: "https://mitr.dev/blog/welcome"
---

# Welcome to mitr.dev

A minimal, pixel-perfect dev portfolio, component registry, and blog to showcase my work as a Design Engineer.

## Tech Stack

* Next.js 16
* Tailwind CSS v4
* shadcn/ui

## Featured

* Clean & modern design
* Light & Dark theme support
* vCard integration
* SEO optimization: [JSON-LD schema](https://json-ld.org), sitemap, robots
* AI-friendly [/llms.txt](https://llmstxt.org)
* Spam-protected email
* Installable PWA

## Blog

* MDX & Markdown support
* Access markdown by adding `.md` to URLs, e.g. `/blog/welcome.md`
* Syntax Highlighting for better readability
* RSS Feed for easy content distribution
* Dynamic OG Images for rich previews

## Component Registry

Easily build and distribute your own components, hooks, and pages using a custom registry powered by the [shadcn CLI](https://ui.shadcn.com/docs/cli).

Each component is well-documented through the Blog, featuring:

* **Live Preview**: Interactive component demos
* **Code Snippets**: Readable and copyable examples
* **Beautiful Code Blocks**: Styled for clarity and dark mode
* **Command Blocks**: One-click copy for quick install commands (`pnpm`, `npm`, `yarn`, `bun`)

## License

Licensed under the [MIT license](https://github.com/trinhvanminh/mitri.dev/blob/main/LICENSE).

The source code is available on [GitHub](https://github.com/trinhvanminh/mitri.dev). You're free to use my code! Just make sure to remove all my personal information before publishing your website. It's awesome to see my code being useful to someone!


Last updated on November 27, 2025

---
title: "Shimmering Text"
description: "Smooth shimmering text animation built with Motion."
last_updated: "November 23, 2025"
source: "https://mitr.dev/blog/shimmering-text"
---

# Shimmering Text

Smooth shimmering text animation built with Motion.

```tsx
import { ShimmeringText } from "@/components/shimmering-text";

export function ShimmeringTextDemo1() {
  return (
    <ShimmeringText className="text-2xl font-medium" text="Shimmering Text" />
  );
}

```

## Installation

<CodeTabs>
  <TabsListInstallType />

  <TabsContent value="cli">
    ```bash
    npx shadcn add @ncdai/shimmering-text
    ```
  </TabsContent>

  <TabsContent value="manual">
    <Steps>
      <Step>Install the following dependencies</Step>

      ```bash
      npm install motion clsx tailwind-merge
      ```

      <Step>Add a cn helper</Step>

      ```ts title="lib/utils.ts" showLineNumbers
      import type { ClassValue } from "clsx";
      import { clsx } from "clsx";
      import { twMerge } from "tailwind-merge";

      export const cn = (...inputs: ClassValue[]) => {
        return twMerge(clsx(inputs));
      };

      ```

      <Step>Copy and paste the following code into your project</Step>

      ```tsx title="components/ncdai/shimmering-text.tsx" showLineNumbers
      "use client";

      import type { HTMLMotionProps, Variants } from "motion/react";
      import { motion } from "motion/react";
      import * as React from "react";

      import { cn } from "@/lib/utils";

      export type ShimmeringTextProps = Omit<HTMLMotionProps<"span">, "children"> & {
        text: string;
        duration?: number;
        isStopped?: boolean;
      };

      export function ShimmeringText({
        text,
        duration = 1,
        isStopped = false,
        className,
        ...props
      }: ShimmeringTextProps) {
        const createCharVariants = React.useCallback(
          (charIndex: number): Variants => ({
            running: {
              color: ["var(--color)", "var(--shimmering-color)", "var(--color)"],
              transition: {
                duration,
                repeat: Infinity,
                repeatType: "loop" as const,
                repeatDelay: text.length * 0.05,
                delay: (charIndex * duration) / text.length,
                ease: "easeInOut",
              },
            },
            stopped: {
              color: "var(--color)",
              transition: {
                duration: duration * 0.5,
                ease: "easeOut",
              },
            },
          }),
          [duration, text.length]
        );

        return (
          <motion.span
            className={cn(
              "inline-block select-none",
              "[--color:var(--color-zinc-400)] [--shimmering-color:var(--color-zinc-950)]",
              "dark:[--color:var(--color-zinc-600)] dark:[--shimmering-color:var(--color-zinc-50)]",
              className
            )}
            {...props}
          >
            {text?.split("")?.map((char, i) => (
              <motion.span
                key={i}
                className="inline-block whitespace-pre"
                initial="stopped"
                animate={isStopped ? "stopped" : "running"}
                variants={createCharVariants(i)}
              >
                {char}
              </motion.span>
            ))}
          </motion.span>
        );
      }

      ```

      <Step>Update the import paths to match your project setup</Step>
    </Steps>
  </TabsContent>
</CodeTabs>

## Usage

```tsx
import { ShimmeringText } from "@/components/ncdai/shimmering-text";
```

```tsx
<ShimmeringText text="slide to unlock" />
```

## Examples

### Custom Color

```tsx
import { ShimmeringText } from "@/components/shimmering-text";

export function ShimmeringTextDemo2() {
  return (
    <ShimmeringText
      className="font-medium [--color:#fbbf24] [--shimmering-color:#ca8a04] dark:[--color:#f59e0b] dark:[--shimmering-color:#fcd34d]"
      text="Processing your request with AI ..."
    />
  );
}

```

CSS Variables explanation:

* `--color`: Base text color (the default/resting state of characters)
* `--shimmering-color`: Peak highlight color (the bright color characters transition to during the shimmer effect)


Last updated on November 23, 2025

---
title: "Testimonials Marquee"
description: "A testimonials marquee component for showcasing user feedback in a scrolling format."
last_updated: "November 22, 2025"
source: "https://mitr.dev/blog/testimonials-marquee"
---

# Testimonials Marquee

A testimonials marquee component for showcasing user feedback in a scrolling format.

```tsx
import {
  Marquee,
  MarqueeContent,
  MarqueeFade,
  MarqueeItem,
} from "@/components/kibo-ui/marquee";
import {
  Testimonial,
  TestimonialAuthor,
  TestimonialAuthorBio,
  TestimonialAuthorName,
  TestimonialAvatar,
  TestimonialAvatarImg,
  TestimonialAvatarRing,
  TestimonialQuote,
  TestimonialVerifiedBadge,
} from "@/components/testimonials-marquee";

export function TestimonialsMarqueeDemo1() {
  return (
    <div className="w-full bg-background">
      <Marquee className="border-y border-edge [&_.rfm-initial-child-container]:items-stretch! [&_.rfm-marquee]:items-stretch!">
        <MarqueeFade side="left" />
        <MarqueeFade side="right" />

        <MarqueeContent>
          {TESTIMONIALS.map((item) => (
            <MarqueeItem
              key={item.url}
              className="mx-0 h-full w-xs border-r border-edge"
            >
              <a
                href={item.url}
                target="_blank"
                rel="noopener noreferrer"
                className="block h-full"
              >
                <Testimonial>
                  <TestimonialQuote>
                    <p>{item.quote}</p>
                  </TestimonialQuote>

                  <TestimonialAuthor>
                    <TestimonialAvatar>
                      <TestimonialAvatarImg src={item.authorAvatar} />
                      <TestimonialAvatarRing />
                    </TestimonialAvatar>

                    <TestimonialAuthorName>
                      {item.authorName}
                      <TestimonialVerifiedBadge />
                    </TestimonialAuthorName>

                    <TestimonialAuthorBio>
                      {item.authorBio}
                    </TestimonialAuthorBio>
                  </TestimonialAuthor>
                </Testimonial>
              </a>
            </MarqueeItem>
          ))}
        </MarqueeContent>
      </Marquee>
    </div>
  );
}

const TESTIMONIALS = [
  {
    authorAvatar:
      "https://pbs.twimg.com/profile_images/1783856060249595904/8TfcCN0r_400x400.jpg",
    authorName: "Guillermo Rauch",
    authorBio: "CEO @Vercel",
    url: "https://x.com/rauchg/status/1978913158514237669",
    quote:
      "awesome. Love the components, especially slide-to-unlock. Great job",
  },
  {
    authorAvatar:
      "https://pbs.twimg.com/profile_images/1756766826736893952/6Gvg6jha_400x400.jpg",
    authorName: "OrcDev",
    authorBio: "Creator of 8bitcn.com",
    url: "https://x.com/theorcdev/status/1980378575170859446",
    quote:
      "Seriously, this is one of the best portfolio templates I've ever seen.",
  },
  {
    authorAvatar:
      "https://pbs.twimg.com/profile_images/1954573702768504833/LW-j7iTr_400x400.jpg",
    authorName: "Sahaj",
    authorBio: "Creator of tweakcn.com",
    url: "https://x.com/iamsahaj_xyz/status/1982814244501381239",
    quote:
      "remember seeing it on @mannupaaji's review. it's one of the best looking ones I've seen",
  },
  {
    authorAvatar:
      "https://pbs.twimg.com/profile_images/1923813473240203264/owJG92AC_400x400.jpg",
    authorName: "Steven Tey",
    authorBio: "Founder @Dub.co",
    url: "https://x.com/steventey/status/1936934909370830924",
    quote: "whoa, this is really dope – needs to get added to @shadcn UI 👀",
  },
  {
    authorAvatar:
      "https://pbs.twimg.com/profile_images/1868745200010428416/jKSU2o_W_400x400.jpg",
    authorName: "Kap",
    authorBio: "Head of Developer Community @Vercel",
    url: "https://x.com/kapehe_ok/status/1948104774358106612",
    quote: "one of my favorite projects that submitted! you are crushing it!",
  },
  {
    authorAvatar:
      "https://pbs.twimg.com/profile_images/1962676904013492224/crWj9Gfa_400x400.jpg",
    authorName: "Jean P.D. Meijer",
    authorBio: "Building analog.now",
    url: "https://x.com/initjean/status/1948159885960438151",
    quote:
      "congrats you deserve it! react wheel picker is so smooth, its insane 🐐",
  },
  {
    authorAvatar:
      "https://pbs.twimg.com/profile_images/1679831765744259073/hoVtsOZ9_400x400.jpg",
    authorName: "GitHub Projects Community",
    authorBio: "UNOFFICIAL, but followed by @github",
    url: "https://x.com/GithubProjects/status/1931034244337271044",
    quote:
      "Everything you'd want in a picker, minus the styling headaches. Awesome job!",
  },
];

```

## Installation

<CodeTabs>
  <TabsListInstallType />

  <TabsContent value="cli">
    ```bash
    npx shadcn add @ncdai/testimonials-marquee
    ```
  </TabsContent>

  <TabsContent value="manual">
    <Steps>
      <Step>Install the following dependencies</Step>

      ```bash
      npm install clsx tailwind-merge react-fast-marquee
      ```

      <Step>Add a cn helper</Step>

      ```ts title="lib/utils.ts" showLineNumbers
      import type { ClassValue } from "clsx";
      import { clsx } from "clsx";
      import { twMerge } from "tailwind-merge";

      export const cn = (...inputs: ClassValue[]) => {
        return twMerge(clsx(inputs));
      };

      ```

      <Step>Copy and paste the following code into your project</Step>

      ```tsx title="components/ncdai/testimonials-marquee.tsx" showLineNumbers
      import { cn } from "@/lib/utils";

      export function Testimonial({
        className,
        children,
        ...props
      }: React.ComponentProps<"figure">) {
        return (
          <figure
            data-slot="testimonial"
            className={cn("flex h-full flex-col", className)}
            {...props}
          >
            {children}
          </figure>
        );
      }

      export function TestimonialQuote({
        className,
        children,
        ...props
      }: React.ComponentProps<"blockquote">) {
        return (
          <blockquote
            data-slot="quote"
            className={cn(
              "grow px-4 py-3 text-base text-balance text-foreground italic",
              className
            )}
            {...props}
          >
            {children}
          </blockquote>
        );
      }

      export function TestimonialAuthor({
        className,
        children,
        ...props
      }: React.ComponentProps<"figcaption">) {
        return (
          <figcaption
            data-slot="author"
            className={cn(
              "grid grid-cols-[auto_1fr] grid-rows-2 items-center gap-x-3.5 px-4 pt-1 pb-3",
              className
            )}
            {...props}
          >
            {children}
          </figcaption>
        );
      }

      export function TestimonialAvatar({
        className,
        children,
        ...props
      }: React.ComponentProps<"div">) {
        return (
          <div
            data-slot="avatar"
            className={cn("relative row-span-2 size-8 shrink-0", className)}
            {...props}
          >
            {children}
          </div>
        );
      }

      export function TestimonialAvatarImg({
        className,
        src,
        alt,
        ...props
      }: React.ComponentProps<"img">) {
        return (
          <img
            data-slot="avatar-img"
            className={cn("size-8 rounded-full select-none", className)}
            src={src}
            alt={alt}
            {...props}
          />
        );
      }

      export function TestimonialAvatarRing({
        className,
        ...props
      }: React.ComponentProps<"div">) {
        return (
          <div
            data-slot="avatar-ring"
            className={cn(
              "pointer-events-none absolute inset-0 rounded-full ring-1 ring-black/10 ring-inset dark:ring-white/15",
              className
            )}
            {...props}
          />
        );
      }

      export function TestimonialAuthorName({
        className,
        children,
        ...props
      }: React.ComponentProps<"div">) {
        return (
          <div
            data-slot="author-name"
            className={cn(
              "text-sm leading-4 font-semibold text-foreground",
              className
            )}
            {...props}
          >
            {children}
          </div>
        );
      }

      export function TestimonialAuthorBio({
        className,
        children,
        ...props
      }: React.ComponentProps<"div">) {
        return (
          <div
            data-slot="author-bio"
            className={cn(
              "text-xs leading-4 text-balance text-muted-foreground",
              className
            )}
            {...props}
          >
            {children}
          </div>
        );
      }

      export function TestimonialVerifiedBadge({
        className,
        ...props
      }: React.ComponentProps<"svg">) {
        return (
          <svg
            data-slot="verified-badge"
            className={cn(
              "ml-1.5 inline-block size-3 -translate-y-px text-info",
              className
            )}
            xmlns="http://www.w3.org/2000/svg"
            fill="none"
            viewBox="0 0 24 24"
            {...props}
          >
            <path
              fill="currentColor"
              d="M24 12a4.454 4.454 0 0 0-2.564-3.91 4.437 4.437 0 0 0-.948-4.578 4.436 4.436 0 0 0-4.577-.948A4.44 4.44 0 0 0 12 0a4.423 4.423 0 0 0-3.9 2.564 4.434 4.434 0 0 0-2.43-.178 4.425 4.425 0 0 0-2.158 1.126 4.42 4.42 0 0 0-1.12 2.156 4.42 4.42 0 0 0 .183 2.421A4.456 4.456 0 0 0 0 12a4.465 4.465 0 0 0 2.576 3.91 4.433 4.433 0 0 0 .936 4.577 4.459 4.459 0 0 0 4.577.95A4.454 4.454 0 0 0 12 24a4.439 4.439 0 0 0 3.91-2.563 4.26 4.26 0 0 0 5.526-5.526A4.453 4.453 0 0 0 24 12Zm-13.709 4.917-4.38-4.378 1.652-1.663 2.646 2.646L15.83 7.4l1.72 1.591-7.258 7.926Z"
            />
          </svg>
        );
      }

      ```

      ```tsx title="components/kibo-ui/marquee.tsx" showLineNumbers
      "use client";

      import type { HTMLAttributes } from "react";
      import type { MarqueeProps as FastMarqueeProps } from "react-fast-marquee";
      import FastMarquee from "react-fast-marquee";

      import { cn } from "@/lib/utils";

      export type MarqueeProps = HTMLAttributes<HTMLDivElement>;

      export const Marquee = ({ className, ...props }: MarqueeProps) => (
        <div
          className={cn("relative w-full overflow-hidden", className)}
          {...props}
        />
      );

      export type MarqueeContentProps = FastMarqueeProps;

      export const MarqueeContent = ({
        loop = 0,
        autoFill = true,
        pauseOnHover = true,
        ...props
      }: MarqueeContentProps) => (
        <FastMarquee
          autoFill={autoFill}
          loop={loop}
          pauseOnHover={pauseOnHover}
          {...props}
        />
      );

      export type MarqueeFadeProps = HTMLAttributes<HTMLDivElement> & {
        side: "left" | "right";
      };

      export const MarqueeFade = ({
        className,
        side,
        ...props
      }: MarqueeFadeProps) => (
        <div
          className={cn(
            "absolute top-0 bottom-0 z-10 h-full w-20 from-background to-transparent",
            side === "left" ? "left-0 bg-linear-to-r" : "right-0 bg-linear-to-l",
            className
          )}
          {...props}
        />
      );

      export type MarqueeItemProps = HTMLAttributes<HTMLDivElement>;

      export const MarqueeItem = ({ className, ...props }: MarqueeItemProps) => (
        <div className={cn("mx-2 shrink-0 object-contain", className)} {...props} />
      );

      ```

      <Step>Update the import paths to match your project setup</Step>
    </Steps>
  </TabsContent>
</CodeTabs>

## Usage

```tsx
import {
  Marquee,
  MarqueeContent,
  MarqueeFade,
  MarqueeItem,
} from "@/components/kibo-ui/marquee";

import {
  Testimonial,
  TestimonialAuthor,
  TestimonialAuthorBio,
  TestimonialAuthorInfo,
  TestimonialAuthorName,
  TestimonialAvatar,
  TestimonialAvatarImg,
  TestimonialAvatarRing,
  TestimonialQuote,
  TestimonialVerifiedBadge,
} from "@/components/ncdai/testimonials-marquee";
```

```tsx
<Marquee>
  <MarqueeFade />
  <MarqueeFade />

  <MarqueeContent>
    <MarqueeItem>
      <Testimonial>
        <TestimonialQuote />

        <TestimonialAuthor>
          <TestimonialAvatar>
            <TestimonialAvatarImg />
            <TestimonialAvatarRing />
          </TestimonialAvatar>

          <TestimonialAuthorName>
            <TestimonialVerifiedBadge />
          </TestimonialAuthorName>

          <TestimonialAuthorBio />
        </TestimonialAuthor>
      </Testimonial>
    </MarqueeItem>
    ...
  </MarqueeContent>
</Marquee>
```

## API Reference

* [Marquee](https://www.kibo-ui.com/components/marquee)
* [React FAST Marquee](https://www.react-fast-marquee.com)

## Examples

### Single Row

```tsx
import {
  Marquee,
  MarqueeContent,
  MarqueeFade,
  MarqueeItem,
} from "@/components/kibo-ui/marquee";
import {
  Testimonial,
  TestimonialAuthor,
  TestimonialAuthorBio,
  TestimonialAuthorName,
  TestimonialAvatar,
  TestimonialAvatarImg,
  TestimonialAvatarRing,
  TestimonialQuote,
  TestimonialVerifiedBadge,
} from "@/components/testimonials-marquee";

export function TestimonialsMarqueeDemo1() {
  return (
    <div className="w-full bg-background">
      <Marquee className="border-y border-edge [&_.rfm-initial-child-container]:items-stretch! [&_.rfm-marquee]:items-stretch!">
        <MarqueeFade side="left" />
        <MarqueeFade side="right" />

        <MarqueeContent>
          {TESTIMONIALS.map((item) => (
            <MarqueeItem
              key={item.url}
              className="mx-0 h-full w-xs border-r border-edge"
            >
              <a
                href={item.url}
                target="_blank"
                rel="noopener noreferrer"
                className="block h-full"
              >
                <Testimonial>
                  <TestimonialQuote>
                    <p>{item.quote}</p>
                  </TestimonialQuote>

                  <TestimonialAuthor>
                    <TestimonialAvatar>
                      <TestimonialAvatarImg src={item.authorAvatar} />
                      <TestimonialAvatarRing />
                    </TestimonialAvatar>

                    <TestimonialAuthorName>
                      {item.authorName}
                      <TestimonialVerifiedBadge />
                    </TestimonialAuthorName>

                    <TestimonialAuthorBio>
                      {item.authorBio}
                    </TestimonialAuthorBio>
                  </TestimonialAuthor>
                </Testimonial>
              </a>
            </MarqueeItem>
          ))}
        </MarqueeContent>
      </Marquee>
    </div>
  );
}

const TESTIMONIALS = [
  {
    authorAvatar:
      "https://pbs.twimg.com/profile_images/1783856060249595904/8TfcCN0r_400x400.jpg",
    authorName: "Guillermo Rauch",
    authorBio: "CEO @Vercel",
    url: "https://x.com/rauchg/status/1978913158514237669",
    quote:
      "awesome. Love the components, especially slide-to-unlock. Great job",
  },
  {
    authorAvatar:
      "https://pbs.twimg.com/profile_images/1756766826736893952/6Gvg6jha_400x400.jpg",
    authorName: "OrcDev",
    authorBio: "Creator of 8bitcn.com",
    url: "https://x.com/theorcdev/status/1980378575170859446",
    quote:
      "Seriously, this is one of the best portfolio templates I've ever seen.",
  },
  {
    authorAvatar:
      "https://pbs.twimg.com/profile_images/1954573702768504833/LW-j7iTr_400x400.jpg",
    authorName: "Sahaj",
    authorBio: "Creator of tweakcn.com",
    url: "https://x.com/iamsahaj_xyz/status/1982814244501381239",
    quote:
      "remember seeing it on @mannupaaji's review. it's one of the best looking ones I've seen",
  },
  {
    authorAvatar:
      "https://pbs.twimg.com/profile_images/1923813473240203264/owJG92AC_400x400.jpg",
    authorName: "Steven Tey",
    authorBio: "Founder @Dub.co",
    url: "https://x.com/steventey/status/1936934909370830924",
    quote: "whoa, this is really dope – needs to get added to @shadcn UI 👀",
  },
  {
    authorAvatar:
      "https://pbs.twimg.com/profile_images/1868745200010428416/jKSU2o_W_400x400.jpg",
    authorName: "Kap",
    authorBio: "Head of Developer Community @Vercel",
    url: "https://x.com/kapehe_ok/status/1948104774358106612",
    quote: "one of my favorite projects that submitted! you are crushing it!",
  },
  {
    authorAvatar:
      "https://pbs.twimg.com/profile_images/1962676904013492224/crWj9Gfa_400x400.jpg",
    authorName: "Jean P.D. Meijer",
    authorBio: "Building analog.now",
    url: "https://x.com/initjean/status/1948159885960438151",
    quote:
      "congrats you deserve it! react wheel picker is so smooth, its insane 🐐",
  },
  {
    authorAvatar:
      "https://pbs.twimg.com/profile_images/1679831765744259073/hoVtsOZ9_400x400.jpg",
    authorName: "GitHub Projects Community",
    authorBio: "UNOFFICIAL, but followed by @github",
    url: "https://x.com/GithubProjects/status/1931034244337271044",
    quote:
      "Everything you'd want in a picker, minus the styling headaches. Awesome job!",
  },
];

```

### Multiple Rows

```tsx
import {
  Marquee,
  MarqueeContent,
  MarqueeFade,
  MarqueeItem,
} from "@/components/kibo-ui/marquee";
import {
  Testimonial,
  TestimonialAuthor,
  TestimonialAuthorBio,
  TestimonialAuthorName,
  TestimonialAvatar,
  TestimonialAvatarImg,
  TestimonialAvatarRing,
  TestimonialQuote,
  TestimonialVerifiedBadge,
} from "@/components/testimonials-marquee";

export function TestimonialsMarqueeDemo2() {
  return (
    <div className="w-full space-y-4 bg-background [&_.rfm-initial-child-container]:items-stretch! [&_.rfm-marquee]:items-stretch!">
      {[TESTIMONIALS_1, TESTIMONIALS_2].map((list, index) => (
        <Marquee key={index} className="border-y border-edge">
          <MarqueeFade side="left" />
          <MarqueeFade side="right" />

          <MarqueeContent direction={index % 2 === 1 ? "right" : "left"}>
            {list.map((item) => (
              <MarqueeItem
                key={item.url}
                className="mx-0 h-full w-xs border-r border-edge"
              >
                <a
                  href={item.url}
                  target="_blank"
                  rel="noopener noreferrer"
                  className="block h-full"
                >
                  <Testimonial>
                    <TestimonialQuote>
                      <p>{item.quote}</p>
                    </TestimonialQuote>

                    <TestimonialAuthor>
                      <TestimonialAvatar>
                        <TestimonialAvatarImg src={item.authorAvatar} />
                        <TestimonialAvatarRing />
                      </TestimonialAvatar>

                      <TestimonialAuthorName>
                        {item.authorName}
                        <TestimonialVerifiedBadge />
                      </TestimonialAuthorName>

                      <TestimonialAuthorBio>
                        {item.authorBio}
                      </TestimonialAuthorBio>
                    </TestimonialAuthor>
                  </Testimonial>
                </a>
              </MarqueeItem>
            ))}
          </MarqueeContent>
        </Marquee>
      ))}
    </div>
  );
}

const TESTIMONIALS_1 = [
  {
    authorAvatar:
      "https://pbs.twimg.com/profile_images/1783856060249595904/8TfcCN0r_400x400.jpg",
    authorName: "Guillermo Rauch",
    authorBio: "CEO @Vercel",
    url: "https://x.com/rauchg/status/1978913158514237669",
    quote:
      "awesome. Love the components, especially slide-to-unlock. Great job",
  },
  {
    authorAvatar:
      "https://pbs.twimg.com/profile_images/1756766826736893952/6Gvg6jha_400x400.jpg",
    authorName: "OrcDev",
    authorBio: "Creator of 8bitcn.com",
    url: "https://x.com/theorcdev/status/1980378575170859446",
    quote:
      "Seriously, this is one of the best portfolio templates I've ever seen.",
  },
  {
    authorAvatar:
      "https://pbs.twimg.com/profile_images/1954573702768504833/LW-j7iTr_400x400.jpg",
    authorName: "Sahaj",
    authorBio: "Creator of tweakcn.com",
    url: "https://x.com/iamsahaj_xyz/status/1982814244501381239",
    quote:
      "remember seeing it on @mannupaaji's review. it's one of the best looking ones I've seen",
  },
  {
    authorAvatar:
      "https://pbs.twimg.com/profile_images/1923813473240203264/owJG92AC_400x400.jpg",
    authorName: "Steven Tey",
    authorBio: "Founder @Dub.co",
    url: "https://x.com/steventey/status/1936934909370830924",
    quote: "whoa, this is really dope – needs to get added to @shadcn UI 👀",
  },
  {
    authorAvatar:
      "https://pbs.twimg.com/profile_images/1868745200010428416/jKSU2o_W_400x400.jpg",
    authorName: "Kap",
    authorBio: "Head of Developer Community @Vercel",
    url: "https://x.com/kapehe_ok/status/1948104774358106612",
    quote: "one of my favorite projects that submitted! you are crushing it!",
  },
  {
    authorAvatar:
      "https://pbs.twimg.com/profile_images/1962676904013492224/crWj9Gfa_400x400.jpg",
    authorName: "Jean P.D. Meijer",
    authorBio: "Building analog.now",
    url: "https://x.com/initjean/status/1948159885960438151",
    quote:
      "congrats you deserve it! react wheel picker is so smooth, its insane 🐐",
  },
  {
    authorAvatar:
      "https://pbs.twimg.com/profile_images/1679831765744259073/hoVtsOZ9_400x400.jpg",
    authorName: "GitHub Projects Community",
    authorBio: "UNOFFICIAL, but followed by @github",
    url: "https://x.com/GithubProjects/status/1931034244337271044",
    quote:
      "Everything you'd want in a picker, minus the styling headaches. Awesome job!",
  },
];

export const TESTIMONIALS_2 = [
  {
    authorAvatar:
      "https://pbs.twimg.com/profile_images/1417752099488636931/cs2R59eW_400x400.jpg",
    authorName: "Manu Arora",
    authorBio: "Creator of ui.aceternity.com",
    url: "https://x.com/mannupaaji/status/1944755561117163597",
    quote: "Great work on the portfolio",
  },
  {
    authorAvatar:
      "https://pbs.twimg.com/profile_images/1913274259985854464/rWnStpCJ_400x400.jpg",
    authorName: "Max Prilutskiy",
    authorBio: "Creator of Lingo.dev",
    url: "https://x.com/MaxPrilutskiy/status/1923952193893466379",
    quote: "i like your style! :)",
  },
  {
    authorAvatar:
      "https://pbs.twimg.com/profile_images/1988148531018743808/bjS19FOd_400x400.jpg",
    authorName: "Aaron",
    authorBio: "Founding Design Engineer @Mail0",
    url: "https://x.com/aaronmahlke/status/1955606729657344490",
    quote: "super cool portfolio!",
  },
  {
    authorAvatar:
      "https://pbs.twimg.com/profile_images/1333329963383103489/ee7VrWQy_400x400.jpg",
    authorName: "jordwalke",
    authorBio: "Creator of React",
    url: "https://x.com/jordwalke/status/1937165909778657589",
    quote: "Looks great.\nAlso, cool wheel picker!",
  },
  {
    authorAvatar:
      "https://pbs.twimg.com/profile_images/1875548702971985920/Fp6h28Es_400x400.jpg",
    authorName: "Yonaries",
    authorBio: "Making orabrowser.com",
    url: "https://x.com/YonathanDejene/status/1984529637309886639",
    quote: "incredible portfolio i've seen by far",
  },
  {
    authorAvatar:
      "https://pbs.twimg.com/profile_images/1927419360492011520/bitKDhjx_400x400.jpg",
    authorName: "Zaid",
    authorBio: "Creator of scira.ai",
    url: "https://x.com/zaidmukaddam/status/1984599685974409374",
    quote: "super clean",
  },
];

```


Last updated on November 22, 2025

---
title: "Slide to Unlock"
description: "A sleek, interactive slider inspired by the classic iPhone OS "slide to unlock" gesture."
last_updated: "November 23, 2025"
source: "https://mitr.dev/blog/slide-to-unlock"
---

# Slide to Unlock

A sleek, interactive slider inspired by the classic iPhone OS "slide to unlock" gesture.

```tsx
"use client";

import { toast } from "sonner";

import { useSound } from "@/hooks/use-sound";
import { ShimmeringText } from "@/components/shimmering-text";
import {
  SlideToUnlock,
  SlideToUnlockHandle,
  SlideToUnlockText,
  SlideToUnlockTrack,
} from "@/components/slide-to-unlock";

export function SlideToUnlockDemo1() {
  const playSound = useSound("/audio/ui-sounds/unlock.wav");

  return (
    <SlideToUnlock
      onUnlock={() => {
        playSound();
        toast.success("Unlocked");
      }}
    >
      <SlideToUnlockTrack>
        <SlideToUnlockText>
          {({ isDragging }) => (
            <ShimmeringText text="slide to unlock" isStopped={isDragging} />
          )}
        </SlideToUnlockText>
        <SlideToUnlockHandle />
      </SlideToUnlockTrack>
    </SlideToUnlock>
  );
}

```

> "awesome. Love the components, especially slide-to-unlock. Great job"<br />
> — [Guillermo Rauch](https://x.com/rauchg/status/1978913158514237669), CEO & Founder at [Vercel](https://vercel.com)

## Installation

<CodeTabs>
  <TabsListInstallType />

  <TabsContent value="cli">
    ```bash
    npx shadcn add @ncdai/slide-to-unlock
    ```
  </TabsContent>

  <TabsContent value="manual">
    <Steps>
      <Step>Install the following dependencies</Step>

      ```bash
      npm install motion clsx tailwind-merge
      ```

      <Step>Add a cn helper</Step>

      ```ts title="lib/utils.ts" showLineNumbers
      import type { ClassValue } from "clsx";
      import { clsx } from "clsx";
      import { twMerge } from "tailwind-merge";

      export const cn = (...inputs: ClassValue[]) => {
        return twMerge(clsx(inputs));
      };

      ```

      <Step>Copy and paste the following code into your project</Step>

      ```tsx title="components/ncdai/shimmering-text.tsx" showLineNumbers
      "use client";

      import type { HTMLMotionProps, Variants } from "motion/react";
      import { motion } from "motion/react";
      import * as React from "react";

      import { cn } from "@/lib/utils";

      export type ShimmeringTextProps = Omit<HTMLMotionProps<"span">, "children"> & {
        text: string;
        duration?: number;
        isStopped?: boolean;
      };

      export function ShimmeringText({
        text,
        duration = 1,
        isStopped = false,
        className,
        ...props
      }: ShimmeringTextProps) {
        const createCharVariants = React.useCallback(
          (charIndex: number): Variants => ({
            running: {
              color: ["var(--color)", "var(--shimmering-color)", "var(--color)"],
              transition: {
                duration,
                repeat: Infinity,
                repeatType: "loop" as const,
                repeatDelay: text.length * 0.05,
                delay: (charIndex * duration) / text.length,
                ease: "easeInOut",
              },
            },
            stopped: {
              color: "var(--color)",
              transition: {
                duration: duration * 0.5,
                ease: "easeOut",
              },
            },
          }),
          [duration, text.length]
        );

        return (
          <motion.span
            className={cn(
              "inline-block select-none",
              "[--color:var(--color-zinc-400)] [--shimmering-color:var(--color-zinc-950)]",
              "dark:[--color:var(--color-zinc-600)] dark:[--shimmering-color:var(--color-zinc-50)]",
              className
            )}
            {...props}
          >
            {text?.split("")?.map((char, i) => (
              <motion.span
                key={i}
                className="inline-block whitespace-pre"
                initial="stopped"
                animate={isStopped ? "stopped" : "running"}
                variants={createCharVariants(i)}
              >
                {char}
              </motion.span>
            ))}
          </motion.span>
        );
      }

      ```

      ```tsx title="components/ncdai/slide-to-unlock.tsx" showLineNumbers
      "use client";

      import {
        animate,
        motion,
        type MotionValue,
        useMotionValue,
        useTransform,
      } from "motion/react";
      import {
        type ComponentPropsWithoutRef,
        createContext,
        useCallback,
        useContext,
        useRef,
        useState,
      } from "react";

      import { cn } from "@/lib/utils";

      type SlideToUnlockContextValue = {
        x: MotionValue<number>;
        trackRef: React.RefObject<HTMLDivElement | null>;
        isDragging: boolean;
        handleWidth: number;
        textOpacity: MotionValue<number>;
        onDragStart: () => void;
        onDragEnd: () => void;
      };

      const SlideToUnlockContext = createContext<SlideToUnlockContextValue | null>(
        null
      );

      function useSlideToUnlock() {
        const context = useContext(SlideToUnlockContext);
        if (!context) {
          throw new Error(
            `SlideToUnlock components must be used within SlideToUnlock`
          );
        }
        return context;
      }

      export type SlideToUnlockRootProps = React.ComponentProps<"div"> & {
        handleWidth?: number;
        onUnlock?: () => void;
      };

      export function SlideToUnlock({
        className,
        handleWidth = 56,
        children,
        onUnlock,
        ...props
      }: SlideToUnlockRootProps) {
        const trackRef = useRef<HTMLDivElement>(null);
        const [isDragging, setIsDragging] = useState(false);
        const x = useMotionValue(0);

        const fadeDistance = handleWidth;
        const textOpacity = useTransform(x, [0, fadeDistance], [1, 0]);

        const handleDragStart = useCallback(() => {
          setIsDragging(true);
        }, []);

        const handleDragEnd = useCallback(() => {
          setIsDragging(false);

          const trackWidth = trackRef.current?.offsetWidth || 0;
          const maxX = trackWidth - handleWidth;

          if (x.get() >= maxX) {
            onUnlock?.();
          } else {
            animate(x, 0, { type: "spring", bounce: 0, duration: 0.25 });
          }
        }, [x, onUnlock, handleWidth]);

        return (
          <SlideToUnlockContext.Provider
            value={{
              x,
              trackRef,
              isDragging,
              handleWidth,
              textOpacity,
              onDragStart: handleDragStart,
              onDragEnd: handleDragEnd,
            }}
          >
            <div
              data-slot="slide-to-unlock"
              className={cn(
                "w-[216px] rounded-xl bg-zinc-100 p-1 shadow-inner ring ring-black/5 ring-inset dark:bg-zinc-900 dark:ring-white/10",
                className
              )}
              {...props}
            >
              {children}
            </div>
          </SlideToUnlockContext.Provider>
        );
      }

      export type SlideToUnlockTrackProps = React.ComponentProps<"div">;

      export function SlideToUnlockTrack({
        className,
        children,
        ...props
      }: SlideToUnlockTrackProps) {
        const { trackRef } = useSlideToUnlock();

        return (
          <div
            ref={trackRef}
            data-slot="track"
            className={cn(
              "relative flex h-10 items-center justify-center",
              className
            )}
            {...props}
          >
            {children}
          </div>
        );
      }

      export type SlideToUnlockTextProps = Omit<
        ComponentPropsWithoutRef<typeof motion.div>,
        "children"
      > & {
        children:
          | React.ReactNode
          | ((props: { isDragging: boolean }) => React.ReactNode);
      };

      export function SlideToUnlockText({
        className,
        children,
        style,
        ...props
      }: SlideToUnlockTextProps) {
        const { handleWidth, textOpacity, isDragging } = useSlideToUnlock();

        return (
          <motion.div
            data-slot="text"
            data-dragging={isDragging}
            className={cn("pl-1 text-lg font-medium", className)}
            style={{ marginLeft: handleWidth, opacity: textOpacity, ...style }}
            {...props}
          >
            {typeof children === "function" ? children({ isDragging }) : children}
          </motion.div>
        );
      }

      export type SlideToUnlockHandleProps = ComponentPropsWithoutRef<
        typeof motion.div
      >;

      export function SlideToUnlockHandle({
        className,
        children,
        style,
        ...props
      }: SlideToUnlockHandleProps) {
        const {
          x,
          trackRef,
          onDragStart,
          onDragEnd,
          handleWidth: width,
        } = useSlideToUnlock();

        return (
          <motion.div
            data-slot="handle"
            className={cn(
              "absolute top-0 left-0 flex h-10 cursor-grab items-center justify-center rounded-lg bg-white text-zinc-400 shadow-sm active:cursor-grabbing",
              "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-6",
              className
            )}
            style={{ width, x, ...style }}
            drag="x"
            dragDirectionLock
            dragConstraints={trackRef}
            dragElastic={0}
            dragMomentum={false}
            onDragStart={onDragStart}
            onDragEnd={onDragEnd}
            {...props}
          >
            {children ?? (
              <svg
                xmlns="http://www.w3.org/2000/svg"
                viewBox="0 0 256 256"
                aria-hidden
              >
                <path
                  d="M237.66,133.66l-96,96A8,8,0,0,1,128,224V184H48a16,16,0,0,1-16-16V88A16,16,0,0,1,48,72h80V32a8,8,0,0,1,13.66-5.66l96,96A8,8,0,0,1,237.66,133.66Z"
                  fill="currentColor"
                />
              </svg>
            )}
          </motion.div>
        );
      }

      ```

      <Step>Update the import paths to match your project setup</Step>
    </Steps>
  </TabsContent>
</CodeTabs>

## Usage

```tsx
import {
  SlideToUnlock,
  SlideToUnlockHandle,
  SlideToUnlockText,
  SlideToUnlockTrack,
} from "@/components/ncdai/slide-to-unlock";

import { ShimmeringText } from "@/component/ncdai/shimmering-text";
```

```tsx
<SlideToUnlock>
  <SlideToUnlockTrack>
    <SlideToUnlockText>
      <ShimmeringText />
    </SlideToUnlockText>
    <SlideToUnlockHandle />
  </SlideToUnlockTrack>
</SlideToUnlock>
```

## API Reference

### SlideToUnlock

The root component that provides context for all child components.

| Prop          | Type         | Default | Description                                                  |
| ------------- | ------------ | ------- | ------------------------------------------------------------ |
| `handleWidth` | `number`     | `56`    | Width of the draggable handle in pixels.                     |
| `onUnlock`    | `() => void` |         | Callback function triggered when the handle reaches the end. |
| `className`   | `string`     |         | Additional CSS classes to apply to the root element.         |

### SlideToUnlockTrack

The container that holds the text and handle components.

| Prop        | Type     | Description                                           |
| ----------- | -------- | ----------------------------------------------------- |
| `className` | `string` | Additional CSS classes to apply to the track element. |

### SlideToUnlockText

The text label that fades out as the handle is dragged.

| Prop        | Type                                                         | Description                                                   |
| ----------- | ------------------------------------------------------------ | ------------------------------------------------------------- |
| `children`  | `ReactNode \| (props: { isDragging: boolean }) => ReactNode` | Text content or render function that receives dragging state. |
| `className` | `string`                                                     | Additional CSS classes to apply to the text element.          |

### SlideToUnlockHandle

The draggable handle element.

| Prop        | Type        | Default      | Description                                            |
| ----------- | ----------- | ------------ | ------------------------------------------------------ |
| `children`  | `ReactNode` | `ArrowRight` | Custom icon or content to display inside the handle.   |
| `className` | `string`    |              | Additional CSS classes to apply to the handle element. |

### ShimmeringText

An animated text component that creates a shimmering effect by sequentially animating each character's color.

| Prop        | Type      | Default | Description                                                                                  |
| ----------- | --------- | ------- | -------------------------------------------------------------------------------------------- |
| `text`      | `string`  |         | The text content to display with the shimmering effect.                                      |
| `duration`  | `number`  | `1`     | The duration (in seconds) for each character's shimmer animation cycle.                      |
| `isStopped` | `boolean` | `false` | Controls whether the animation is stopped. When true, characters return to their base color. |
| `className` | `string`  |         | Additional CSS classes to apply to the root span element.                                    |

## Examples

### Default

```tsx
"use client";

import { toast } from "sonner";

import { useSound } from "@/hooks/use-sound";
import { ShimmeringText } from "@/components/shimmering-text";
import {
  SlideToUnlock,
  SlideToUnlockHandle,
  SlideToUnlockText,
  SlideToUnlockTrack,
} from "@/components/slide-to-unlock";

export function SlideToUnlockDemo1() {
  const playSound = useSound("/audio/ui-sounds/unlock.wav");

  return (
    <SlideToUnlock
      onUnlock={() => {
        playSound();
        toast.success("Unlocked");
      }}
    >
      <SlideToUnlockTrack>
        <SlideToUnlockText>
          {({ isDragging }) => (
            <ShimmeringText text="slide to unlock" isStopped={isDragging} />
          )}
        </SlideToUnlockText>
        <SlideToUnlockHandle />
      </SlideToUnlockTrack>
    </SlideToUnlock>
  );
}

```

### Custom Color

```tsx
"use client";

import { toast } from "sonner";

import { ShimmeringText } from "@/components/shimmering-text";
import {
  SlideToUnlock,
  SlideToUnlockHandle,
  SlideToUnlockText,
  SlideToUnlockTrack,
} from "@/components/slide-to-unlock";

export function SlideToUnlockDemo2() {
  return (
    <>
      <SlideToUnlock
        className="bg-linear-to-b from-zinc-800 to-zinc-900"
        onUnlock={() => {
          const myPromise = new Promise((resolve) => {
            setTimeout(() => {
              resolve(true);
            }, 1000);
          });

          toast.promise(myPromise, {
            loading: "Connecting...",
            success: () => `Connected`,
            error: ({ message }) => `Error: ${message}`,
          });
        }}
      >
        <SlideToUnlockTrack>
          <SlideToUnlockText>
            {({ isDragging }) => (
              <ShimmeringText
                className="[--color:var(--color-zinc-600)] [--shimmering-color:var(--color-zinc-50)]"
                text="slide to answer"
                isStopped={isDragging}
              />
            )}
          </SlideToUnlockText>

          <SlideToUnlockHandle className="bg-linear-to-b from-emerald-500 to-emerald-700 text-white" />
        </SlideToUnlockTrack>
      </SlideToUnlock>
    </>
  );
}

```

### Custom Handle

```tsx
"use client";

import { toast } from "sonner";

import { ShimmeringText } from "@/components/shimmering-text";
import {
  SlideToUnlock,
  SlideToUnlockHandle,
  SlideToUnlockText,
  SlideToUnlockTrack,
} from "@/components/slide-to-unlock";

export function SlideToUnlockDemo3() {
  return (
    <SlideToUnlock
      className="w-[180px] rounded-full ring-0"
      handleWidth={40}
      onUnlock={() => toast.success("Stopped")}
    >
      <SlideToUnlockTrack>
        <SlideToUnlockText className="pl-0">
          {({ isDragging }) => (
            <ShimmeringText text="slide to stop" isStopped={isDragging} />
          )}
        </SlideToUnlockText>

        <SlideToUnlockHandle className="w-10 rounded-full dark:bg-zinc-700">
          <svg
            className="size-5 dark:text-white"
            xmlns="http://www.w3.org/2000/svg"
            viewBox="0 0 256 256"
          >
            <path
              d="M216,56V200a16,16,0,0,1-16,16H56a16,16,0,0,1-16-16V56A16,16,0,0,1,56,40H200A16,16,0,0,1,216,56Z"
              fill="currentColor"
            />
          </svg>
        </SlideToUnlockHandle>
      </SlideToUnlockTrack>
    </SlideToUnlock>
  );
}

```


Last updated on November 23, 2025

---
title: "Tips for Creating Beautiful Image Borders"
description: "Borders help images stand out, but fixed colors often clash with the content. Here's a simple tip to make borders look more natural and balanced."
last_updated: "September 13, 2025"
source: "https://mitr.dev/blog/tips-for-creating-beautiful-image-borders"
---

# Tips for Creating Beautiful Image Borders

Borders help images stand out, but fixed colors often clash with the content. Here's a simple tip to make borders look more natural and balanced.

## The common way: Solid color border

The most basic method is to add a solid border with a fixed color, like a soft black or white:

<FramedImage src="https://assets.chanhdai.com/images/blog/sample-1.webp?v=1" alt="Sample 1" />

```tsx
<img
  src="https://images.unsplash.com/photo-1503023345310-bd7c1de61c7d"
  className="rounded-xl border border-black/10 dark:border-white/10"
/>
```

This works fine, but if the image has many colors, the fixed border may not blend well and can look distracting.

## A better way: Border that adapts to the image

Instead of setting the border color directly, you can use a wrapper with a subtle `ring`. This way, the border adapts better to the image content, creating a softer, more natural look:

<FramedImage src="https://assets.chanhdai.com/images/blog/sample-2.webp?v=1" alt="Sample 2" />

```tsx
<div className="relative">
  <img
    src="https://images.unsplash.com/photo-1503023345310-bd7c1de61c7d"
    className="rounded-xl"
  />
  <div className="pointer-events-none absolute inset-0 rounded-xl ring-1 ring-black/10 ring-inset dark:ring-white/10" />
</div>
```

With this technique, the border is visible enough to separate the image from the background, but it won't steal focus from the image itself.

## Real-world Example

This technique is already used in real products. For example, here's a screenshot from the App Store, where images have subtle adaptive borders. The borders are soft enough to separate the image from the background, while still keeping the focus on the content inside.

<FramedImage src="https://assets.chanhdai.com/images/blog/sample-3.webp?v=1" alt="Sample 3" />

<FramedImage src="https://assets.chanhdai.com/images/blog/sample-4.webp?v=1" alt="Sample 4" />

## Conclusion

If you want your images to look cleaner and more polished, try switching from a fixed `border-color` to a `ring` that adapts to the image. It's a small change, but it makes the design feel much more modern and balanced.


Last updated on September 13, 2025

---
title: "Work Experience"
description: "Displays a list of work experiences with role details and durations."
last_updated: "June 16, 2025"
source: "https://mitr.dev/blog/work-experience-component"
---

# Work Experience

Displays a list of work experiences with role details and durations.

```tsx
import type { ExperienceItemType } from "@/components/work-experience";
import { WorkExperience } from "@/components/work-experience";

const WORK_EXPERIENCE: ExperienceItemType[] = [
  {
    id: "quaric",
    companyName: "Quaric Co., Ltd.",
    companyLogo: "https://assets.chanhdai.com/images/companies/quaric.svg",
    positions: [
      {
        id: "30d3a9fb-021d-452a-9d27-83655369b4b9",
        title: "Software Engineer",
        employmentPeriod: "03.2024 — present",
        employmentType: "Part-time",
        icon: "code",
        description: `- Integrated VNPAY-QR for secure transactions.
- Registered the e-commerce site with [online.gov.vn](https://online.gov.vn) for compliance.
- Developed online ordering to streamline purchases.
- Build and maintain ZaDark.com with Docusaurus, integrating AdSense.
- Develop and maintain the ZaDark extension for Zalo Web on Chrome, Safari, Edge, and Firefox — with 15,000+ active users via Chrome Web Store.`,
        skills: [
          "Next.js",
          "Strapi",
          "Auth0",
          "VNPAY-QR",
          "Docker",
          "NGINX",
          "Google Cloud",
          "Docusaurus",
          "Extension",
          "Research",
          "Project Management",
        ],
        isExpanded: true,
      },
      {
        id: "7586afb2-40e8-49c4-8983-2254c9446540",
        title: "Product Designer",
        employmentPeriod: "03.2024 — present",
        employmentType: "Part-time",
        icon: "design",
        description: `- Design UI/UX for Quaric Website with a seamless experience.
- Develop a Design System for consistency and efficiency.
- Create Quaric's brand identity, including logo and guidelines.`,
        skills: [
          "UI/UX Design",
          "UX Writing",
          "Design System",
          "Brand Design",
          "Figma",
        ],
      },
      {
        id: "991692c4-7d02-4666-8d31-933c4831768d",
        title: "Founder & Director",
        employmentPeriod: "03.2024 — present",
        employmentType: "Part-time",
        icon: "business",
        description: `- Lead and manage the company's strategy.
- Oversee technical teams and product development.
- Manage relationships with customers and partners.`,
        skills: ["Business Ownership", "Business Law", "Business Tax"],
      },
    ],
    isCurrentEmployer: true,
  },
];

export function WorkExperienceDemo() {
  return <WorkExperience className="w-full" experiences={WORK_EXPERIENCE} />;
}

```

## Installation

<CodeTabs>
  <TabsListInstallType />

  <TabsContent value="cli">
    ```bash
    npx shadcn add @ncdai/work-experience
    ```
  </TabsContent>

  <TabsContent value="manual">
    <Steps>
      <Step>Install the following dependencies</Step>

      ```bash
      npm install clsx tailwind-merge react-markdown lucide-react
      npm install -D @tailwindcss/typography tw-animate-css
      ```

      <Step>Add a cn helper</Step>

      ```ts title="lib/utils.ts" showLineNumbers
      import type { ClassValue } from "clsx";
      import { clsx } from "clsx";
      import { twMerge } from "tailwind-merge";

      export const cn = (...inputs: ClassValue[]) => {
        return twMerge(clsx(inputs));
      };

      ```

      <Step>Add plugin for tailwindcss</Step>

      ```css title="globals.css" showLineNumbers {2,3}
      @import "tailwindcss";
      @import "tw-animate-css";
      @plugin "@tailwindcss/typography";
      ```

      <Step>Copy and paste the following code into your project</Step>

      ```tsx title="components/work-experience.tsx" showLineNumbers
      import {
        BriefcaseBusinessIcon,
        ChevronsDownUpIcon,
        ChevronsUpDownIcon,
        CodeXmlIcon,
        DraftingCompassIcon,
        GraduationCapIcon,
      } from "lucide-react";
      import Image from "next/image";
      import React from "react";
      import ReactMarkdown from "react-markdown";

      import {
        Collapsible,
        CollapsibleContent,
        CollapsibleTrigger,
      } from "@/components/ui/collapsible";
      import { Separator } from "@/components/ui/separator";
      import { cn } from "@/lib/utils";

      const iconMap = {
        code: CodeXmlIcon,
        design: DraftingCompassIcon,
        business: BriefcaseBusinessIcon,
        education: GraduationCapIcon,
      } as const;

      /**
       * Represents the valid keys of the `iconMap` object, used to specify the type of icon
       * associated with an experience position.
       */
      export type ExperiencePositionIconType = keyof typeof iconMap;

      export type ExperiencePositionItemType = {
        /** Unique identifier for the position */
        id: string;
        /** The job title or position name */
        title: string;
        /** The period during which the position was held (e.g., "Jan 2020 - Dec 2021") */
        employmentPeriod: string;
        /** The type of employment (e.g., "Full-time", "Part-time", "Contract") */
        employmentType?: string;
        /** A brief description of the position or responsibilities */
        description?: string;
        /** An icon representing the position */
        icon?: ExperiencePositionIconType;
        /** A list of skills associated with the position */
        skills?: string[];
        /** Indicates if the position details are expanded in the UI */
        isExpanded?: boolean;
      };

      export type ExperienceItemType = {
        /** Unique identifier for the experience item */
        id: string;
        /** Name of the company where the experience was gained */
        companyName: string;
        /** URL or path to the company's logo image */
        companyLogo?: string;
        /** List of positions held at the company */
        positions: ExperiencePositionItemType[];
        /** Indicates if this is the user's current employer */
        isCurrentEmployer?: boolean;
      };

      export function WorkExperience({
        className,
        experiences,
      }: {
        className?: string;
        experiences: ExperienceItemType[];
      }) {
        return (
          <div className={cn("bg-background px-4", className)}>
            {experiences.map((experience) => (
              <ExperienceItem key={experience.id} experience={experience} />
            ))}
          </div>
        );
      }

      export function ExperienceItem({
        experience,
      }: {
        experience: ExperienceItemType;
      }) {
        return (
          <div className="space-y-4 py-4">
            <div className="not-prose flex items-center gap-3">
              <div
                className="flex size-6 shrink-0 items-center justify-center"
                aria-hidden
              >
                {experience.companyLogo ? (
                  <Image
                    src={experience.companyLogo}
                    alt={experience.companyName}
                    width={24}
                    height={24}
                    quality={100}
                    className="rounded-full"
                    unoptimized
                  />
                ) : (
                  <span className="flex size-2 rounded-full bg-zinc-300 dark:bg-zinc-600" />
                )}
              </div>

              <h3 className="text-lg leading-snug font-medium text-foreground">
                {experience.companyName}
              </h3>

              {experience.isCurrentEmployer && (
                <span className="relative flex items-center justify-center">
                  <span className="absolute inline-flex size-3 animate-ping rounded-full bg-info opacity-50" />
                  <span className="relative inline-flex size-2 rounded-full bg-info" />
                  <span className="sr-only">Current Employer</span>
                </span>
              )}
            </div>

            <div className="relative space-y-4 before:absolute before:left-3 before:h-full before:w-px before:bg-border">
              {experience.positions.map((position) => (
                <ExperiencePositionItem key={position.id} position={position} />
              ))}
            </div>
          </div>
        );
      }

      export function ExperiencePositionItem({
        position,
      }: {
        position: ExperiencePositionItemType;
      }) {
        const ExperienceIcon = iconMap[position.icon || "business"];

        return (
          <Collapsible defaultOpen={position.isExpanded} asChild>
            <div className="relative last:before:absolute last:before:h-full last:before:w-4 last:before:bg-background">
              <CollapsibleTrigger
                className={cn(
                  "group/experience not-prose block w-full text-left select-none",
                  "relative before:absolute before:-top-1 before:-right-1 before:-bottom-1.5 before:left-7 before:rounded-lg hover:before:bg-muted/50"
                )}
              >
                <div className="relative z-1 mb-1 flex items-center gap-3">
                  <div
                    className="flex size-6 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground"
                    aria-hidden
                  >
                    <ExperienceIcon className="size-4" />
                  </div>

                  <h4 className="flex-1 text-base font-medium text-balance text-foreground">
                    {position.title}
                  </h4>

                  <div
                    className="shrink-0 text-muted-foreground [&_svg]:size-4"
                    aria-hidden
                  >
                    <ChevronsDownUpIcon className="hidden group-data-[state=open]/experience:block" />
                    <ChevronsUpDownIcon className="hidden group-data-[state=closed]/experience:block" />
                  </div>
                </div>

                <div className="relative z-1 flex items-center gap-2 pl-9 text-sm text-muted-foreground">
                  {position.employmentType && (
                    <>
                      <dl>
                        <dt className="sr-only">Employment Type</dt>
                        <dd>{position.employmentType}</dd>
                      </dl>

                      <Separator
                        className="data-[orientation=vertical]:h-4"
                        orientation="vertical"
                      />
                    </>
                  )}

                  <dl>
                    <dt className="sr-only">Employment Period</dt>
                    <dd>{position.employmentPeriod}</dd>
                  </dl>
                </div>
              </CollapsibleTrigger>

              <CollapsibleContent className="overflow-hidden duration-300 data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down">
                {position.description && (
                  <Prose className="pt-2 pl-9">
                    <ReactMarkdown>{position.description}</ReactMarkdown>
                  </Prose>
                )}

                {Array.isArray(position.skills) && position.skills.length > 0 && (
                  <ul className="not-prose flex flex-wrap gap-1.5 pt-2 pl-9">
                    {position.skills.map((skill, index) => (
                      <li key={index} className="flex">
                        <Skill>{skill}</Skill>
                      </li>
                    ))}
                  </ul>
                )}
              </CollapsibleContent>
            </div>
          </Collapsible>
        );
      }

      function Prose({ className, ...props }: React.ComponentProps<"div">) {
        return (
          <div
            className={cn(
              "prose prose-sm max-w-none font-mono text-foreground prose-zinc dark:prose-invert",
              "prose-a:font-medium prose-a:wrap-break-word prose-a:text-foreground prose-a:underline prose-a:underline-offset-4",
              "prose-code:rounded-md prose-code:border prose-code:bg-muted/50 prose-code:px-[0.3rem] prose-code:py-[0.2rem] prose-code:text-sm prose-code:font-normal prose-code:before:content-none prose-code:after:content-none",
              className
            )}
            {...props}
          />
        );
      }

      function Skill({ className, ...props }: React.ComponentProps<"span">) {
        return (
          <span
            className={cn(
              "inline-flex items-center rounded-lg border bg-muted/50 px-1.5 py-0.5 font-mono text-xs text-muted-foreground",
              className
            )}
            {...props}
          />
        );
      }

      ```

      <Step>Update the import paths to match your project setup</Step>
    </Steps>
  </TabsContent>
</CodeTabs>

## Usage

```tsx
import { WorkExperience } from "@/components/ncdai/work-experience";
import type { ExperienceItemType } from "@/components/ncdai/work-experience";
```

<CodeCollapsibleWrapper>
  ```tsx
  const WORK_EXPERIENCE: ExperienceItemType[] = [
    {
      id: "1",
      companyName: "Acme Inc.",
      companyLogo: "https://assets.chanhdai.com/images/companies/quaric.svg",
      isCurrentEmployer: true,
      positions: [
        {
          id: "1-1",
          title: "Senior Software Engineer",
          employmentPeriod: "Jan 2022 - Present",
          employmentType: "Full-time",
          description:
            "Leading a team of developers to build scalable web applications.",
          icon: "code",
          skills: ["JavaScript", "React", "Node.js"],
          isExpanded: true,
        },
        {
          id: "1-2",
          title: "Software Engineer",
          employmentPeriod: "Jan 2020 - Dec 2021",
          employmentType: "Full-time",
          description:
            "Developed and maintained web applications using modern technologies.",
          icon: "code",
          skills: ["HTML", "CSS", "JavaScript"],
        },
      ],
    },
  ];
  ```
</CodeCollapsibleWrapper>

```tsx
<WorkExperience experiences={WORK_EXPERIENCE} />
```

## Props

Props for the `WorkExperience` component:

| Prop          | Type                   | Description                                                                                          |
| ------------- | ---------------------- | ---------------------------------------------------------------------------------------------------- |
| `experiences` | `ExperienceItemType[]` | An array of work experience items to display. Each item includes company details and positions held. |
| `className`   | `string`               | Additional CSS classes to apply to the root element.                                                 |

## Types

```ts
type ExperiencePositionIconType = "design" | "code" | "business" | "education";

type ExperiencePositionItemType = {
  /** Unique identifier for the position */
  id: string;
  /** The job title or position name */
  title: string;
  /** The period during which the position was held (e.g., "Jan 2020 - Dec 2021") */
  employmentPeriod: string;
  /** The type of employment (e.g., "Full-time", "Part-time", "Contract") */
  employmentType?: string;
  /** A brief description of the position or responsibilities */
  description?: string;
  /** An icon representing the position */
  icon?: ExperiencePositionIconType;
  /** A list of skills associated with the position */
  skills?: string[];
  /** Indicates if the position details are expanded in the UI */
  isExpanded?: boolean;
};

type ExperienceItemType = {
  /** Unique identifier for the experience item */
  id: string;
  /** Name of the company where the experience was gained */
  companyName: string;
  /** URL or path to the company's logo image */
  companyLogo?: string;
  /** List of positions held at the company */
  positions: ExperiencePositionItemType[];
  /** Indicates if this is the user's current employer */
  isCurrentEmployer?: boolean;
};
```

## Examples

```tsx
import type { ExperienceItemType } from "@/components/work-experience";
import { WorkExperience } from "@/components/work-experience";

const WORK_EXPERIENCE: ExperienceItemType[] = [
  {
    id: "quaric",
    companyName: "Quaric Co., Ltd.",
    companyLogo: "https://assets.chanhdai.com/images/companies/quaric.svg",
    positions: [
      {
        id: "30d3a9fb-021d-452a-9d27-83655369b4b9",
        title: "Software Engineer",
        employmentPeriod: "03.2024 — present",
        employmentType: "Part-time",
        icon: "code",
        description: `- Integrated VNPAY-QR for secure transactions.
- Registered the e-commerce site with [online.gov.vn](https://online.gov.vn) for compliance.
- Developed online ordering to streamline purchases.
- Build and maintain ZaDark.com with Docusaurus, integrating AdSense.
- Develop and maintain the ZaDark extension for Zalo Web on Chrome, Safari, Edge, and Firefox — with 15,000+ active users via Chrome Web Store.`,
        skills: [
          "Next.js",
          "Strapi",
          "Auth0",
          "VNPAY-QR",
          "Docker",
          "NGINX",
          "Google Cloud",
          "Docusaurus",
          "Extension",
          "Research",
          "Project Management",
        ],
        isExpanded: true,
      },
      {
        id: "7586afb2-40e8-49c4-8983-2254c9446540",
        title: "Product Designer",
        employmentPeriod: "03.2024 — present",
        employmentType: "Part-time",
        icon: "design",
        description: `- Design UI/UX for Quaric Website with a seamless experience.
- Develop a Design System for consistency and efficiency.
- Create Quaric's brand identity, including logo and guidelines.`,
        skills: [
          "UI/UX Design",
          "UX Writing",
          "Design System",
          "Brand Design",
          "Figma",
        ],
      },
      {
        id: "991692c4-7d02-4666-8d31-933c4831768d",
        title: "Founder & Director",
        employmentPeriod: "03.2024 — present",
        employmentType: "Part-time",
        icon: "business",
        description: `- Lead and manage the company's strategy.
- Oversee technical teams and product development.
- Manage relationships with customers and partners.`,
        skills: ["Business Ownership", "Business Law", "Business Tax"],
      },
    ],
    isCurrentEmployer: true,
  },
];

export function WorkExperienceDemo() {
  return <WorkExperience className="w-full" experiences={WORK_EXPERIENCE} />;
}

```


Last updated on June 16, 2025

---
title: "React Wheel Picker"
description: "iOS-like wheel picker for React with smooth inertia scrolling and infinite loop support."
last_updated: "October 9, 2025"
source: "https://mitr.dev/blog/react-wheel-picker"
---

# React Wheel Picker

iOS-like wheel picker for React with smooth inertia scrolling and infinite loop support.

```tsx
import type { WheelPickerOption } from "@/components/wheel-picker";
import { WheelPicker, WheelPickerWrapper } from "@/components/wheel-picker";

const createArray = (length: number, add = 0): WheelPickerOption<number>[] =>
  Array.from({ length }, (_, i) => {
    const value = i + add;
    return {
      label: value.toString().padStart(2, "0"),
      value: value,
    };
  });

const hourOptions = createArray(12, 1);
const minuteOptions = createArray(60);
const meridiemOptions: WheelPickerOption[] = [
  { label: "AM", value: "AM" },
  { label: "PM", value: "PM" },
];

export function WheelPickerDemo() {
  return (
    <div className="w-56">
      <WheelPickerWrapper>
        <WheelPicker options={hourOptions} defaultValue={9} infinite />
        <WheelPicker options={minuteOptions} defaultValue={41} infinite />
        <WheelPicker options={meridiemOptions} defaultValue="AM" />
      </WheelPickerWrapper>
    </div>
  );
}

```

## About

The Wheel Picker component is built on top of [React Wheel Picker](https://react-wheel-picker.chanhdai.com).

* 📱 Natural touch scrolling with smooth inertia effect
* 🖱️ Mouse drag and scroll support for desktop
* 🔄 Infinite loop scrolling
* 🎨 Unstyled components for complete style customization
* ⚡️ Easy installation via shadcn CLI

## Installation

<CodeTabs>
  <TabsListInstallType />

  <TabsContent value="cli">
    ```bash
    npx shadcn add @ncdai/wheel-picker
    ```
  </TabsContent>

  <TabsContent value="manual">
    <Steps>
      <Step>Install the following dependencies</Step>

      ```bash
      npm install @ncdai/react-wheel-picker clsx tailwind-merge
      ```

      <Step>Add a cn helper</Step>

      ```ts title="lib/utils.ts" showLineNumbers
      import type { ClassValue } from "clsx";
      import { clsx } from "clsx";
      import { twMerge } from "tailwind-merge";

      export const cn = (...inputs: ClassValue[]) => {
        return twMerge(clsx(inputs));
      };

      ```

      <Step>Copy and paste the following code into your project</Step>

      ```tsx title="components/wheel-picker.tsx" showLineNumbers
      import "@ncdai/react-wheel-picker/style.css";

      import * as WheelPickerPrimitive from "@ncdai/react-wheel-picker";

      import { cn } from "@/lib/utils";

      type WheelPickerValue = WheelPickerPrimitive.WheelPickerValue;

      type WheelPickerOption<T extends WheelPickerValue = string> =
        WheelPickerPrimitive.WheelPickerOption<T>;

      type WheelPickerClassNames = WheelPickerPrimitive.WheelPickerClassNames;

      function WheelPickerWrapper({
        className,
        ...props
      }: React.ComponentProps<typeof WheelPickerPrimitive.WheelPickerWrapper>) {
        return (
          <WheelPickerPrimitive.WheelPickerWrapper
            className={cn(
              "w-56 rounded-lg border border-zinc-200 bg-white px-1 shadow-xs dark:border-zinc-700/80 dark:bg-zinc-900",
              "*:data-rwp:first:*:data-rwp-highlight-wrapper:rounded-s-md",
              "*:data-rwp:last:*:data-rwp-highlight-wrapper:rounded-e-md",
              className
            )}
            {...props}
          />
        );
      }

      function WheelPicker<T extends WheelPickerValue = string>({
        classNames,
        ...props
      }: WheelPickerPrimitive.WheelPickerProps<T>) {
        return (
          <WheelPickerPrimitive.WheelPicker
            classNames={{
              optionItem: "text-zinc-400 dark:text-zinc-500",
              highlightWrapper:
                "bg-zinc-100 text-zinc-950 dark:bg-zinc-800 dark:text-zinc-50",
              ...classNames,
            }}
            {...props}
          />
        );
      }

      export { WheelPicker, WheelPickerWrapper };
      export type { WheelPickerClassNames, WheelPickerOption };

      ```

      <Step>Update the import paths to match your project setup</Step>
    </Steps>
  </TabsContent>
</CodeTabs>

## Usage

```tsx
import {
  WheelPicker,
  WheelPickerWrapper,
  type WheelPickerOption,
} from "@/components/ncdai/wheel-picker";
```

```tsx
const options: WheelPickerOption[] = [
  {
    label: "React",
    value: "react",
  },
  {
    label: "Vue",
    value: "vue",
  },
  {
    label: "Angular",
    value: "angular",
  },
  {
    label: "Svelte",
    value: "svelte",
  },
];

export function WheelPickerDemo() {
  const [value, setValue] = useState("react");

  return (
    <WheelPickerWrapper>
      <WheelPicker options={options} value={value} onValueChange={setValue} />
    </WheelPickerWrapper>
  );
}
```

See the [React Wheel Picker](https://react-wheel-picker.chanhdai.com/docs/getting-started) documentation for more information.

## Examples

### Multiple Pickers, Infinite Loop

```tsx
import type { WheelPickerOption } from "@/components/wheel-picker";
import { WheelPicker, WheelPickerWrapper } from "@/components/wheel-picker";

const createArray = (length: number, add = 0): WheelPickerOption<number>[] =>
  Array.from({ length }, (_, i) => {
    const value = i + add;
    return {
      label: value.toString().padStart(2, "0"),
      value: value,
    };
  });

const hourOptions = createArray(12, 1);
const minuteOptions = createArray(60);
const meridiemOptions: WheelPickerOption[] = [
  { label: "AM", value: "AM" },
  { label: "PM", value: "PM" },
];

export function WheelPickerDemo() {
  return (
    <div className="w-56">
      <WheelPickerWrapper>
        <WheelPicker options={hourOptions} defaultValue={9} infinite />
        <WheelPicker options={minuteOptions} defaultValue={41} infinite />
        <WheelPicker options={meridiemOptions} defaultValue="AM" />
      </WheelPickerWrapper>
    </div>
  );
}

```

### React Hook Form

```tsx
"use client";

import { zodResolver } from "@hookform/resolvers/zod";
import type { SubmitHandler } from "react-hook-form";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";

import { Button } from "@/components/ui/button";
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form";
import type { WheelPickerOption } from "@/components/wheel-picker";
import { WheelPicker, WheelPickerWrapper } from "@/components/wheel-picker";

const formSchema = z.object({
  framework: z.string(),
});

type FormSchema = z.infer<typeof formSchema>;

const options: WheelPickerOption[] = [
  {
    label: "Vite",
    value: "vite",
  },
  {
    label: "Laravel",
    value: "laravel",
  },
  {
    label: "React Router",
    value: "react-router",
  },
  {
    label: "Next.js",
    value: "nextjs",
  },
  {
    label: "Astro",
    value: "astro",
  },
  {
    label: "TanStack Start",
    value: "tanstack-start",
  },
  {
    label: "TanStack Router",
    value: "tanstack-router",
  },
  {
    label: "Gatsby",
    value: "gatsby",
  },
];

export function WheelPickerFormDemo() {
  const form = useForm<FormSchema>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      framework: "nextjs",
    },
  });

  const onSubmit: SubmitHandler<FormSchema> = (values) => {
    toast("You submitted the following values:", {
      description: (
        <pre className="mt-2 w-80 rounded-lg bg-zinc-950 p-4">
          <code className="text-white">{JSON.stringify(values, null, 2)}</code>
        </pre>
      ),
    });
  };

  return (
    <Form {...form}>
      <form
        onSubmit={form.handleSubmit(onSubmit)}
        className="w-56 max-w-full space-y-4"
      >
        <FormField
          control={form.control}
          name="framework"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Framework</FormLabel>

              <FormControl>
                <WheelPickerWrapper>
                  <WheelPicker
                    options={options}
                    value={field.value}
                    onValueChange={field.onChange}
                  />
                </WheelPickerWrapper>
              </FormControl>

              <FormMessage />
            </FormItem>
          )}
        />

        <div className="flex justify-center">
          <Button type="submit">Submit</Button>
        </div>
      </form>
    </Form>
  );
}

```


Last updated on October 9, 2025

---
title: "Theme Switcher"
description: "A theme switcher component for Next.js apps with next-themes and Tailwind CSS, supporting system, light, and dark modes."
last_updated: "April 11, 2025"
source: "https://mitr.dev/blog/theme-switcher-component"
---

# Theme Switcher

A theme switcher component for Next.js apps with next-themes and Tailwind CSS, supporting system, light, and dark modes.

```tsx
import { ThemeSwitcher } from "@/components/theme-switcher";

export function ThemeSwitcherDemo() {
  return <ThemeSwitcher />;
}

```

## Installation

<CodeTabs>
  <TabsListInstallType />

  <TabsContent value="cli">
    ```bash
    npx shadcn add @ncdai/theme-switcher
    ```
  </TabsContent>

  <TabsContent value="manual">
    <Steps>
      <Step>Install the following dependencies</Step>

      ```bash
      npm install next-themes lucide-react motion clsx tailwind-merge
      ```

      <Step>Add a cn helper</Step>

      ```ts title="lib/utils.ts" showLineNumbers
      import type { ClassValue } from "clsx";
      import { clsx } from "clsx";
      import { twMerge } from "tailwind-merge";

      export const cn = (...inputs: ClassValue[]) => {
        return twMerge(clsx(inputs));
      };

      ```

      <Step>Copy and paste the following code into your project</Step>

      ```tsx title="components/ncdai/theme-switcher.tsx" showLineNumbers
      "use client";

      import { MonitorIcon, MoonStarIcon, SunIcon } from "lucide-react";
      import { motion } from "motion/react";
      import { useTheme } from "next-themes";
      import type { JSX } from "react";
      import { useSyncExternalStore } from "react";

      import { cn } from "@/lib/utils";

      function ThemeOption({
        icon,
        value,
        isActive,
        onClick,
      }: {
        icon: JSX.Element;
        value: string;
        isActive?: boolean;
        onClick: (value: string) => void;
      }) {
        return (
          <button
            className={cn(
              "relative flex size-8 cursor-default items-center justify-center rounded-full transition-[color] [&_svg]:size-4",
              isActive
                ? "text-zinc-950 dark:text-zinc-50"
                : "text-zinc-400 hover:text-zinc-950 dark:text-zinc-500 dark:hover:text-zinc-50"
            )}
            role="radio"
            aria-checked={isActive}
            aria-label={`Switch to ${value} theme`}
            onClick={() => onClick(value)}
          >
            {icon}

            {isActive && (
              <motion.div
                layoutId="theme-option"
                transition={{ type: "spring", bounce: 0.3, duration: 0.6 }}
                className="absolute inset-0 rounded-full border border-zinc-200 dark:border-zinc-700"
              />
            )}
          </button>
        );
      }

      const THEME_OPTIONS = [
        {
          icon: <MonitorIcon />,
          value: "system",
        },
        {
          icon: <SunIcon />,
          value: "light",
        },
        {
          icon: <MoonStarIcon />,
          value: "dark",
        },
      ];

      function ThemeSwitcher() {
        const { theme, setTheme } = useTheme();

        const isMounted = useSyncExternalStore(
          () => () => {},
          () => true,
          () => false
        );

        if (!isMounted) {
          return <div className="flex h-8 w-24" />;
        }

        return (
          <motion.div
            key={String(isMounted)}
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            transition={{ duration: 0.3 }}
            className="inline-flex items-center overflow-hidden rounded-full bg-white ring-1 ring-zinc-200 ring-inset dark:bg-zinc-950 dark:ring-zinc-700"
            role="radiogroup"
          >
            {THEME_OPTIONS.map((option) => (
              <ThemeOption
                key={option.value}
                icon={option.icon}
                value={option.value}
                isActive={theme === option.value}
                onClick={setTheme}
              />
            ))}
          </motion.div>
        );
      }

      export { ThemeSwitcher };

      ```

      <Step>Update the import paths to match your project setup</Step>
    </Steps>
  </TabsContent>
</CodeTabs>

## Usage

```tsx
import { ThemeSwitcher } from "@/components/ncdai/theme-switcher";
```

```tsx
<ThemeSwitcher />
```

## References

* [https://ui.shadcn.com/docs/registry](https://ui.shadcn.com/docs/registry)
* [https://ui.shadcn.com/docs/dark-mode/next](https://ui.shadcn.com/docs/dark-mode/next)
* [https://github.com/pacocoursey/next-themes](https://github.com/pacocoursey/next-themes)
* [https://motion.dev/docs/react-motion-component](https://motion.dev/docs/react-motion-component)


Last updated on April 11, 2025

---
title: "Writing Effect inspired by Apple"
description: "Create a "Xin chào" and "Hello" writing effect inspired by Apple using Motion for React."
last_updated: "April 11, 2025"
source: "https://mitr.dev/blog/writing-effect-inspired-by-apple"
---

# Writing Effect inspired by Apple

Create a "Xin chào" and "Hello" writing effect inspired by Apple using Motion for React.

```tsx
import { AppleHelloVietnameseEffect } from "@/components/apple-hello-effect";

export function VietnameseDemo() {
  return <AppleHelloVietnameseEffect />;
}

```

```tsx
import { AppleHelloEnglishEffect } from "@/components/apple-hello-effect";

export function EnglishDemo() {
  return <AppleHelloEnglishEffect />;
}

```

## Installation

<CodeTabs>
  <TabsListInstallType />

  <TabsContent value="cli">
    ```bash
    npx shadcn add @ncdai/apple-hello-effect
    ```
  </TabsContent>

  <TabsContent value="manual">
    <Steps>
      <Step>Install the following dependencies</Step>

      ```bash
      npm install motion clsx tailwind-merge
      ```

      <Step>Add a cn helper</Step>

      ```ts title="lib/utils.ts" showLineNumbers
      import type { ClassValue } from "clsx";
      import { clsx } from "clsx";
      import { twMerge } from "tailwind-merge";

      export const cn = (...inputs: ClassValue[]) => {
        return twMerge(clsx(inputs));
      };

      ```

      <Step>Copy and paste the following code into your project</Step>

      ```tsx title="components/apple-hello-effect.tsx" showLineNumbers
      "use client";

      import type { TargetAndTransition } from "motion/react";
      import { motion } from "motion/react";

      import { cn } from "@/lib/utils";

      const initialProps: TargetAndTransition = {
        pathLength: 0,
        opacity: 0,
      };

      const animateProps: TargetAndTransition = {
        pathLength: 1,
        opacity: 1,
      };

      type Props = React.ComponentProps<typeof motion.svg> & {
        speed?: number;
        onAnimationComplete?: () => void;
      };

      function AppleHelloVietnameseEffect({
        className,
        speed = 1,
        onAnimationComplete,
        ...props
      }: Props) {
        const calc = (x: number) => x * speed;

        return (
          <motion.svg
            className={cn("h-20", className)}
            xmlns="http://www.w3.org/2000/svg"
            viewBox="0 0 1009 200"
            fill="none"
            stroke="currentColor"
            strokeWidth="14.8883"
            initial={{ opacity: 1 }}
            exit={{ opacity: 0 }}
            transition={{ duration: 0.5 }}
            {...props}
          >
            <title>xin chào</title>

            {/* x1 */}
            <motion.path
              d="M102.233 96.2277C75.6823 127.245 45.1612 158.759 11.4143 190.521"
              style={{ strokeLinecap: "round" }}
              initial={initialProps}
              animate={animateProps}
              transition={{
                duration: calc(0.3),
                ease: "easeInOut",
                opacity: { duration: 0.15 },
              }}
            />

            {/* x2 */}
            <motion.path
              d="M7.69214 116.575C9.67725 105.16 16.8733 95.7311 28.5358 95.7311C40.4465 95.7311 46.8981 105.408 53.3497 124.019C56.7409 133.283 60.1322 142.547 63.5234 151.81C73.689 179.58 81.1988 191.513 100.855 191.513C128.722 191.513 154.043 159.148 161.595 118.502C162.929 111.321 164.774 103.736 166.043 96.2273"
              style={{ strokeLinecap: "round" }}
              initial={initialProps}
              animate={animateProps}
              transition={{
                duration: calc(0.7),
                ease: "easeInOut",
                delay: calc(0.4),
                opacity: { duration: 0.35, delay: calc(0.4) },
              }}
            />

            {/* i */}
            <motion.path
              d="M166.043 96.2273C163.191 113.101 160.565 126.997 158.92 139.404C157.989 147.592 157.544 154.54 157.596 161.488C157.729 179.354 164.764 191.513 182.695 191.513C209.39 191.513 236.181 159.123 243.73 118.5C245.064 111.321 247.012 103.759 248.139 96.2273"
              style={{ strokeLinecap: "round" }}
              initial={initialProps}
              animate={animateProps}
              transition={{
                duration: calc(0.5),
                ease: "easeOut",
                delay: calc(1),
                opacity: { duration: 0.25, delay: calc(1) },
              }}
            />

            {/* n1 */}
            <motion.path
              d="M248.139 96.2278C243.424 127.741 239.454 158.759 234.491 190.272"
              style={{ strokeLinecap: "round" }}
              initial={initialProps}
              animate={animateProps}
              transition={{
                duration: calc(0.3),
                ease: "easeOut",
                delay: calc(1.5),
                opacity: { duration: 0.15, delay: calc(1.5) },
              }}
            />

            {/* n2 */}
            <motion.path
              d="M237.873 167.951C244.704 121.32 265.508 94.2422 290.322 94.2422C307.692 94.2422 316.625 106.153 315.136 123.026C313.896 135.681 309.677 150.322 308.685 162.729C307.444 179.85 316.499 191.513 330.769 191.513C348.722 191.513 359.309 179.314 364.143 165.965"
              style={{ strokeLinecap: "round" }}
              initial={initialProps}
              animate={animateProps}
              transition={{
                duration: calc(0.9),
                ease: "easeOut",
                delay: calc(1.8),
                opacity: { duration: 0.45, delay: calc(1.8) },
              }}
            />

            {/* c, h1 */}
            <motion.path
              d="M535.91 109.876C531.265 100.446 520.943 93.4984 505.459 93.4984C476.516 93.4984 462.044 117.816 462.044 143.374C462.044 171.503 482.265 192.506 511.307 192.506C559.762 192.506 592.902 136.708 621.581 97.8807C640.764 71.9101 649.874 49.2359 650.372 31.1674C650.62 17.7684 644.168 7.60362 632.01 7.60362C618.61 7.60362 610.173 17.7684 604.963 41.1011C599.255 66.7441 595.037 96.1684 584.367 190.521"
              style={{ strokeLinecap: "round" }}
              initial={initialProps}
              animate={animateProps}
              transition={{
                duration: calc(1.1),
                ease: "easeInOut",
                delay: calc(2.6),
                opacity: { duration: 0.55, delay: calc(2.6) },
              }}
            />

            {/* h2 */}
            <motion.path
              d="M585.413 181.299C590.677 135.025 611.663 98.2125 638.213 98.2125C654.094 98.2125 664.187 110.868 661.321 128.982C659.708 139.652 656.794 152.059 655.128 164.217C653.102 179.602 658.89 191.513 676.813 191.513C702.178 191.513 717.375 164.077 725.613 135.196"
              style={{ strokeLinecap: "round" }}
              initial={initialProps}
              animate={animateProps}
              transition={{
                duration: calc(1),
                ease: "easeInOut",
                delay: calc(3.6),
                opacity: { duration: 0.5, delay: calc(3.6) },
              }}
            />

            {/* a1 */}
            <motion.path
              d="M803.871 112.995C799.007 101.8 788.666 94.2423 772.207 94.2423C744.912 94.2423 724.398 121.538 723.052 150.818C721.878 177.617 734.244 192.681 751.857 192.505C776.858 192.255 795.234 167.699 803.437 115.742C804.449 109.332 805.498 102.638 806.51 96.2274"
              style={{ strokeLinecap: "round" }}
              initial={initialProps}
              animate={animateProps}
              transition={{
                duration: calc(0.8),
                ease: "easeOut",
                delay: calc(4.6),
                opacity: { duration: 0.4, delay: calc(4.6) },
              }}
            />

            {/* a2, o */}
            <motion.path
              d="M806.51 96.2274C805.486 102.73 804.461 109.232 803.436 115.735C798.955 144.175 796.887 155.395 797.109 162.729C797.628 179.85 803.785 191.513 820.064 191.513C842.563 191.513 860.966 164.721 870.266 138.289C879.653 111.612 891.315 94.9867 915.633 94.9867C935.732 94.9867 951.613 109.875 951.613 137.915C951.613 168.932 931.489 192.257 906.059 192.505C883.681 192.753 868.983 174.639 870.471 147.344C872.208 117.071 890.571 94.9867 914.64 94.9867C928.536 94.9867 940.207 101.164 949.38 107.89C974.247 126.031 993.407 114.82 1000.74 96.8832"
              style={{ strokeLinecap: "round" }}
              initial={initialProps}
              animate={animateProps}
              transition={{
                duration: calc(1.5),
                ease: "easeOut",
                delay: calc(5.4),
                opacity: { duration: 0.75, delay: calc(5.4) },
              }}
            />

            {/* sign */}
            <motion.path
              className="stroke-yellow-400"
              d="M763.027 19.3039C768.734 34.6886 780.397 48.3362 792.059 55.5322"
              style={{ strokeLinecap: "round" }}
              initial={initialProps}
              animate={animateProps}
              transition={{
                duration: calc(0.8),
                ease: "easeInOut",
                delay: calc(7),
                opacity: { duration: 0.4, delay: calc(7) },
              }}
              onAnimationComplete={onAnimationComplete}
            />
          </motion.svg>
        );
      }

      function AppleHelloEnglishEffect({
        className,
        speed = 1,
        onAnimationComplete,
        ...props
      }: Props) {
        const calc = (x: number) => x * speed;

        return (
          <motion.svg
            className={cn("h-20", className)}
            xmlns="http://www.w3.org/2000/svg"
            viewBox="0 0 638 200"
            fill="none"
            stroke="currentColor"
            strokeWidth="14.8883"
            initial={{ opacity: 1 }}
            exit={{ opacity: 0 }}
            transition={{ duration: 0.5 }}
            {...props}
          >
            <title>hello</title>

            {/* h1 */}
            <motion.path
              d="M8.69214 166.553C36.2393 151.239 61.3409 131.548 89.8191 98.0295C109.203 75.1488 119.625 49.0228 120.122 31.0026C120.37 17.6036 113.836 7.43883 101.759 7.43883C88.3598 7.43883 79.9231 17.6036 74.7122 40.9363C69.005 66.5793 64.7866 96.0036 54.1166 190.356"
              style={{ strokeLinecap: "round" }}
              initial={initialProps}
              animate={animateProps}
              transition={{
                duration: calc(0.8),
                ease: "easeInOut",
                opacity: { duration: 0.4 },
              }}
            />

            {/* h2, ello */}
            <motion.path
              d="M55.1624 181.135C60.6251 133.114 81.4118 98.0479 107.963 98.0479C123.844 98.0479 133.937 110.703 131.071 128.817C129.457 139.487 127.587 150.405 125.408 163.06C122.869 178.941 130.128 191.348 152.122 191.348C184.197 191.348 219.189 173.523 237.097 145.915C243.198 136.509 245.68 128.073 245.928 119.884C246.176 104.996 237.739 93.8296 222.851 93.8296C203.992 93.8296 189.6 115.17 189.6 142.465C189.6 171.745 205.481 192.341 239.208 192.341C285.066 192.341 335.86 137.292 359.199 75.8585C365.788 58.513 368.26 42.4065 368.26 31.1512C368.26 17.8057 364.042 7.55823 352.131 7.55823C340.469 7.55823 332.777 16.6141 325.829 30.9129C317.688 47.4967 311.667 71.4162 309.203 98.4549C303 166.301 316.896 191.348 349.936 191.348C390 191.348 434.542 135.534 457.286 75.6686C463.803 58.513 466.275 42.4065 466.275 31.1512C466.275 17.8057 462.057 7.55823 450.146 7.55823C438.484 7.55823 430.792 16.6141 423.844 30.9129C415.703 47.4967 409.682 71.4162 407.218 98.4549C401.015 166.301 414.911 191.348 444.416 191.348C473.874 191.348 489.877 165.67 499.471 138.402C508.955 111.447 520.618 94.8221 544.935 94.8221C565.035 94.8221 580.916 109.71 580.916 137.75C580.916 168.768 560.792 192.093 535.362 192.341C512.984 192.589 498.285 174.475 499.774 147.179C501.511 116.907 519.873 94.8221 543.943 94.8221C557.839 94.8221 569.51 100.999 578.682 107.725C603.549 125.866 622.709 114.656 630.047 96.7186"
              style={{ strokeLinecap: "round" }}
              initial={initialProps}
              animate={animateProps}
              transition={{
                duration: calc(2.8),
                ease: "easeInOut",
                delay: calc(0.7),
                opacity: { duration: 0.7, delay: calc(0.7) },
              }}
              onAnimationComplete={onAnimationComplete}
            />
          </motion.svg>
        );
      }

      export { AppleHelloEnglishEffect, AppleHelloVietnameseEffect };

      ```

      <Step>Update the import paths to match your project setup</Step>
    </Steps>
  </TabsContent>
</CodeTabs>

## Usage

```tsx
import {
  AppleHelloVietnameseEffect,
  AppleHelloEnglishEffect,
} from "@/components/ncdai/apple-hello-effect";
```

```tsx
<AppleHelloVietnameseEffect />
<AppleHelloEnglishEffect />
```

## Props

| Prop                  | Type         | Default     | Description                                                                                |
| --------------------- | ------------ | ----------- | ------------------------------------------------------------------------------------------ |
| `speed`               | `number`     | 1           | A multiplier that affects the animation duration. Higher values make the animation slower. |
| `onAnimationComplete` | `() => void` | `undefined` | Callback function triggered when the animation completes.                                  |

## Acknowledgments

* Official Apple Hello Lettering, extracted from macOS Sonoma in SVG Form: [https://www.figma.com/community/file/1414773009964314315/official-apple-hello-lettering](https://www.figma.com/community/file/1414773009964314315/official-apple-hello-lettering)
* A modern animation library for JavaScript and React: [https://motion.dev](https://motion.dev)
* Run your own component registry: [https://ui.shadcn.com/docs/registry](https://ui.shadcn.com/docs/registry)

## References

* [https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/pathLength](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/pathLength)
* [https://motion.dev/docs/react-motion-component](https://motion.dev/docs/react-motion-component)


Last updated on April 11, 2025

---
title: "Awesome Terminal — iTerm2 + Zsh + Oh My Zsh + Amazon Q"
description: "Optimize your terminal with iTerm2, Zsh, Oh My Zsh, and Amazon Q - a guide to installation, theme customization, plugins, and configuration for an enhanced command-line experience."
last_updated: "April 2, 2025"
source: "https://mitr.dev/blog/awesome-terminal"
---

# Awesome Terminal — iTerm2 + Zsh + Oh My Zsh + Amazon Q

Optimize your terminal with iTerm2, Zsh, Oh My Zsh, and Amazon Q - a guide to installation, theme customization, plugins, and configuration for an enhanced command-line experience.

## Install iTerm2

### Download

[https://iterm2.com/downloads.html](https://iterm2.com/downloads.html)

### Custom Theme

[https://draculatheme.com/iterm](https://draculatheme.com/iterm)

## Install Oh My Zsh

Reference: [https://ohmyz.sh/#install](https://ohmyz.sh/#install)

```bash
sh -c "$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
```

## Install Oh My Zsh Theme

Reference: [https://github.com/ncdai/ncdai.zsh-theme](https://github.com/ncdai/ncdai.zsh-theme)

```bash
git clone git@github.com:ncdai/ncdai.zsh-theme.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/themes/ncdai.zsh-theme
```

**Activate the theme**

```bash title="~/.zshrc"
ZSH_THEME="ncdai.zsh-theme/ncdai"
```

## Install Oh My Zsh Plugins

### zsh-z

Reference: [https://github.com/agkozak/zsh-z#installation](https://github.com/agkozak/zsh-z#installation)

```bash
git clone https://github.com/agkozak/zsh-z $ZSH_CUSTOM/plugins/zsh-z
```

### zsh-syntax-highlighting

Reference: [https://github.com/zsh-users/zsh-syntax-highlighting/blob/master/INSTALL.md](https://github.com/zsh-users/zsh-syntax-highlighting/blob/master/INSTALL.md)

```bash
git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting
```

### zsh-autosuggestions

Reference: [https://github.com/zsh-users/zsh-autosuggestions/blob/master/INSTALL.md](https://github.com/zsh-users/zsh-autosuggestions/blob/master/INSTALL.md)

```bash
git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions
```

### Activate plugins

```bash title="~/.zshrc"
plugins=(git zsh-z zsh-syntax-highlighting zsh-autosuggestions)
```

## User Configuration

```bash title="~/.zshrc"
# Language environment
export LANG=en_US.UTF-8

# Alias
alias workspace-on-mac="cd ~/Workspace"

# Homebrew
export HOMEBREW_EDITOR="/usr/bin/nano"
```

## Install Amazon Q for command line

Reference: [https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line-installing.html](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line-installing.html)


Last updated on April 2, 2025