Repository: siluat/web-projects
Pull request #12: 💄 (ui-craft-playground) Add shadcn/ui sidebar
Author: login: siluat
Created: 2025-03-19T08:10:39Z | Merged: 2025-03-19T08:48:25Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **새 기능**
  - 다크 모드 테마를 포함하여 버튼, 입력창, 구분선, 시트, 사이드바, 스켈레톤, 툴팁 등 다양한 UI 컴포넌트를 추가해 사용자 인터랙션과 내비게이션 경험을 강화했습니다.
- **스타일**
  - 글로벌 스타일을 확장하여 사용자 지정 CSS 변수와 애니메이션 효과를 도입, 전반적인 디자인 일관성과 미적 감각을 향상시켰습니다.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/playgrounds/ui-craft-playground/app/globals.css b/playgrounds/ui-craft-playground/app/globals.css
index d4b5078..33f17e9 100644
--- a/playgrounds/ui-craft-playground/app/globals.css
+++ b/playgrounds/ui-craft-playground/app/globals.css
@@ -1 +1,120 @@
 @import 'tailwindcss';
+@import "tw-animate-css";
+
+@custom-variant dark (&:is(.dark *));
+
+:root {
+  --radius: 0.625rem;
+  --background: oklch(1 0 0);
+  --foreground: oklch(0.141 0.005 285.823);
+  --card: oklch(1 0 0);
+  --card-foreground: oklch(0.141 0.005 285.823);
+  --popover: oklch(1 0 0);
+  --popover-foreground: oklch(0.141 0.005 285.823);
+  --primary: oklch(0.21 0.006 285.885);
+  --primary-foreground: oklch(0.985 0 0);
+  --secondary: oklch(0.967 0.001 286.375);
+  --secondary-foreground: oklch(0.21 0.006 285.885);
+  --muted: oklch(0.967 0.001 286.375);
+  --muted-foreground: oklch(0.552 0.016 285.938);
+  --accent: oklch(0.967 0.001 286.375);
+  --accent-foreground: oklch(0.21 0.006 285.885);
+  --destructive: oklch(0.577 0.245 27.325);
+  --border: oklch(0.92 0.004 286.32);
+  --input: oklch(0.92 0.004 286.32);
+  --ring: oklch(0.705 0.015 286.067);
+  --chart-1: oklch(0.646 0.222 41.116);
+  --chart-2: oklch(0.6 0.118 184.704);
+  --chart-3: oklch(0.398 0.07 227.392);
+  --chart-4: oklch(0.828 0.189 84.429);
+  --chart-5: oklch(0.769 0.188 70.08);
+  --sidebar: oklch(0.985 0 0);
+  --sidebar-foreground: oklch(0.141 0.005 285.823);
+  --sidebar-primary: oklch(0.21 0.006 285.885);
+  --sidebar-primary-foreground: oklch(0.985 0 0);
+  --sidebar-accent: oklch(0.967 0.001 286.375);
+  --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
+  --sidebar-border: oklch(0.92 0.004 286.32);
+  --sidebar-ring: oklch(0.705 0.015 286.067);
+}
+
+.dark {
+  --background: oklch(0.141 0.005 285.823);
+  --foreground: oklch(0.985 0 0);
+  --card: oklch(0.21 0.006 285.885);
+  --card-foreground: oklch(0.985 0 0);
+  --popover: oklch(0.21 0.006 285.885);
+  --popover-foreground: oklch(0.985 0 0);
+  --primary: oklch(0.92 0.004 286.32);
+  --primary-foreground: oklch(0.21 0.006 285.885);
+  --secondary: oklch(0.274 0.006 286.033);
+  --secondary-foreground: oklch(0.985 0 0);
+  --muted: oklch(0.274 0.006 286.033);
+  --muted-foreground: oklch(0.705 0.015 286.067);
+  --accent: oklch(0.274 0.006 286.033);
+  --accent-foreground: oklch(0.985 0 0);
+  --destructive: oklch(0.704 0.191 22.216);
+  --border: oklch(1 0 0 / 10%);
+  --input: oklch(1 0 0 / 15%);
+  --ring: oklch(0.552 0.016 285.938);
+  --chart-1: oklch(0.488 0.243 264.376);
+  --chart-2: oklch(0.696 0.17 162.48);
+  --chart-3: oklch(0.769 0.188 70.08);
+  --chart-4: oklch(0.627 0.265 303.9);
+  --chart-5: oklch(0.645 0.246 16.439);
+  --sidebar: oklch(0.21 0.006 285.885);
+  --sidebar-foreground: oklch(0.985 0 0);
+  --sidebar-primary: oklch(0.488 0.243 264.376);
+  --sidebar-primary-foreground: oklch(0.985 0 0);
+  --sidebar-accent: oklch(0.274 0.006 286.033);
+  --sidebar-accent-foreground: oklch(0.985 0 0);
+  --sidebar-border: oklch(1 0 0 / 10%);
+  --sidebar-ring: oklch(0.552 0.016 285.938);
+}
+
+@theme inline {
+  --radius-sm: calc(var(--radius) - 4px);
+  --radius-md: calc(var(--radius) - 2px);
+  --radius-lg: var(--radius);
+  --radius-xl: calc(var(--radius) + 4px);
+  --color-background: var(--background);
+  --color-foreground: var(--foreground);
+  --color-card: var(--card);
+  --color-card-foreground: var(--card-foreground);
+  --color-popover: var(--popover);
+  --color-popover-foreground: var(--popover-foreground);
+  --color-primary: var(--primary);
+  --color-primary-foreground: var(--primary-foreground);
+  --color-secondary: var(--secondary);
+  --color-secondary-foreground: var(--secondary-foreground);
+  --color-muted: var(--muted);
+  --color-muted-foreground: var(--muted-foreground);
+  --color-accent: var(--accent);
+  --color-accent-foreground: var(--accent-foreground);
+  --color-destructive: var(--destructive);
+  --color-border: var(--border);
+  --color-input: var(--input);
+  --color-ring: var(--ring);
+  --color-chart-1: var(--chart-1);
+  --color-chart-2: var(--chart-2);
+  --color-chart-3: var(--chart-3);
+  --color-chart-4: var(--chart-4);
+  --color-chart-5: var(--chart-5);
+  --color-sidebar: var(--sidebar);
+  --color-sidebar-foreground: var(--sidebar-foreground);
+  --color-sidebar-primary: var(--sidebar-primary);
+  --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+  --color-sidebar-accent: var(--sidebar-accent);
+  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+  --color-sidebar-border: var(--sidebar-border);
+  --color-sidebar-ring: var(--sidebar-ring);
+}
+
+@layer base {
+  * {
+    @apply border-border outline-ring/50;
+  }
+  body {
+    @apply bg-background text-foreground;
+  }
+}
diff --git a/playgrounds/ui-craft-playground/app/layout.tsx b/playgrounds/ui-craft-playground/app/layout.tsx
index 04037e6..c2932a6 100644
--- a/playgrounds/ui-craft-playground/app/layout.tsx
+++ b/playgrounds/ui-craft-playground/app/layout.tsx
@@ -4,7 +4,7 @@ export default function RootLayout({
   children,
 }: {
   children: React.ReactNode;
-}) {
+}): JSX.Element {
   return (
     <html lang="en">
       <body>{children}</body>
diff --git a/playgrounds/ui-craft-playground/app/page.tsx b/playgrounds/ui-craft-playground/app/page.tsx
index 3b4be53..977580c 100644
--- a/playgrounds/ui-craft-playground/app/page.tsx
+++ b/playgrounds/ui-craft-playground/app/page.tsx
@@ -1,6 +1,6 @@
 import { FfxivJobIconTransformUsingGooeyEffect } from '@siluat/ui-craft/react/ffxiv-job-icon-transform-using-gooey-effect';
 
-export default function Page() {
+export default function Page(): JSX.Element {
   return (
     <main className="flex flex-col items-center justify-center h-screen">
       <span>FFXIV Job Icon Transform Using Gooey Effect</span>
diff --git a/playgrounds/ui-craft-playground/components.json b/playgrounds/ui-craft-playground/components.json
new file mode 100644
index 0000000..0c04595
--- /dev/null
+++ b/playgrounds/ui-craft-playground/components.json
@@ -0,0 +1,21 @@
+{
+  "$schema": "https://ui.shadcn.com/schema.json",
+  "style": "new-york",
+  "rsc": true,
+  "tsx": true,
+  "tailwind": {
+    "config": "",
+    "css": "app/globals.css",
+    "baseColor": "zinc",
+    "cssVariables": true,
+    "prefix": ""
+  },
+  "aliases": {
+    "components": "@/shadcn-components",
+    "utils": "@/shadcn-lib/utils",
+    "ui": "@/shadcn-components/ui",
+    "lib": "@/shadcn-lib",
+    "hooks": "@/shadcn-hooks"
+  },
+  "iconLibrary": "lucide"
+}
diff --git a/playgrounds/ui-craft-playground/package.json b/playgrounds/ui-craft-playground/package.json
index 5dbb1e7..7a8d2b7 100644
--- a/playgrounds/ui-craft-playground/package.json
+++ b/playgrounds/ui-craft-playground/package.json
@@ -11,13 +11,22 @@
     "lint": "next lint"
   },
   "dependencies": {
+    "@radix-ui/react-dialog": "^1.1.6",
+    "@radix-ui/react-separator": "^1.1.2",
+    "@radix-ui/react-slot": "^1.1.2",
+    "@radix-ui/react-tooltip": "^1.1.8",
     "@siluat/ui-craft": "workspace:*",
     "@tailwindcss/postcss": "^4.0.14",
+    "class-variance-authority": "^0.7.1",
+    "clsx": "^2.1.1",
+    "lucide-react": "^0.483.0",
     "next": "^15.2.3",
     "postcss": "^8.5.3",
     "react": "^19.0.0",
     "react-dom": "^19.0.0",
-    "tailwindcss": "^4.0.14"
+    "tailwind-merge": "^3.0.2",
+    "tailwindcss": "^4.0.14",
+    "tw-animate-css": "^1.2.4"
   },
   "devDependencies": {
     "@siluat/typescript-config": "workspace:*",
diff --git a/playgrounds/ui-craft-playground/shadcn-components/ui/button.tsx b/playgrounds/ui-craft-playground/shadcn-components/ui/button.tsx
new file mode 100644
index 0000000..e6745ff
--- /dev/null
+++ b/playgrounds/ui-craft-playground/shadcn-components/ui/button.tsx
@@ -0,0 +1,59 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/shadcn-lib/utils"
+
+const buttonVariants = cva(
+  "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+  {
+    variants: {
+      variant: {
+        default:
+          "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
+        destructive:
+          "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+        outline:
+          "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
+        secondary:
+          "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
+        ghost:
+          "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
+        link: "text-primary underline-offset-4 hover:underline",
+      },
+      size: {
+        default: "h-9 px-4 py-2 has-[>svg]:px-3",
+        sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
+        lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
+        icon: "size-9",
+      },
+    },
+    defaultVariants: {
+      variant: "default",
+      size: "default",
+    },
+  }
+)
+
+function Button({
+  className,
+  variant,
+  size,
+  asChild = false,
+  ...props
+}: React.ComponentProps<"button"> &
+  VariantProps<typeof buttonVariants> & {
+    asChild?: boolean
+  }) {
+  const Comp = asChild ? Slot : "button"
+
+  return (
+    <Comp
+      data-slot="button"
+      className={cn(buttonVariants({ variant, size, className }))}
+      {...props}
+    />
+  )
+}
+
+export { Button, buttonVariants }
diff --git a/playgrounds/ui-craft-playground/shadcn-components/ui/input.tsx b/playgrounds/ui-craft-playground/shadcn-components/ui/input.tsx
new file mode 100644
index 0000000..4314df6
--- /dev/null
+++ b/playgrounds/ui-craft-playground/shadcn-components/ui/input.tsx
@@ -0,0 +1,21 @@
+import * as React from "react"
+
+import { cn } from "@/shadcn-lib/utils"
+
+function Input({ className, type, ...props }: React.ComponentProps<"input">) {
+  return (
+    <input
+      type={type}
+      data-slot="input"
+      className={cn(
+        "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
+        "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
+        "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+export { Input }
diff --git a/playgrounds/ui-craft-playground/shadcn-components/ui/separator.tsx b/playgrounds/ui-craft-playground/shadcn-components/ui/separator.tsx
new file mode 100644
index 0000000..29203f2
--- /dev/null
+++ b/playgrounds/ui-craft-playground/shadcn-components/ui/separator.tsx
@@ -0,0 +1,28 @@
+"use client"
+
+import * as React from "react"
+import * as SeparatorPrimitive from "@radix-ui/react-separator"
+
+import { cn } from "@/shadcn-lib/utils"
+
+function Separator({
+  className,
+  orientation = "horizontal",
+  decorative = true,
+  ...props
+}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
+  return (
+    <SeparatorPrimitive.Root
+      data-slot="separator-root"
+      decorative={decorative}
+      orientation={orientation}
+      className={cn(
+        "bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+export { Separator }
diff --git a/playgrounds/ui-craft-playground/shadcn-components/ui/sheet.tsx b/playgrounds/ui-craft-playground/shadcn-components/ui/sheet.tsx
new file mode 100644
index 0000000..08417c2
--- /dev/null
+++ b/playgrounds/ui-craft-playground/shadcn-components/ui/sheet.tsx
@@ -0,0 +1,139 @@
+"use client"
+
+import * as React from "react"
+import * as SheetPrimitive from "@radix-ui/react-dialog"
+import { XIcon } from "lucide-react"
+
+import { cn } from "@/shadcn-lib/utils"
+
+function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
+  return <SheetPrimitive.Root data-slot="sheet" {...props} />
+}
+
+function SheetTrigger({
+  ...props
+}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
+  return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
+}
+
+function SheetClose({
+  ...props
+}: React.ComponentProps<typeof SheetPrimitive.Close>) {
+  return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
+}
+
+function SheetPortal({
+  ...props
+}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
+  return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
+}
+
+function SheetOverlay({
+  className,
+  ...props
+}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
+  return (
+    <SheetPrimitive.Overlay
+      data-slot="sheet-overlay"
+      className={cn(
+        "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function SheetContent({
+  className,
+  children,
+  side = "right",
+  ...props
+}: React.ComponentProps<typeof SheetPrimitive.Content> & {
+  side?: "top" | "right" | "bottom" | "left"
+}) {
+  return (
+    <SheetPortal>
+      <SheetOverlay />
+      <SheetPrimitive.Content
+        data-slot="sheet-content"
+        className={cn(
+          "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
+          side === "right" &&
+            "data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
+          side === "left" &&
+            "data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
+          side === "top" &&
+            "data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
+          side === "bottom" &&
+            "data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
+          className
+        )}
+        {...props}
+      >
+        {children}
+        <SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
+          <XIcon className="size-4" />
+          <span className="sr-only">Close</span>
+        </SheetPrimitive.Close>
+      </SheetPrimitive.Content>
+    </SheetPortal>
+  )
+}
+
+function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="sheet-header"
+      className={cn("flex flex-col gap-1.5 p-4", className)}
+      {...props}
+    />
+  )
+}
+
+function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="sheet-footer"
+      className={cn("mt-auto flex flex-col gap-2 p-4", className)}
+      {...props}
+    />
+  )
+}
+
+function SheetTitle({
+  className,
+  ...props
+}: React.ComponentProps<typeof SheetPrimitive.Title>) {
+  return (
+    <SheetPrimitive.Title
+      data-slot="sheet-title"
+      className={cn("text-foreground font-semibold", className)}
+      {...props}
+    />
+  )
+}
+
+function SheetDescription({
+  className,
+  ...props
+}: React.ComponentProps<typeof SheetPrimitive.Description>) {
+  return (
+    <SheetPrimitive.Description
+      data-slot="sheet-description"
+      className={cn("text-muted-foreground text-sm", className)}
+      {...props}
+    />
+  )
+}
+
+export {
+  Sheet,
+  SheetTrigger,
+  SheetClose,
+  SheetContent,
+  SheetHeader,
+  SheetFooter,
+  SheetTitle,
+  SheetDescription,
+}
diff --git a/playgrounds/ui-craft-playground/shadcn-components/ui/sidebar.tsx b/playgrounds/ui-craft-playground/shadcn-components/ui/sidebar.tsx
new file mode 100644
index 0000000..8f897c6
--- /dev/null
+++ b/playgrounds/ui-craft-playground/shadcn-components/ui/sidebar.tsx
@@ -0,0 +1,726 @@
+"use client"
+
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { VariantProps, cva } from "class-variance-authority"
+import { PanelLeftIcon } from "lucide-react"
+
+import { useIsMobile } from "@/shadcn-hooks/use-mobile"
+import { cn } from "@/shadcn-lib/utils"
+import { Button } from "@/shadcn-components/ui/button"
+import { Input } from "@/shadcn-components/ui/input"
+import { Separator } from "@/shadcn-components/ui/separator"
+import {
+  Sheet,
+  SheetContent,
+  SheetDescription,
+  SheetHeader,
+  SheetTitle,
+} from "@/shadcn-components/ui/sheet"
+import { Skeleton } from "@/shadcn-components/ui/skeleton"
+import {
+  Tooltip,
+  TooltipContent,
+  TooltipProvider,
+  TooltipTrigger,
+} from "@/shadcn-components/ui/tooltip"
+
+const SIDEBAR_COOKIE_NAME = "sidebar_state"
+const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
+const SIDEBAR_WIDTH = "16rem"
+const SIDEBAR_WIDTH_MOBILE = "18rem"
+const SIDEBAR_WIDTH_ICON = "3rem"
+const SIDEBAR_KEYBOARD_SHORTCUT = "b"
+
+type SidebarContextProps = {
+  state: "expanded" | "collapsed"
+  open: boolean
+  setOpen: (open: boolean) => void
+  openMobile: boolean
+  setOpenMobile: (open: boolean) => void
+  isMobile: boolean
+  toggleSidebar: () => void
+}
+
+const SidebarContext = React.createContext<SidebarContextProps | null>(null)
+
+function useSidebar() {
+  const context = React.useContext(SidebarContext)
+  if (!context) {
+    throw new Error("useSidebar must be used within a SidebarProvider.")
+  }
+
+  return context
+}
+
+function SidebarProvider({
+  defaultOpen = true,
+  open: openProp,
+  onOpenChange: setOpenProp,
+  className,
+  style,
+  children,
+  ...props
+}: React.ComponentProps<"div"> & {
+  defaultOpen?: boolean
+  open?: boolean
+  onOpenChange?: (open: boolean) => void
+}) {
+  const isMobile = useIsMobile()
+  const [openMobile, setOpenMobile] = React.useState(false)
+
+  // This is the internal state of the sidebar.
+  // We use openProp and setOpenProp for control from outside the component.
+  const [_open, _setOpen] = React.useState(defaultOpen)
+  const open = openProp ?? _open
+  const setOpen = React.useCallback(
+    (value: boolean | ((value: boolean) => boolean)) => {
+      const openState = typeof value === "function" ? value(open) : value
+      if (setOpenProp) {
+        setOpenProp(openState)
+      } else {
+        _setOpen(openState)
+      }
+
+      // This sets the cookie to keep the sidebar state.
+      document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
+    },
+    [setOpenProp, open]
+  )
+
+  // Helper to toggle the sidebar.
+  const toggleSidebar = React.useCallback(() => {
+    return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
+  }, [isMobile, setOpen, setOpenMobile])
+
+  // Adds a keyboard shortcut to toggle the sidebar.
+  React.useEffect(() => {
+    const handleKeyDown = (event: KeyboardEvent) => {
+      if (
+        event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
+        (event.metaKey || event.ctrlKey)
+      ) {
+        event.preventDefault()
+        toggleSidebar()
+      }
+    }
+
+    window.addEventListener("keydown", handleKeyDown)
+    return () => window.removeEventListener("keydown", handleKeyDown)
+  }, [toggleSidebar])
+
+  // We add a state so that we can do data-state="expanded" or "collapsed".
+  // This makes it easier to style the sidebar with Tailwind classes.
+  const state = open ? "expanded" : "collapsed"
+
+  const contextValue = React.useMemo<SidebarContextProps>(
+    () => ({
+      state,
+      open,
+      setOpen,
+      isMobile,
+      openMobile,
+      setOpenMobile,
+      toggleSidebar,
+    }),
+    [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
+  )
+
+  return (
+    <SidebarContext.Provider value={contextValue}>
+      <TooltipProvider delayDuration={0}>
+        <div
+          data-slot="sidebar-wrapper"
+          style={
+            {
+              "--sidebar-width": SIDEBAR_WIDTH,
+              "--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
+              ...style,
+            } as React.CSSProperties
+          }
+          className={cn(
+            "group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
+            className
+          )}
+          {...props}
+        >
+          {children}
+        </div>
+      </TooltipProvider>
+    </SidebarContext.Provider>
+  )
+}
+
+function Sidebar({
+  side = "left",
+  variant = "sidebar",
+  collapsible = "offcanvas",
+  className,
+  children,
+  ...props
+}: React.ComponentProps<"div"> & {
+  side?: "left" | "right"
+  variant?: "sidebar" | "floating" | "inset"
+  collapsible?: "offcanvas" | "icon" | "none"
+}) {
+  const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
+
+  if (collapsible === "none") {
+    return (
+      <div
+        data-slot="sidebar"
+        className={cn(
+          "bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
+          className
+        )}
+        {...props}
+      >
+        {children}
+      </div>
+    )
+  }
+
+  if (isMobile) {
+    return (
+      <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
+        <SheetContent
+          data-sidebar="sidebar"
+          data-slot="sidebar"
+          data-mobile="true"
+          className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
+          style={
+            {
+              "--sidebar-width": SIDEBAR_WIDTH_MOBILE,
+            } as React.CSSProperties
+          }
+          side={side}
+        >
+          <SheetHeader className="sr-only">
+            <SheetTitle>Sidebar</SheetTitle>
+            <SheetDescription>Displays the mobile sidebar.</SheetDescription>
+          </SheetHeader>
+          <div className="flex h-full w-full flex-col">{children}</div>
+        </SheetContent>
+      </Sheet>
+    )
+  }
+
+  return (
+    <div
+      className="group peer text-sidebar-foreground hidden md:block"
+      data-state={state}
+      data-collapsible={state === "collapsed" ? collapsible : ""}
+      data-variant={variant}
+      data-side={side}
+      data-slot="sidebar"
+    >
+      {/* This is what handles the sidebar gap on desktop */}
+      <div
+        data-slot="sidebar-gap"
+        className={cn(
+          "relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",
+          "group-data-[collapsible=offcanvas]:w-0",
+          "group-data-[side=right]:rotate-180",
+          variant === "floating" || variant === "inset"
+            ? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
+            : "group-data-[collapsible=icon]:w-(--sidebar-width-icon)"
+        )}
+      />
+      <div
+        data-slot="sidebar-container"
+        className={cn(
+          "fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",
+          side === "left"
+            ? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
+            : "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
+          // Adjust the padding for floating and inset variants.
+          variant === "floating" || variant === "inset"
+            ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
+            : "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
+          className
+        )}
+        {...props}
+      >
+        <div
+          data-sidebar="sidebar"
+          data-slot="sidebar-inner"
+          className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
+        >
+          {children}
+        </div>
+      </div>
+    </div>
+  )
+}
+
+function SidebarTrigger({
+  className,
+  onClick,
+  ...props
+}: React.ComponentProps<typeof Button>) {
+  const { toggleSidebar } = useSidebar()
+
+  return (
+    <Button
+      data-sidebar="trigger"
+      data-slot="sidebar-trigger"
+      variant="ghost"
+      size="icon"
+      className={cn("size-7", className)}
+      onClick={(event) => {
+        onClick?.(event)
+        toggleSidebar()
+      }}
+      {...props}
+    >
+      <PanelLeftIcon />
+      <span className="sr-only">Toggle Sidebar</span>
+    </Button>
+  )
+}
+
+function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
+  const { toggleSidebar } = useSidebar()
+
+  return (
+    <button
+      data-sidebar="rail"
+      data-slot="sidebar-rail"
+      aria-label="Toggle Sidebar"
+      tabIndex={-1}
+      onClick={toggleSidebar}
+      title="Toggle Sidebar"
+      className={cn(
+        "hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
+        "in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
+        "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
+        "hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
+        "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
+        "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
+  return (
+    <main
+      data-slot="sidebar-inset"
+      className={cn(
+        "bg-background relative flex w-full flex-1 flex-col",
+        "md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function SidebarInput({
+  className,
+  ...props
+}: React.ComponentProps<typeof Input>) {
+  return (
+    <Input
+      data-slot="sidebar-input"
+      data-sidebar="input"
+      className={cn("bg-background h-8 w-full shadow-none", className)}
+      {...props}
+    />
+  )
+}
+
+function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="sidebar-header"
+      data-sidebar="header"
+      className={cn("flex flex-col gap-2 p-2", className)}
+      {...props}
+    />
+  )
+}
+
+function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="sidebar-footer"
+      data-sidebar="footer"
+      className={cn("flex flex-col gap-2 p-2", className)}
+      {...props}
+    />
+  )
+}
+
+function SidebarSeparator({
+  className,
+  ...props
+}: React.ComponentProps<typeof Separator>) {
+  return (
+    <Separator
+      data-slot="sidebar-separator"
+      data-sidebar="separator"
+      className={cn("bg-sidebar-border mx-2 w-auto", className)}
+      {...props}
+    />
+  )
+}
+
+function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="sidebar-content"
+      data-sidebar="content"
+      className={cn(
+        "flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="sidebar-group"
+      data-sidebar="group"
+      className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
+      {...props}
+    />
+  )
+}
+
+function SidebarGroupLabel({
+  className,
+  asChild = false,
+  ...props
+}: React.ComponentProps<"div"> & { asChild?: boolean }) {
+  const Comp = asChild ? Slot : "div"
+
+  return (
+    <Comp
+      data-slot="sidebar-group-label"
+      data-sidebar="group-label"
+      className={cn(
+        "text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
+        "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function SidebarGroupAction({
+  className,
+  asChild = false,
+  ...props
+}: React.ComponentProps<"button"> & { asChild?: boolean }) {
+  const Comp = asChild ? Slot : "button"
+
+  return (
+    <Comp
+      data-slot="sidebar-group-action"
+      data-sidebar="group-action"
+      className={cn(
+        "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
+        // Increases the hit area of the button on mobile.
+        "after:absolute after:-inset-2 md:after:hidden",
+        "group-data-[collapsible=icon]:hidden",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function SidebarGroupContent({
+  className,
+  ...props
+}: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="sidebar-group-content"
+      data-sidebar="group-content"
+      className={cn("w-full text-sm", className)}
+      {...props}
+    />
+  )
+}
+
+function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
+  return (
+    <ul
+      data-slot="sidebar-menu"
+      data-sidebar="menu"
+      className={cn("flex w-full min-w-0 flex-col gap-1", className)}
+      {...props}
+    />
+  )
+}
+
+function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
+  return (
+    <li
+      data-slot="sidebar-menu-item"
+      data-sidebar="menu-item"
+      className={cn("group/menu-item relative", className)}
+      {...props}
+    />
+  )
+}
+
+const sidebarMenuButtonVariants = cva(
+  "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
+  {
+    variants: {
+      variant: {
+        default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
+        outline:
+          "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
+      },
+      size: {
+        default: "h-8 text-sm",
+        sm: "h-7 text-xs",
+        lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
+      },
+    },
+    defaultVariants: {
+      variant: "default",
+      size: "default",
+    },
+  }
+)
+
+function SidebarMenuButton({
+  asChild = false,
+  isActive = false,
+  variant = "default",
+  size = "default",
+  tooltip,
+  className,
+  ...props
+}: React.ComponentProps<"button"> & {
+  asChild?: boolean
+  isActive?: boolean
+  tooltip?: string | React.ComponentProps<typeof TooltipContent>
+} & VariantProps<typeof sidebarMenuButtonVariants>) {
+  const Comp = asChild ? Slot : "button"
+  const { isMobile, state } = useSidebar()
+
+  const button = (
+    <Comp
+      data-slot="sidebar-menu-button"
+      data-sidebar="menu-button"
+      data-size={size}
+      data-active={isActive}
+      className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
+      {...props}
+    />
+  )
+
+  if (!tooltip) {
+    return button
+  }
+
+  if (typeof tooltip === "string") {
+    tooltip = {
+      children: tooltip,
+    }
+  }
+
+  return (
+    <Tooltip>
+      <TooltipTrigger asChild>{button}</TooltipTrigger>
+      <TooltipContent
+        side="right"
+        align="center"
+        hidden={state !== "collapsed" || isMobile}
+        {...tooltip}
+      />
+    </Tooltip>
+  )
+}
+
+function SidebarMenuAction({
+  className,
+  asChild = false,
+  showOnHover = false,
+  ...props
+}: React.ComponentProps<"button"> & {
+  asChild?: boolean
+  showOnHover?: boolean
+}) {
+  const Comp = asChild ? Slot : "button"
+
+  return (
+    <Comp
+      data-slot="sidebar-menu-action"
+      data-sidebar="menu-action"
+      className={cn(
+        "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
+        // Increases the hit area of the button on mobile.
+        "after:absolute after:-inset-2 md:after:hidden",
+        "peer-data-[size=sm]/menu-button:top-1",
+        "peer-data-[size=default]/menu-button:top-1.5",
+        "peer-data-[size=lg]/menu-button:top-2.5",
+        "group-data-[collapsible=icon]:hidden",
+        showOnHover &&
+          "peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function SidebarMenuBadge({
+  className,
+  ...props
+}: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="sidebar-menu-badge"
+      data-sidebar="menu-badge"
+      className={cn(
+        "text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none",
+        "peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
+        "peer-data-[size=sm]/menu-button:top-1",
+        "peer-data-[size=default]/menu-button:top-1.5",
+        "peer-data-[size=lg]/menu-button:top-2.5",
+        "group-data-[collapsible=icon]:hidden",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function SidebarMenuSkeleton({
+  className,
+  showIcon = false,
+  ...props
+}: React.ComponentProps<"div"> & {
+  showIcon?: boolean
+}) {
+  // Random width between 50 to 90%.
+  const width = React.useMemo(() => {
+    return `${Math.floor(Math.random() * 40) + 50}%`
+  }, [])
+
+  return (
+    <div
+      data-slot="sidebar-menu-skeleton"
+      data-sidebar="menu-skeleton"
+      className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
+      {...props}
+    >
+      {showIcon && (
+        <Skeleton
+          className="size-4 rounded-md"
+          data-sidebar="menu-skeleton-icon"
+        />
+      )}
+      <Skeleton
+        className="h-4 max-w-(--skeleton-width) flex-1"
+        data-sidebar="menu-skeleton-text"
+        style={
+          {
+            "--skeleton-width": width,
+          } as React.CSSProperties
+        }
+      />
+    </div>
+  )
+}
+
+function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
+  return (
+    <ul
+      data-slot="sidebar-menu-sub"
+      data-sidebar="menu-sub"
+      className={cn(
+        "border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
+        "group-data-[collapsible=icon]:hidden",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function SidebarMenuSubItem({
+  className,
+  ...props
+}: React.ComponentProps<"li">) {
+  return (
+    <li
+      data-slot="sidebar-menu-sub-item"
+      data-sidebar="menu-sub-item"
+      className={cn("group/menu-sub-item relative", className)}
+      {...props}
+    />
+  )
+}
+
+function SidebarMenuSubButton({
+  asChild = false,
+  size = "md",
+  isActive = false,
+  className,
+  ...props
+}: React.ComponentProps<"a"> & {
+  asChild?: boolean
+  size?: "sm" | "md"
+  isActive?: boolean
+}) {
+  const Comp = asChild ? Slot : "a"
+
+  return (
+    <Comp
+      data-slot="sidebar-menu-sub-button"
+      data-sidebar="menu-sub-button"
+      data-size={size}
+      data-active={isActive}
+      className={cn(
+        "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
+        "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
+        size === "sm" && "text-xs",
+        size === "md" && "text-sm",
+        "group-data-[collapsible=icon]:hidden",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+export {
+  Sidebar,
+  SidebarContent,
+  SidebarFooter,
+  SidebarGroup,
+  SidebarGroupAction,
+  SidebarGroupContent,
+  SidebarGroupLabel,
+  SidebarHeader,
+  SidebarInput,
+  SidebarInset,
+  SidebarMenu,
+  SidebarMenuAction,
+  SidebarMenuBadge,
+  SidebarMenuButton,
+  SidebarMenuItem,
+  SidebarMenuSkeleton,
+  SidebarMenuSub,
+  SidebarMenuSubButton,
+  SidebarMenuSubItem,
+  SidebarProvider,
+  SidebarRail,
+  SidebarSeparator,
+  SidebarTrigger,
+  useSidebar,
+}
diff --git a/playgrounds/ui-craft-playground/shadcn-components/ui/skeleton.tsx b/playgrounds/ui-craft-playground/shadcn-components/ui/skeleton.tsx
new file mode 100644
index 0000000..b51cd1b
--- /dev/null
+++ b/playgrounds/ui-craft-playground/shadcn-components/ui/skeleton.tsx
@@ -0,0 +1,13 @@
+import { cn } from "@/shadcn-lib/utils"
+
+function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="skeleton"
+      className={cn("bg-accent animate-pulse rounded-md", className)}
+      {...props}
+    />
+  )
+}
+
+export { Skeleton }
diff --git a/playgrounds/ui-craft-playground/shadcn-components/ui/tooltip.tsx b/playgrounds/ui-craft-playground/shadcn-components/ui/tooltip.tsx
new file mode 100644
index 0000000..dec624a
--- /dev/null
+++ b/playgrounds/ui-craft-playground/shadcn-components/ui/tooltip.tsx
@@ -0,0 +1,61 @@
+"use client"
+
+import * as React from "react"
+import * as TooltipPrimitive from "@radix-ui/react-tooltip"
+
+import { cn } from "@/shadcn-lib/utils"
+
+function TooltipProvider({
+  delayDuration = 0,
+  ...props
+}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
+  return (
+    <TooltipPrimitive.Provider
+      data-slot="tooltip-provider"
+      delayDuration={delayDuration}
+      {...props}
+    />
+  )
+}
+
+function Tooltip({
+  ...props
+}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
+  return (
+    <TooltipProvider>
+      <TooltipPrimitive.Root data-slot="tooltip" {...props} />
+    </TooltipProvider>
+  )
+}
+
+function TooltipTrigger({
+  ...props
+}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
+  return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
+}
+
+function TooltipContent({
+  className,
+  sideOffset = 0,
+  children,
+  ...props
+}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
+  return (
+    <TooltipPrimitive.Portal>
+      <TooltipPrimitive.Content
+        data-slot="tooltip-content"
+        sideOffset={sideOffset}
+        className={cn(
+          "bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
+          className
+        )}
+        {...props}
+      >
+        {children}
+        <TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
+      </TooltipPrimitive.Content>
+    </TooltipPrimitive.Portal>
+  )
+}
+
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
diff --git a/playgrounds/ui-craft-playground/shadcn-hooks/use-mobile.ts b/playgrounds/ui-craft-playground/shadcn-hooks/use-mobile.ts
new file mode 100644
index 0000000..2b0fe1d
--- /dev/null
+++ b/playgrounds/ui-craft-playground/shadcn-hooks/use-mobile.ts
@@ -0,0 +1,19 @@
+import * as React from "react"
+
+const MOBILE_BREAKPOINT = 768
+
+export function useIsMobile() {
+  const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
+
+  React.useEffect(() => {
+    const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
+    const onChange = () => {
+      setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
+    }
+    mql.addEventListener("change", onChange)
+    setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
+    return () => mql.removeEventListener("change", onChange)
+  }, [])
+
+  return !!isMobile
+}
diff --git a/playgrounds/ui-craft-playground/shadcn-lib/utils.ts b/playgrounds/ui-craft-playground/shadcn-lib/utils.ts
new file mode 100644
index 0000000..bd0c391
--- /dev/null
+++ b/playgrounds/ui-craft-playground/shadcn-lib/utils.ts
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from "clsx"
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs: ClassValue[]) {
+  return twMerge(clsx(inputs))
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 136e326..c7a3a27 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -98,12 +98,33 @@ importers:
 
   playgrounds/ui-craft-playground:
     dependencies:
+      '@radix-ui/react-dialog':
+        specifier: ^1.1.6
+        version: 1.1.6(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-separator':
+        specifier: ^1.1.2
+        version: 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-slot':
+        specifier: ^1.1.2
+        version: 1.1.2(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-tooltip':
+        specifier: ^1.1.8
+        version: 1.1.8(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
       '@siluat/ui-craft':
         specifier: workspace:*
         version: link:../../packages/ui-craft
       '@tailwindcss/postcss':
         specifier: ^4.0.14
         version: 4.0.14
+      class-variance-authority:
+        specifier: ^0.7.1
+        version: 0.7.1
+      clsx:
+        specifier: ^2.1.1
+        version: 2.1.1
+      lucide-react:
+        specifier: ^0.483.0
+        version: 0.483.0(react@19.0.0)
       next:
         specifier: ^15.2.3
         version: 15.2.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@@ -116,9 +137,15 @@ importers:
       react-dom:
         specifier: ^19.0.0
         version: 19.0.0(react@19.0.0)
+      tailwind-merge:
+        specifier: ^3.0.2
+        version: 3.0.2
       tailwindcss:
         specifier: ^4.0.14
         version: 4.0.14
+      tw-animate-css:
+        specifier: ^1.2.4
+        version: 1.2.4
     devDependencies:
       '@siluat/typescript-config':
         specifier: workspace:*
@@ -573,6 +600,21 @@ packages:
     cpu: [x64]
     os: [win32]
 
+  '@floating-ui/core@1.6.9':
+    resolution: {integrity: sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==}
+
+  '@floating-ui/dom@1.6.13':
+    resolution: {integrity: sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==}
+
+  '@floating-ui/react-dom@2.1.2':
+    resolution: {integrity: sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==}
+    peerDependencies:
+      react: '>=16.8.0'
+      react-dom: '>=16.8.0'
+
+  '@floating-ui/utils@0.2.9':
+    resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==}
+
   '@fontsource-variable/noto-sans-kr@5.2.5':
     resolution: {integrity: sha512-dIJyVPIBRJXtvaFmQ6GKCV93J9Fx3O2Ym5C3gDRcz5zLpEigrnlEfBnsXBUv4aDBHEF/gm/Q07d40dddGxFytg==}
 
@@ -875,6 +917,254 @@ packages:
   '@radix-ui/colors@3.0.0':
     resolution: {integrity: sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg==}
 
+  '@radix-ui/primitive@1.1.1':
+    resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==}
+
+  '@radix-ui/react-arrow@1.1.2':
+    resolution: {integrity: sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/react-compose-refs@1.1.1':
+    resolution: {integrity: sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  '@radix-ui/react-context@1.1.1':
+    resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  '@radix-ui/react-dialog@1.1.6':
+    resolution: {integrity: sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/react-dismissable-layer@1.1.5':
+    resolution: {integrity: sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/react-focus-guards@1.1.1':
+    resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  '@radix-ui/react-focus-scope@1.1.2':
+    resolution: {integrity: sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/react-id@1.1.0':
+    resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  '@radix-ui/react-popper@1.2.2':
+    resolution: {integrity: sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/react-portal@1.1.4':
+    resolution: {integrity: sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/react-presence@1.1.2':
+    resolution: {integrity: sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/react-primitive@2.0.2':
+    resolution: {integrity: sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/react-separator@1.1.2':
+    resolution: {integrity: sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/react-slot@1.1.2':
+    resolution: {integrity: sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  '@radix-ui/react-tooltip@1.1.8':
+    resolution: {integrity: sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/react-use-callback-ref@1.1.0':
+    resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  '@radix-ui/react-use-controllable-state@1.1.0':
+    resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  '@radix-ui/react-use-escape-keydown@1.1.0':
+    resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  '@radix-ui/react-use-layout-effect@1.1.0':
+    resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  '@radix-ui/react-use-rect@1.1.0':
+    resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  '@radix-ui/react-use-size@1.1.0':
+    resolution: {integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  '@radix-ui/react-visually-hidden@1.1.2':
+    resolution: {integrity: sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/rect@1.1.0':
+    resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==}
+
   '@rollup/pluginutils@5.1.4':
     resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==}
     engines: {node: '>=14.0.0'}
@@ -1282,6 +1572,10 @@ packages:
   argparse@2.0.1:
     resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
 
+  aria-hidden@1.2.4:
+    resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==}
+    engines: {node: '>=10'}
+
   aria-query@5.3.0:
     resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
 
@@ -1421,6 +1715,9 @@ packages:
     resolution: {integrity: sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==}
     engines: {node: '>=8'}
 
+  class-variance-authority@0.7.1:
+    resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
+
   cli-boxes@3.0.0:
     resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==}
     engines: {node: '>=10'}
@@ -1546,6 +1843,9 @@ packages:
     resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==}
     engines: {node: '>=8'}
 
+  detect-node-es@1.1.0:
+    resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
+
   deterministic-object-hash@2.0.2:
     resolution: {integrity: sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ==}
     engines: {node: '>=18'}
@@ -1743,6 +2043,10 @@ packages:
     resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==}
     engines: {node: '>=18'}
 
+  get-nonce@1.0.1:
+    resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
+    engines: {node: '>=6'}
+
   github-slugger@2.0.0:
     resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==}
 
@@ -2662,6 +2966,36 @@ packages:
     resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
     engines: {node: '>=0.10.0'}
 
+  react-remove-scroll-bar@2.3.8:
+    resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==}
+    engines: {node: '>=10'}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  react-remove-scroll@2.6.3:
+    resolution: {integrity: sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==}
+    engines: {node: '>=10'}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  react-style-singleton@2.2.3:
+    resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}
+    engines: {node: '>=10'}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
   react@19.0.0:
     resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==}
     engines: {node: '>=0.10.0'}
@@ -2910,6 +3244,9 @@ packages:
     resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==}
     engines: {node: '>=10.0.0'}
 
+  tailwind-merge@3.0.2:
+    resolution: {integrity: sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==}
+
   tailwindcss@4.0.14:
     resolution: {integrity: sha512-92YT2dpt671tFiHH/e1ok9D987N9fHD5VWoly1CdPD/Cd1HMglvZwP3nx2yTj2lbXDAHt8QssZkxTLCCTNL+xw==}
 
@@ -3041,6 +3378,9 @@ packages:
     resolution: {integrity: sha512-N9FDOVaY3yz0YCOhYIgOGYad7+m2ptvinXygw27WPLQvcZDl3+0Sa77KGVlLSiuPDChOUEnTKE9VJwLSi9BPGQ==}
     hasBin: true
 
+  tw-animate-css@1.2.4:
+    resolution: {integrity: sha512-yt+HkJB41NAvOffe4NweJU6fLqAlVx/mBX6XmHRp15kq0JxTtOKaIw8pVSWM1Z+n2nXtyi7cW6C9f0WG/F/QAQ==}
+
   type-fest@0.21.3:
     resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
     engines: {node: '>=10'}
@@ -3181,6 +3521,26 @@ packages:
   url-parse@1.5.10:
     resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
 
+  use-callback-ref@1.3.3:
+    resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==}
+    engines: {node: '>=10'}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  use-sidecar@1.1.3:
+    resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
+    engines: {node: '>=10'}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
   util-deprecate@1.0.2:
     resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
 
@@ -3919,6 +4279,23 @@ snapshots:
   '@esbuild/win32-x64@0.25.1':
     optional: true
 
+  '@floating-ui/core@1.6.9':
+    dependencies:
+      '@floating-ui/utils': 0.2.9
+
+  '@floating-ui/dom@1.6.13':
+    dependencies:
+      '@floating-ui/core': 1.6.9
+      '@floating-ui/utils': 0.2.9
+
+  '@floating-ui/react-dom@2.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@floating-ui/dom': 1.6.13
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+
+  '@floating-ui/utils@0.2.9': {}
+
   '@fontsource-variable/noto-sans-kr@5.2.5': {}
 
   '@img/sharp-darwin-arm64@0.33.5':
@@ -4320,6 +4697,222 @@ snapshots:
 
   '@radix-ui/colors@3.0.0': {}
 
+  '@radix-ui/primitive@1.1.1': {}
+
+  '@radix-ui/react-arrow@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+    optionalDependencies:
+      '@types/react': 19.0.11
+      '@types/react-dom': 19.0.4(@types/react@19.0.11)
+
+  '@radix-ui/react-compose-refs@1.1.1(@types/react@19.0.11)(react@19.0.0)':
+    dependencies:
+      react: 19.0.0
+    optionalDependencies:
+      '@types/react': 19.0.11
+
+  '@radix-ui/react-context@1.1.1(@types/react@19.0.11)(react@19.0.0)':
+    dependencies:
+      react: 19.0.0
+    optionalDependencies:
+      '@types/react': 19.0.11
+
+  '@radix-ui/react-dialog@1.1.6(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@radix-ui/primitive': 1.1.1
+      '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-context': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-dismissable-layer': 1.1.5(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-focus-guards': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-focus-scope': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-id': 1.1.0(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-portal': 1.1.4(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-slot': 1.1.2(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.11)(react@19.0.0)
+      aria-hidden: 1.2.4
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+      react-remove-scroll: 2.6.3(@types/react@19.0.11)(react@19.0.0)
+    optionalDependencies:
+      '@types/react': 19.0.11
+      '@types/react-dom': 19.0.4(@types/react@19.0.11)
+
+  '@radix-ui/react-dismissable-layer@1.1.5(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@radix-ui/primitive': 1.1.1
+      '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@19.0.11)(react@19.0.0)
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+    optionalDependencies:
+      '@types/react': 19.0.11
+      '@types/react-dom': 19.0.4(@types/react@19.0.11)
+
+  '@radix-ui/react-focus-guards@1.1.1(@types/react@19.0.11)(react@19.0.0)':
+    dependencies:
+      react: 19.0.0
+    optionalDependencies:
+      '@types/react': 19.0.11
+
+  '@radix-ui/react-focus-scope@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.11)(react@19.0.0)
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+    optionalDependencies:
+      '@types/react': 19.0.11
+      '@types/react-dom': 19.0.4(@types/react@19.0.11)
+
+  '@radix-ui/react-id@1.1.0(@types/react@19.0.11)(react@19.0.0)':
+    dependencies:
+      '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.11)(react@19.0.0)
+      react: 19.0.0
+    optionalDependencies:
+      '@types/react': 19.0.11
+
+  '@radix-ui/react-popper@1.2.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@floating-ui/react-dom': 2.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-arrow': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-context': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-use-rect': 1.1.0(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-use-size': 1.1.0(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/rect': 1.1.0
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+    optionalDependencies:
+      '@types/react': 19.0.11
+      '@types/react-dom': 19.0.4(@types/react@19.0.11)
+
+  '@radix-ui/react-portal@1.1.4(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.11)(react@19.0.0)
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+    optionalDependencies:
+      '@types/react': 19.0.11
+      '@types/react-dom': 19.0.4(@types/react@19.0.11)
+
+  '@radix-ui/react-presence@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.11)(react@19.0.0)
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+    optionalDependencies:
+      '@types/react': 19.0.11
+      '@types/react-dom': 19.0.4(@types/react@19.0.11)
+
+  '@radix-ui/react-primitive@2.0.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@radix-ui/react-slot': 1.1.2(@types/react@19.0.11)(react@19.0.0)
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+    optionalDependencies:
+      '@types/react': 19.0.11
+      '@types/react-dom': 19.0.4(@types/react@19.0.11)
+
+  '@radix-ui/react-separator@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+    optionalDependencies:
+      '@types/react': 19.0.11
+      '@types/react-dom': 19.0.4(@types/react@19.0.11)
+
+  '@radix-ui/react-slot@1.1.2(@types/react@19.0.11)(react@19.0.0)':
+    dependencies:
+      '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      react: 19.0.0
+    optionalDependencies:
+      '@types/react': 19.0.11
+
+  '@radix-ui/react-tooltip@1.1.8(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@radix-ui/primitive': 1.1.1
+      '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-context': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-dismissable-layer': 1.1.5(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-id': 1.1.0(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-popper': 1.2.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-portal': 1.1.4(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-slot': 1.1.2(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-visually-hidden': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+    optionalDependencies:
+      '@types/react': 19.0.11
+      '@types/react-dom': 19.0.4(@types/react@19.0.11)
+
+  '@radix-ui/react-use-callback-ref@1.1.0(@types/react@19.0.11)(react@19.0.0)':
+    dependencies:
+      react: 19.0.0
+    optionalDependencies:
+      '@types/react': 19.0.11
+
+  '@radix-ui/react-use-controllable-state@1.1.0(@types/react@19.0.11)(react@19.0.0)':
+    dependencies:
+      '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.11)(react@19.0.0)
+      react: 19.0.0
+    optionalDependencies:
+      '@types/react': 19.0.11
+
+  '@radix-ui/react-use-escape-keydown@1.1.0(@types/react@19.0.11)(react@19.0.0)':
+    dependencies:
+      '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.11)(react@19.0.0)
+      react: 19.0.0
+    optionalDependencies:
+      '@types/react': 19.0.11
+
+  '@radix-ui/react-use-layout-effect@1.1.0(@types/react@19.0.11)(react@19.0.0)':
+    dependencies:
+      react: 19.0.0
+    optionalDependencies:
+      '@types/react': 19.0.11
+
+  '@radix-ui/react-use-rect@1.1.0(@types/react@19.0.11)(react@19.0.0)':
+    dependencies:
+      '@radix-ui/rect': 1.1.0
+      react: 19.0.0
+    optionalDependencies:
+      '@types/react': 19.0.11
+
+  '@radix-ui/react-use-size@1.1.0(@types/react@19.0.11)(react@19.0.0)':
+    dependencies:
+      '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.11)(react@19.0.0)
+      react: 19.0.0
+    optionalDependencies:
+      '@types/react': 19.0.11
+
+  '@radix-ui/react-visually-hidden@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+    optionalDependencies:
+      '@types/react': 19.0.11
+      '@types/react-dom': 19.0.4(@types/react@19.0.11)
+
+  '@radix-ui/rect@1.1.0': {}
+
   '@rollup/pluginutils@5.1.4(rollup@4.36.0)':
     dependencies:
       '@types/estree': 1.0.6
@@ -4723,6 +5316,10 @@ snapshots:
 
   argparse@2.0.1: {}
 
+  aria-hidden@1.2.4:
+    dependencies:
+      tslib: 2.8.1
+
   aria-query@5.3.0:
     dependencies:
       dequal: 2.0.3
@@ -4941,6 +5538,10 @@ snapshots:
 
   ci-info@4.2.0: {}
 
+  class-variance-authority@0.7.1:
+    dependencies:
+      clsx: 2.1.1
+
   cli-boxes@3.0.0: {}
 
   cli-width@4.1.0:
@@ -5037,6 +5638,8 @@ snapshots:
 
   detect-libc@2.0.3: {}
 
+  detect-node-es@1.1.0: {}
+
   deterministic-object-hash@2.0.2:
     dependencies:
       base-64: 1.0.0
@@ -5245,6 +5848,8 @@ snapshots:
 
   get-east-asian-width@1.3.0: {}
 
+  get-nonce@1.0.1: {}
+
   github-slugger@2.0.0: {}
 
   glob-parent@5.1.2:
@@ -6437,6 +7042,33 @@ snapshots:
 
   react-refresh@0.14.2: {}
 
+  react-remove-scroll-bar@2.3.8(@types/react@19.0.11)(react@19.0.0):
+    dependencies:
+      react: 19.0.0
+      react-style-singleton: 2.2.3(@types/react@19.0.11)(react@19.0.0)
+      tslib: 2.8.1
+    optionalDependencies:
+      '@types/react': 19.0.11
+
+  react-remove-scroll@2.6.3(@types/react@19.0.11)(react@19.0.0):
+    dependencies:
+      react: 19.0.0
+      react-remove-scroll-bar: 2.3.8(@types/react@19.0.11)(react@19.0.0)
+      react-style-singleton: 2.2.3(@types/react@19.0.11)(react@19.0.0)
+      tslib: 2.8.1
+      use-callback-ref: 1.3.3(@types/react@19.0.11)(react@19.0.0)
+      use-sidecar: 1.1.3(@types/react@19.0.11)(react@19.0.0)
+    optionalDependencies:
+      '@types/react': 19.0.11
+
+  react-style-singleton@2.2.3(@types/react@19.0.11)(react@19.0.0):
+    dependencies:
+      get-nonce: 1.0.1
+      react: 19.0.0
+      tslib: 2.8.1
+    optionalDependencies:
+      '@types/react': 19.0.11
+
   react@19.0.0: {}
 
   read-yaml-file@1.1.0:
@@ -6787,6 +7419,8 @@ snapshots:
       string-width: 4.2.3
       strip-ansi: 6.0.1
 
+  tailwind-merge@3.0.2: {}
+
   tailwindcss@4.0.14: {}
 
   tapable@2.2.1: {}
@@ -6883,6 +7517,8 @@ snapshots:
       turbo-windows-64: 2.4.4
       turbo-windows-arm64: 2.4.4
 
+  tw-animate-css@1.2.4: {}
+
   type-fest@0.21.3:
     optional: true
 
@@ -6990,6 +7626,21 @@ snapshots:
       requires-port: 1.0.0
     optional: true
 
+  use-callback-ref@1.3.3(@types/react@19.0.11)(react@19.0.0):
+    dependencies:
+      react: 19.0.0
+      tslib: 2.8.1
+    optionalDependencies:
+      '@types/react': 19.0.11
+
+  use-sidecar@1.1.3(@types/react@19.0.11)(react@19.0.0):
+    dependencies:
+      detect-node-es: 1.1.0
+      react: 19.0.0
+      tslib: 2.8.1
+    optionalDependencies:
+      '@types/react': 19.0.11
+
   util-deprecate@1.0.2: {}
 
   vfile-location@5.0.3:


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 10**

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: ASSERTIVE**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between a63c38e83c4c592b7024c4b4a2f54b5d8229c5f3 and eabe7b93d788c160ad0e82c38c1b9c8a0a6da929.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `pnpm-lock.yaml` is excluded by `!**/pnpm-lock.yaml`

</details>

<details>
<summary>📒 Files selected for processing (14)</summary>

* `playgrounds/ui-craft-playground/app/globals.css` (1 hunks)
* `playgrounds/ui-craft-playground/app/layout.tsx` (1 hunks)
* `playgrounds/ui-craft-playground/app/page.tsx` (1 hunks)
* `playgrounds/ui-craft-playground/components.json` (1 hunks)
* `playgrounds/ui-craft-playground/package.json` (1 hunks)
* `playgrounds/ui-craft-playground/shadcn-components/ui/button.tsx` (1 hunks)
* `playgrounds/ui-craft-playground/shadcn-components/ui/input.tsx` (1 hunks)
* `playgrounds/ui-craft-playground/shadcn-components/ui/separator.tsx` (1 hunks)
* `playgrounds/ui-craft-playground/shadcn-components/ui/sheet.tsx` (1 hunks)
* `playgrounds/ui-craft-playground/shadcn-components/ui/sidebar.tsx` (1 hunks)
* `playgrounds/ui-craft-playground/shadcn-components/ui/skeleton.tsx` (1 hunks)
* `playgrounds/ui-craft-playground/shadcn-components/ui/tooltip.tsx` (1 hunks)
* `playgrounds/ui-craft-playground/shadcn-hooks/use-mobile.ts` (1 hunks)
* `playgrounds/ui-craft-playground/shadcn-lib/utils.ts` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (7)</summary>

<details>
<summary>playgrounds/ui-craft-playground/shadcn-components/ui/input.tsx (1)</summary><blockquote>

<details>
<summary>playgrounds/ui-craft-playground/shadcn-lib/utils.ts (1) (1)</summary>

* `cn` (4-6)

</details>

</blockquote></details>
<details>
<summary>playgrounds/ui-craft-playground/shadcn-components/ui/skeleton.tsx (1)</summary><blockquote>

<details>
<summary>playgrounds/ui-craft-playground/shadcn-lib/utils.ts (1) (1)</summary>

* `cn` (4-6)

</details>

</blockquote></details>
<details>
<summary>playgrounds/ui-craft-playground/shadcn-components/ui/button.tsx (1)</summary><blockquote>

<details>
<summary>playgrounds/ui-craft-playground/shadcn-lib/utils.ts (1) (1)</summary>

* `cn` (4-6)

</details>

</blockquote></details>
<details>
<summary>playgrounds/ui-craft-playground/shadcn-components/ui/separator.tsx (1)</summary><blockquote>

<details>
<summary>playgrounds/ui-craft-playground/shadcn-lib/utils.ts (1) (1)</summary>

* `cn` (4-6)

</details>

</blockquote></details>
<details>
<summary>playgrounds/ui-craft-playground/shadcn-components/ui/sheet.tsx (1)</summary><blockquote>

<details>
<summary>playgrounds/ui-craft-playground/shadcn-lib/utils.ts (1) (1)</summary>

* `cn` (4-6)

</details>

</blockquote></details>
<details>
<summary>playgrounds/ui-craft-playground/shadcn-components/ui/tooltip.tsx (1)</summary><blockquote>

<details>
<summary>playgrounds/ui-craft-playground/shadcn-lib/utils.ts (1) (1)</summary>

* `cn` (4-6)

</details>

</blockquote></details>
<details>
<summary>playgrounds/ui-craft-playground/shadcn-components/ui/sidebar.tsx (8)</summary><blockquote>

<details>
<summary>playgrounds/ui-craft-playground/shadcn-hooks/use-mobile.ts (1) (1)</summary>

* `useIsMobile` (5-19)

</details>
<details>
<summary>playgrounds/ui-craft-playground/shadcn-components/ui/tooltip.tsx (4) (4)</summary>

* `TooltipProvider` (61-61)
* `TooltipContent` (61-61)
* `Tooltip` (61-61)
* `TooltipTrigger` (61-61)

</details>
<details>
<summary>playgrounds/ui-craft-playground/shadcn-lib/utils.ts (1) (1)</summary>

* `cn` (4-6)

</details>
<details>
<summary>playgrounds/ui-craft-playground/shadcn-components/ui/sheet.tsx (5) (5)</summary>

* `Sheet` (131-131)
* `SheetContent` (134-134)
* `SheetHeader` (135-135)
* `SheetTitle` (137-137)
* `SheetDescription` (138-138)

</details>
<details>
<summary>playgrounds/ui-craft-playground/shadcn-components/ui/button.tsx (1) (1)</summary>

* `Button` (59-59)

</details>
<details>
<summary>playgrounds/ui-craft-playground/shadcn-components/ui/input.tsx (1) (1)</summary>

* `Input` (21-21)

</details>
<details>
<summary>playgrounds/ui-craft-playground/shadcn-components/ui/separator.tsx (1) (1)</summary>

* `Separator` (28-28)

</details>
<details>
<summary>playgrounds/ui-craft-playground/shadcn-components/ui/skeleton.tsx (1) (1)</summary>

* `Skeleton` (13-13)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>playgrounds/ui-craft-playground/shadcn-components/ui/input.tsx</summary>

[error] 1-1: All these imports are only used as types.

Importing the types with import type ensures that they are removed by the compilers and avoids loading unnecessary modules.
Safe fix: Use import type.


(lint/style/useImportType)

</details>
<details>
<summary>playgrounds/ui-craft-playground/shadcn-components/ui/button.tsx</summary>

[error] 1-1: All these imports are only used as types.

Importing the types with import type ensures that they are removed by the compilers and avoids loading unnecessary modules.
Safe fix: Use import type.


(lint/style/useImportType)

</details>
<details>
<summary>playgrounds/ui-craft-playground/shadcn-components/ui/separator.tsx</summary>

[error] 3-3: All these imports are only used as types.

Importing the types with import type ensures that they are removed by the compilers and avoids loading unnecessary modules.
Safe fix: Use import type.


(lint/style/useImportType)

</details>
<details>
<summary>playgrounds/ui-craft-playground/shadcn-components/ui/sheet.tsx</summary>

[error] 3-3: All these imports are only used as types.

Importing the types with import type ensures that they are removed by the compilers and avoids loading unnecessary modules.
Safe fix: Use import type.


(lint/style/useImportType)

</details>
<details>
<summary>playgrounds/ui-craft-playground/shadcn-components/ui/tooltip.tsx</summary>

[error] 3-3: All these imports are only used as types.

Importing the types with import type ensures that they are removed by the compilers and avoids loading unnecessary modules.
Safe fix: Use import type.


(lint/style/useImportType)

</details>
<details>
<summary>playgrounds/ui-craft-playground/shadcn-components/ui/sidebar.tsx</summary>

[error] 5-5: Some named imports are only used as types.

This import is only used as a type.

Importing the types with import type ensures that they are removed by the compilers and avoids loading unnecessary modules.
Safe fix: Add inline type keywords.


(lint/style/useImportType)

---

[error] 92-92: This hook specifies more dependencies than necessary: setOpenMobile

This dependency can be removed from the list.


(lint/correctness/useExhaustiveDependencies)

---

[error] 116-116: This hook specifies more dependencies than necessary: setOpenMobile

This dependency can be removed from the list.


(lint/correctness/useExhaustiveDependencies)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (31)</summary><blockquote>

<details>
<summary>playgrounds/ui-craft-playground/components.json (1)</summary>

`1-21`: **shadcn/ui 구성 파일이 제대로 설정되었습니다.**

새로운 components.json 파일이 shadcn/ui 컴포넌트 라이브러리를 위해 적절하게 구성되었습니다. "new-york" 스타일, RSC 및 TSX 지원, Tailwind CSS 설정(zinc 기본 색상, CSS 변수 사용), 그리고 컴포넌트 및 유틸리티를 위한 유용한 경로 별칭들이 포함되어 있습니다. 이는 UI 컴포넌트의 일관성을 유지하는 데 도움이 될 것입니다.

</details>
<details>
<summary>playgrounds/ui-craft-playground/shadcn-lib/utils.ts (1)</summary>

`1-6`: **유틸리티 함수가 제대로 구현되었습니다.**

`cn` 함수는 클래스 이름을 관리하기 위한 일반적인 패턴을 잘 따르고 있습니다. `clsx`를 사용해 여러 클래스 값을 결합하고, `tailwind-merge`를 통해 Tailwind CSS 클래스 충돌을 해결하는 방식은 React 컴포넌트에서 조건부 스타일링을 처리하는 좋은 방법입니다. 이 유틸리티 함수는 프로젝트 전체에서 일관된 스타일링을 유지하는 데 매우 유용할 것입니다.

</details>
<details>
<summary>playgrounds/ui-craft-playground/app/page.tsx (1)</summary>

`3-3`: **명시적인 반환 타입 정의 좋습니다.**

Page 컴포넌트에 명시적인 반환 타입 `JSX.Element`를 추가하여 타입 안전성과 코드 가독성을 향상시킨 것은 좋은 개선사항입니다. 이러한 일관된 타입 선언은 코드베이스 전반의 유지보수성을 높이는 데 도움이 됩니다.

</details>
<details>
<summary>playgrounds/ui-craft-playground/shadcn-components/ui/skeleton.tsx (1)</summary>

`1-13`: **컴포넌트가 적절히 구현되었습니다.**

`Skeleton` 컴포넌트는 로딩 상태를 표시하기 위한 UI 컴포넌트로 잘 구현되었습니다. `cn` 유틸리티를 사용하여 기본 클래스와 사용자 정의 클래스를 효과적으로 결합하고 있습니다. `data-slot` 속성은 스타일링이나 테스트를 위한 선택자로 활용될 수 있어 유용합니다.

이 컴포넌트는 애니메이션 효과(`animate-pulse`)와 함께 로딩 상태를 시각적으로 표현하는 데 적합합니다.

</details>
<details>
<summary>playgrounds/ui-craft-playground/shadcn-components/ui/separator.tsx (1)</summary>

`8-26`: **Separator 컴포넌트가 잘 구현되었습니다.**

`Separator` 컴포넌트는 Radix UI 라이브러리를 기반으로 잘 구현되어 있습니다. 기본값 설정과 방향에 따른 스타일 적용이 적절히 처리되었습니다. `data-slot` 속성을 사용하여 컴포넌트 식별자를 추가한 것도 좋은 접근입니다.

`data-[orientation=horizontal]`, `data-[orientation=vertical]` 등의 데이터 속성을 활용한 조건부 스타일링 방식은 Tailwind CSS와 잘 통합되어 있어 효율적입니다.

</details>
<details>
<summary>playgrounds/ui-craft-playground/app/layout.tsx (1)</summary>

`3-7`: **명시적인 반환 타입 추가가 좋습니다.**

`RootLayout` 함수에 명시적인 반환 타입(`JSX.Element`)을 추가한 것은 타입 안전성을 높이는 좋은 변경사항입니다. 이는 코드의 가독성을 향상시키고 타입 오류를 방지하는 데 도움이 됩니다.

</details>
<details>
<summary>playgrounds/ui-craft-playground/shadcn-components/ui/button.tsx (1)</summary>

`7-36`: **버튼 스타일 정의가 잘 되어 있습니다.**

`buttonVariants` 정의가 체계적으로 잘 되어 있습니다. 다양한 스타일 변형과 크기 옵션을 제공하여 재사용성이 높습니다. Tailwind CSS와 함께 사용하기 좋은 구조로 되어 있습니다.

</details>
<details>
<summary>playgrounds/ui-craft-playground/package.json (3)</summary>

`14-17`: **Radix UI 의존성이 적절히 추가되었습니다.**

Radix UI 컴포넌트 라이브러리 의존성이 적절하게 추가되었습니다. 이 라이브러리들은 접근성과 상호작용성이 뛰어난 컴포넌트를 구현하는 데 도움이 됩니다.

---

`20-22`: **유틸리티 라이브러리 추가가 적절합니다.**

`class-variance-authority`, `clsx` 라이브러리의 추가는 컴포넌트 스타일링과 클래스 관리에 매우 유용합니다. 특히 조건부 클래스 적용과 버튼 같은 컴포넌트의 다양한 변형을 관리하는 데 효과적입니다.

---

`27-29`: **Tailwind 관련 라이브러리가 적절히 추가되었습니다.**

`tailwind-merge`와 `tw-animate-css`의 추가는 Tailwind CSS의 기능을 확장하고 애니메이션 효과를 쉽게 적용할 수 있게 해줍니다. 이 라이브러리들은 UI 개발 경험을 향상시키는 데 도움이 됩니다.

</details>
<details>
<summary>playgrounds/ui-craft-playground/app/globals.css (5)</summary>

`2-4`: **애니메이션 CSS 라이브러리와 다크 모드 설정이 추가되었습니다.**

`tw-animate-css` 라이브러리 임포트와 다크 모드 변형 설정이 적절히 추가되었습니다. 다크 모드 구현을 위한 좋은 기반이 됩니다.

---

`6-39`: **색상 변수 정의가 체계적입니다.**

Root 단계에서 다양한 UI 요소에 대한 색상 변수를 `oklch` 형식으로 정의한 것은 좋은 접근입니다. 이는 일관된 색상 시스템을 유지하는 데 도움이 됩니다. 특히 사이드바 관련 변수를 별도로 정의한 것은 PR의 목적에 맞게 잘 구성되었습니다.

---

`41-73`: **다크 모드 테마 설정이 잘 구현되었습니다.**

다크 모드를 위한 색상 변수 재정의가 체계적으로 잘 되어 있습니다. 밝은 테마와 어두운 테마 간의 색상 대비가 적절하여 사용자 경험을 향상시킬 수 있습니다.

---

`75-111`: **테마 변수가 재사용성 좋게 정의되었습니다.**

`@theme inline` 블록을 통해 기본 변수를 기반으로 한 파생 변수들이 잘 정의되어 있습니다. 이러한 구조는 테마 시스템의 유지보수성과 확장성을 높여줍니다.

---

`113-120`: **기본 스타일 레이어 설정이 적절합니다.**

`@layer base` 설정을 통해 기본 스타일을 일관되게 적용한 것은 좋은 접근입니다. 모든 요소에 대한 테두리와 아웃라인 스타일, 그리고 body에 대한 배경과 텍스트 색상을 Tailwind 클래스로 설정하여 일관성을 유지하고 있습니다.

</details>
<details>
<summary>playgrounds/ui-craft-playground/shadcn-components/ui/tooltip.tsx (2)</summary>

`8-19`: **TooltipProvider 컴포넌트가 잘 구현되었습니다.**

지연 시간을 설정할 수 있는 `delayDuration` 옵션을 제공하며 기본값을 0으로 설정한 것은 좋은 접근입니다. 데이터 속성을 통해 스타일링과 테스팅에 활용할 수 있도록 한 것도 좋습니다.

---

`21-35`: **Tooltip 및 TooltipTrigger 컴포넌트가 효율적으로 구현되었습니다.**

두 컴포넌트 모두 Radix UI의 프리미티브 컴포넌트를 적절히 래핑하고, 데이터 속성을 통해 스타일링과 테스팅에 활용할 수 있도록 구현되었습니다. 타입도 적절히 상속받아 사용하고 있습니다.

</details>
<details>
<summary>playgrounds/ui-craft-playground/shadcn-components/ui/sheet.tsx (6)</summary>

`3-7`: **[불필요한 'type' 전환은 아닌 것으로 보임]**

static analysis에서 "type import"를 권장하지만, 이 import들은 실제 런타임 JSX 렌더링에 사용되고 있어 'type import'로 전환할 필요는 없어 보입니다. 대부분 컴포넌트를 직접 호출하고 있으므로 현재 형태 그대로 유지해도 문제없습니다.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 3-3: All these imports are only used as types.

Importing the types with import type ensures that they are removed by the compilers and avoids loading unnecessary modules.
Safe fix: Use import type.


(lint/style/useImportType)

</details>

</details>

---

`9-23`: **[기본 컴포넌트 래핑 구현 확인]**

`Sheet`, `SheetTrigger`, `SheetClose` 모두 Radix UI 컴포넌트를 잘 감싸는 형태로 구현되었습니다. props 스프레드 방식도 문제없으며, 접근성 속성(`data-slot`, 등)도 적절히 추가되어 있습니다. 큰 이슈 없어 보이며, 재사용성도 좋습니다.

---

`25-45`: **[오버레이 애니메이션 유용성]**

`SheetPortal`과 `SheetOverlay` 구성은 Radix UI의 Portal 기능을 적절히 활용하며, 오버레이가 열리고 닫히는 처리를 명확히 해줍니다. 특히 `data-[state=open]`/`data-[state=closed]` 등 상태 기반 애니메이션이 유용하게 보입니다.

---

`47-82`: **[SheetContent 컴포넌트 전체 동작 검토]**

- 드롭다운 방향(side)에 따라 슬라이드 애니메이션 변화를 분기하는 로직이 깔끔하게 정리되어 있습니다.
- `SheetPortal` 내부에서 `SheetOverlay`와 함께 렌더링되므로 모달 형태를 유지하기에 충분합니다.
- 닫기 버튼(`SheetPrimitive.Close`)도 접근성이 잘 고려되어 있어서 문제 없어 보입니다.

---

`84-127`: **[Header/Footer/Title/Description 구조]**

`SheetHeader`, `SheetFooter`, `SheetTitle`, `SheetDescription` 컴포넌트들은 간단한 래핑과 className 병합 정도만 수행하며, 잘 분리되어 있습니다. UI 계층을 명확하게 구조화한다는 점에서 유지보수 및 확장성 면에서 적절해 보입니다.

---

`130-139`: **[일관된 API 내보내기]**

마지막 export 구문에서 필요한 컴포넌트가 모두 명시적으로 내보내지고 있습니다. 모듈 전체를 유연하게 재사용하기에 좋은 형태로 보이며, 불필요한 내보내기도 없습니다.

</details>
<details>
<summary>playgrounds/ui-craft-playground/shadcn-components/ui/sidebar.tsx (8)</summary>

`35-54`: **[Context 사용 로직 검토]**

`SidebarContext`와 `useSidebar()`는 사용 범위가 명확하고, 사용 시점에 오류 예방을 위해 `if (!context) { throw new Error(...) }` 처리가 되어 있습니다. 전반적으로 접근성이 좋고, 재사용하기에도 적절해 보입니다.

---

`56-151`: **[SidebarProvider 전반적 구조 점검]**

- `defaultOpen`/`openProp` 구분으로 내부·외부 제어 로직을 챙기는 점이 인상적입니다.
- 쿠키를 통해 사이드바 열린 상태를 유지하는 부분도 문제 없어 보입니다.
- 키보드 단축키(`Ctrl/Cmd + b`)로 토글하는 기능은 유용하며, 부수효과 최소화를 위해 event listener를 효과적으로 관리하고 있습니다.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 92-92: This hook specifies more dependencies than necessary: setOpenMobile

This dependency can be removed from the list.


(lint/correctness/useExhaustiveDependencies)

---

[error] 116-116: This hook specifies more dependencies than necessary: setOpenMobile

This dependency can be removed from the list.


(lint/correctness/useExhaustiveDependencies)

</details>

</details>

---

`92-95`: **[의존성 배열에서 'setOpenMobile' 제거 권고 여부]**

static analysis 도구에서 `useCallback`의 의존성 배열 중 `setOpenMobile`을 제거하라는 메시지가 발생하지만, 실제로 내부 콜백에서 `setOpenMobile`을 사용하고 있으므로 제거 시 기능에 영향이 있을 수 있습니다. 혹시 불필요한 오동작으로 인한 가짜 경고인지 재확인해 보시는 것을 권장합니다.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 92-92: This hook specifies more dependencies than necessary: setOpenMobile

This dependency can be removed from the list.


(lint/correctness/useExhaustiveDependencies)

</details>

</details>

---

`116-127`: **[useMemo 의존성 검사]**

동일하게 `setOpenMobile`이 의존성에서 필요 없다는 static analysis 경고가 있지만, `contextValue`에 해당 state setter를 포함해 반환하기 때문에 실제로는 의존성 배열에서 제거하면 의도치 않은 결과가 발생할 수 있습니다. 실제로 제거해도 문제가 없는지 재확인 후 반영 여부를 결정해 주세요.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 116-116: This hook specifies more dependencies than necessary: setOpenMobile

This dependency can be removed from the list.


(lint/correctness/useExhaustiveDependencies)

</details>

</details>

---

`154-253`: **[Sidebar 컴포넌트: 데스크톱·모바일 대응]**

- 모바일 환경에서는 `Sheet`를 활용해 사이드바가 오프캔버스 형태로 자연스러운 전환을 보여줍니다.
- 데스크톱 환경에서는 Tailwind 상태 클래스로 다양한 variant(오프캔버스, floating, inset)를 깔끔하게 지원하고 있습니다.
- UI/UX 흐름이 직관적이고, 접근성 속성(`data-slot`, etc.)도 확인되었습니다.

---

`256-280`: **[SidebarTrigger 로직 검토]**

`Button`을 래핑해 사이드바 토글을 간단히 호출하는 방식이 직관적이며, 클릭 핸들러에서 기존 `onClick`도 정상적으로 병행 호출됩니다. 접근성 측면에서 `aria-label` 대신 `sr-only` 텍스트로 대체 텍스트를 제공해 사용자 친화적입니다.

---

`282-699`: **[기타 구조( SidebarRail, SidebarInset 등 )]**

- `SidebarRail`, `SidebarInset`, `SidebarInput`, `SidebarHeader`, `SidebarFooter`, `SidebarSeparator`, 등등 모든 세부 컴포넌트는 역할별로 작게 분리되어, 사용 시 조합이 유연합니다.
- `group-data-[...]`와 같은 Tailwind 상태 커스텀을 적극 활용해 다양한 Micro-UX 제어도 쉽게 가능해 보입니다.
- 큰 로직 이슈나 보안상 취약점은 없어 보입니다.

---

`701-726`: **[마무리 내보내기 부분]**

마지막 export 구문에서 훅(`useSidebar`)과 컴포넌트를 일괄적으로 제공해주어 훨씬 재사용 가능성이 높아 보입니다. 별도 코드 스플리팅이나 트리셰이킹 시에도 문제없이 동작할 것으로 보입니다.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-03-19T08:16:21Z
user: login: coderabbitai[bot]
state: APPROVED
author_association: NONE
submitted_at: 2025-03-19T08:48:15Z

Review comments:
diff_hunk: @@ -0,0 +1,21 @@
+import * as React from "react"
+
+import { cn } from "@/shadcn-lib/utils"
+
+function Input({ className, type, ...props }: React.ComponentProps<"input">) {
+  return (
+    <input
+      type={type}
+      data-slot="input"
+      className={cn(
+        "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
+        "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
+        "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+        className
+      )}
+      {...props}
+    />
+  )
+}
path: playgrounds/ui-craft-playground/shadcn-components/ui/input.tsx
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Input 컴포넌트에 forwardRef 추가 고려**

현재 구현은 잘 되어 있지만, `forwardRef`를 사용하여 컴포넌트 외부에서 input 엘리먼트에 직접 접근할 수 있게 하면 더 유용할 수 있습니다. 특히 폼 라이브러리나 포커스 관리가 필요한 복잡한 UI에서 유용합니다.

```diff
-function Input({ className, type, ...props }: React.ComponentProps<"input">) {
+const Input = React.forwardRef<
+  HTMLInputElement,
+  React.ComponentProps<"input">
+>(({ className, type, ...props }, ref) => {
   return (
     <input
       type={type}
       data-slot="input"
       className={cn(
         "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
         "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
         "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
         className
       )}
+      ref={ref}
       {...props}
     />
   )
-}
+})
+
+Input.displayName = "Input"
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
const Input = React.forwardRef<
  HTMLInputElement,
  React.ComponentProps<"input">
>(({ className, type, ...props }, ref) => {
  return (
    <input
      type={type}
      data-slot="input"
      className={cn(
        "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
        "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
        "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
        className
      )}
      ref={ref}
      {...props}
    />
  )
})
Input.displayName = "Input"
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-19T08:16:19Z
author_association: NONE
diff_hunk: @@ -0,0 +1,21 @@
+import * as React from "react"
+
+import { cn } from "@/shadcn-lib/utils"
+
path: playgrounds/ui-craft-playground/shadcn-components/ui/input.tsx
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**타입 임포트 사용 권장**

정적 분석 도구에서 지적한 대로, 타입으로만 사용되는 임포트의 경우 `import type`을 사용하면 컴파일러가 불필요한 모듈 로딩을 방지할 수 있습니다.

```diff
-import * as React from "react"
+import type * as React from "react"
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
import type * as React from "react"

import { cn } from "@/shadcn-lib/utils"
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 1-1: All these imports are only used as types.

Importing the types with import type ensures that they are removed by the compilers and avoids loading unnecessary modules.
Safe fix: Use import type.


(lint/style/useImportType)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-19T08:16:19Z
author_association: NONE
diff_hunk: @@ -0,0 +1,28 @@
+"use client"
+
+import * as React from "react"
+import * as SeparatorPrimitive from "@radix-ui/react-separator"
+
+import { cn } from "@/shadcn-lib/utils"
+
path: playgrounds/ui-craft-playground/shadcn-components/ui/separator.tsx
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**타입 임포트 최적화가 필요합니다.**

React를 임포트할 때 타입으로만 사용되는 경우 `import type`을 사용하는 것이 더 효율적입니다. 이렇게 하면 컴파일러가 불필요한 모듈 로딩을 제거할 수 있습니다.


```diff
-import * as React from "react"
+import type * as React from "react"
 import * as SeparatorPrimitive from "@radix-ui/react-separator"

 import { cn } from "@/shadcn-lib/utils"
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
"use client"

import type * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"

import { cn } from "@/shadcn-lib/utils"
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 3-3: All these imports are only used as types.

Importing the types with import type ensures that they are removed by the compilers and avoids loading unnecessary modules.
Safe fix: Use import type.


(lint/style/useImportType)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-19T08:16:19Z
author_association: NONE
diff_hunk: @@ -0,0 +1,19 @@
+import * as React from "react"
+
+const MOBILE_BREAKPOINT = 768
+
+export function useIsMobile() {
+  const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
+
+  React.useEffect(() => {
+    const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
+    const onChange = () => {
+      setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
+    }
+    mql.addEventListener("change", onChange)
+    setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
+    return () => mql.removeEventListener("change", onChange)
+  }, [])
+
+  return !!isMobile
+}
path: playgrounds/ui-craft-playground/shadcn-hooks/use-mobile.ts
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**useIsMobile 훅의 상태 초기화 최적화가 필요합니다.**

`useIsMobile` 훅은 반응형 UI를 구현하는 데 유용하지만, 상태 초기화 방식에 개선이 필요합니다. 현재 undefined로 초기화한 후 마지막에 `!!` 연산자를 사용하여 boolean으로 변환하고 있는데, 처음부터 boolean으로 초기화하는 것이 더 명확합니다.


```diff
export function useIsMobile() {
-  const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
+  const [isMobile, setIsMobile] = React.useState<boolean>(false)

  React.useEffect(() => {
    const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
    const onChange = () => {
      setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
    }
    mql.addEventListener("change", onChange)
    setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
    return () => mql.removeEventListener("change", onChange)
  }, [])

-  return !!isMobile
+  return isMobile
}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
export function useIsMobile() {
  const [isMobile, setIsMobile] = React.useState<boolean>(false)

  React.useEffect(() => {
    const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
    const onChange = () => {
      setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
    }
    mql.addEventListener("change", onChange)
    setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
    return () => mql.removeEventListener("change", onChange)
  }, [])

  return isMobile
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-19T08:16:19Z
author_association: NONE
diff_hunk: @@ -0,0 +1,19 @@
+import * as React from "react"
+
+const MOBILE_BREAKPOINT = 768
+
path: playgrounds/ui-craft-playground/shadcn-hooks/use-mobile.ts
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**상수 값에 대한 설명 추가가 필요합니다.**

`MOBILE_BREAKPOINT` 상수는 모바일 디바이스 감지에 중요한 값입니다. 이 값이 왜 768px로 설정되었는지에 대한 간단한 주석을 추가하면 코드 이해도가 높아질 것입니다.


```diff
import * as React from "react"

+// 태블릿/모바일 디바이스의 기준점 (일반적인 중간 크기 브레이크포인트)
const MOBILE_BREAKPOINT = 768
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
import * as React from "react"

// 태블릿/모바일 디바이스의 기준점 (일반적인 중간 크기 브레이크포인트)
const MOBILE_BREAKPOINT = 768
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-19T08:16:19Z
author_association: NONE
diff_hunk: @@ -0,0 +1,59 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/shadcn-lib/utils"
path: playgrounds/ui-craft-playground/shadcn-components/ui/button.tsx
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**타입 임포트 최적화가 필요합니다.**

리액트 타입 임포트를 최적화하면 번들 사이즈를 줄이고 컴파일 효율성을 높일 수 있습니다. 정적 분석 도구에서도 이 문제를 지적하고 있습니다.


```diff
-import * as React from "react"
+import * as React from "react"
+import type { ComponentProps } from "react"
 import { Slot } from "@radix-ui/react-slot"
-import { cva, type VariantProps } from "class-variance-authority"
+import { cva } from "class-variance-authority"
+import type { VariantProps } from "class-variance-authority"
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
import * as React from "react"
import type { ComponentProps } from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva } from "class-variance-authority"
import type { VariantProps } from "class-variance-authority"

import { cn } from "@/shadcn-lib/utils"
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 1-1: All these imports are only used as types.

Importing the types with import type ensures that they are removed by the compilers and avoids loading unnecessary modules.
Safe fix: Use import type.


(lint/style/useImportType)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-19T08:16:20Z
author_association: NONE
diff_hunk: @@ -0,0 +1,59 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/shadcn-lib/utils"
+
+const buttonVariants = cva(
+  "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+  {
+    variants: {
+      variant: {
+        default:
+          "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
+        destructive:
+          "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+        outline:
+          "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
+        secondary:
+          "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
+        ghost:
+          "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
+        link: "text-primary underline-offset-4 hover:underline",
+      },
+      size: {
+        default: "h-9 px-4 py-2 has-[>svg]:px-3",
+        sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
+        lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
+        icon: "size-9",
+      },
+    },
+    defaultVariants: {
+      variant: "default",
+      size: "default",
+    },
+  }
+)
+
+function Button({
+  className,
+  variant,
+  size,
+  asChild = false,
+  ...props
+}: React.ComponentProps<"button"> &
+  VariantProps<typeof buttonVariants> & {
+    asChild?: boolean
+  }) {
+  const Comp = asChild ? Slot : "button"
+
+  return (
+    <Comp
+      data-slot="button"
+      className={cn(buttonVariants({ variant, size, className }))}
+      {...props}
+    />
+  )
+}
path: playgrounds/ui-craft-playground/shadcn-components/ui/button.tsx
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Button 컴포넌트 구현이 효율적입니다.**

`asChild` 속성을 통해 다른 컴포넌트로 렌더링할 수 있는 기능은 유연한 컴포넌트 설계의 좋은 예시입니다. 하지만 타입 정의 부분을 개선할 수 있습니다.


```diff
 function Button({
   className,
   variant,
   size,
   asChild = false,
   ...props
-}: React.ComponentProps<"button"> &
+}: ComponentProps<"button"> &
   VariantProps<typeof buttonVariants> & {
     asChild?: boolean
   }) {
   const Comp = asChild ? Slot : "button"
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
function Button({
  className,
  variant,
  size,
  asChild = false,
  ...props
}: ComponentProps<"button"> &
  VariantProps<typeof buttonVariants> & {
    asChild?: boolean
  }) {
  const Comp = asChild ? Slot : "button"

  return (
    <Comp
      data-slot="button"
      className={cn(buttonVariants({ variant, size, className }))}
      {...props}
    />
  )
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-19T08:16:20Z
author_association: NONE
diff_hunk: @@ -0,0 +1,61 @@
+"use client"
+
+import * as React from "react"
+import * as TooltipPrimitive from "@radix-ui/react-tooltip"
+
+import { cn } from "@/shadcn-lib/utils"
+
+function TooltipProvider({
+  delayDuration = 0,
+  ...props
+}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
+  return (
+    <TooltipPrimitive.Provider
+      data-slot="tooltip-provider"
+      delayDuration={delayDuration}
+      {...props}
+    />
+  )
+}
+
+function Tooltip({
+  ...props
+}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
+  return (
+    <TooltipProvider>
+      <TooltipPrimitive.Root data-slot="tooltip" {...props} />
+    </TooltipProvider>
+  )
+}
+
+function TooltipTrigger({
+  ...props
+}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
+  return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
+}
+
+function TooltipContent({
+  className,
+  sideOffset = 0,
+  children,
+  ...props
+}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
+  return (
+    <TooltipPrimitive.Portal>
+      <TooltipPrimitive.Content
+        data-slot="tooltip-content"
+        sideOffset={sideOffset}
+        className={cn(
+          "bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
+          className
+        )}
+        {...props}
+      >
+        {children}
+        <TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
+      </TooltipPrimitive.Content>
+    </TooltipPrimitive.Portal>
+  )
+}
path: playgrounds/ui-craft-playground/shadcn-components/ui/tooltip.tsx
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**TooltipContent 컴포넌트에 애니메이션과 스타일이 잘 적용되었습니다.**

Portal을 사용하여 DOM 계층 외부에 렌더링하는 방식은 적절합니다. `cn` 유틸리티를 활용한 클래스 결합과 애니메이션 효과가 잘 적용되어 있습니다. 다만, 타입 정의 부분을 개선할 수 있습니다.


```diff
 function TooltipContent({
   className,
   sideOffset = 0,
   children,
   ...props
-}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
+}: ComponentProps<typeof TooltipPrimitive.Content>) {
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
function TooltipContent({
  className,
  sideOffset = 0,
  children,
  ...props
}: ComponentProps<typeof TooltipPrimitive.Content>) {
  return (
    <TooltipPrimitive.Portal>
      <TooltipPrimitive.Content
        data-slot="tooltip-content"
        sideOffset={sideOffset}
        className={cn(
          "bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
          className
        )}
        {...props}
      >
        {children}
        <TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
      </TooltipPrimitive.Content>
    </TooltipPrimitive.Portal>
  )
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-19T08:16:20Z
author_association: NONE
diff_hunk: @@ -0,0 +1,61 @@
+"use client"
+
+import * as React from "react"
+import * as TooltipPrimitive from "@radix-ui/react-tooltip"
+
+import { cn } from "@/shadcn-lib/utils"
+
path: playgrounds/ui-craft-playground/shadcn-components/ui/tooltip.tsx
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**클라이언트 컴포넌트 지시자와 임포트가 적절합니다.**

Next.js 환경에서 "use client" 지시자를 사용하여 클라이언트 컴포넌트로 명시한 것은 적절합니다. 다만, 타입 임포트 최적화를 적용하면 좋을 것 같습니다.


```diff
 "use client"

-import * as React from "react"
+import * as React from "react"
+import type { ComponentProps } from "react"
 import * as TooltipPrimitive from "@radix-ui/react-tooltip"

 import { cn } from "@/shadcn-lib/utils"
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
"use client"

import * as React from "react"
import type { ComponentProps } from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"

import { cn } from "@/shadcn-lib/utils"
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 3-3: All these imports are only used as types.

Importing the types with import type ensures that they are removed by the compilers and avoids loading unnecessary modules.
Safe fix: Use import type.


(lint/style/useImportType)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-19T08:16:20Z
author_association: NONE
diff_hunk: @@ -0,0 +1,726 @@
+"use client"
+
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { VariantProps, cva } from "class-variance-authority"
path: playgrounds/ui-craft-playground/shadcn-components/ui/sidebar.tsx
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**[일부 import를 'type'으로 전환 가능 여부 확인]**

`VariantProps`와 같은 타입 전용 import는 `import type { VariantProps } from "class-variance-authority"` 형태로 변경할 수 있습니다. 빌드 시 사용되지 않는 모듈을 제거해 번들 크기를 조금이라도 절약할 수 있습니다. 다만, 실제 런타임 사용이 있다면 그대로 두셔야 합니다.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 5-5: Some named imports are only used as types.

This import is only used as a type.

Importing the types with import type ensures that they are removed by the compilers and avoids loading unnecessary modules.
Safe fix: Add inline type keywords.


(lint/style/useImportType)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-19T08:16:20Z
author_association: NONE

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-03-19T08:10:42Z
body: ###  ⚠️  No Changeset found

Latest commit: eabe7b93d788c160ad0e82c38c1b9c8a0a6da929

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/siluat/web-projects/new/ui-craft-playground/add-sidebar-1?filename=.changeset/slimy-kids-kiss.md&value=---%0A%22ui-craft-playground%22%3A%20patch%0A---%0A%0A%F0%9F%92%84%20(ui-craft-playground)%20Add%20shadcn%2Fui%20sidebar%0A)


author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-03-19T08:10:49Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough
이번 변경사항은 UI Craft Playground 애플리케이션의 전역 스타일과 테마 설정을 확장하고, React 컴포넌트의 반환 타입을 명시하며, 새로운 구성 파일과 여러 의존성을 추가한 것입니다. 또한 다양한 UI 컴포넌트(버튼, 입력창, 구분선, 시트, 사이드바, 스켈레톤, 툴팁), 커스텀 훅 및 유틸리티 함수가 도입되어 프로젝트의 디자인, 반응형 및 접근성 기능을 강화합니다.

## Changes

| 파일 경로 | 변경 내용 요약 |
| --- | --- |
| `playgrounds/.../app/globals.css` | CSS 커스텀 프로퍼티 추가, 다크 테마(.dark), `@theme inline`, `@layer base` 블록 및 `"tw-animate-css"` import 추가 |
| `playgrounds/.../app/layout.tsx`<br>`playgrounds/.../app/page.tsx` | React 함수 컴포넌트(`RootLayout`, `Page`)의 반환 타입을 `JSX.Element`로 명시 |
| `playgrounds/.../components.json`<br>`playgrounds/.../package.json` | UI Craft Playground용 구성 파일 추가 및 Tailwind, Radix UI, 기타 유틸리티 의존성 다수 추가  |
| `playgrounds/.../shadcn-components/ui/{button,input,separator,sheet,sidebar,skeleton,tooltip}.tsx` | 버튼, 입력창, 구분선, 시트, 사이드바, 스켈레톤, 툴팁 등 다양한 UI 컴포넌트를 신규 추가 |
| `playgrounds/.../shadcn-hooks/use-mobile.ts` | 모바일 뷰 감지를 위한 커스텀 훅 `useIsMobile` 추가 |
| `playgrounds/.../shadcn-lib/utils.ts` | `clsx`와 `tailwind-merge`를 활용한 `cn` 유틸리티 함수 추가 |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant U as 사용자
    participant ST as SidebarTrigger
    participant SP as SidebarProvider
    participant SC as SidebarContent/Sheet

    U->>ST: 사이드바 토글 클릭
    ST->>SP: 토글 상태 업데이트 요청
    SP->>SC: 사이드바/시트 렌더링 상태 변경
    alt 모바일 환경 감지 (useIsMobile)
        SP->>SC: Sheet 컴포넌트로 전환
    end
```

## Poem
> 안녕, 부드러운 코드의 숲 속에서,  
> 토끼 나는 새 변화를 노래해요.  
> CSS 색채가 반짝이고,  
> 컴포넌트들의 춤판이 열리네.  
> 새로운 훅과 도구들이 모여,  
> 즐거운 코드의 여정, 함께 뛰어요! 🐰✨

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!TIP]
> <details>
> <summary>⚡🧪 Multi-step agentic review comment chat (experimental)</summary>
> 
> - We're introducing multi-step agentic chat in review comments. This experimental feature enhances review discussions with the CodeRabbit agentic chat by enabling advanced interactions, including the ability to create pull requests directly from comments.
> 						- To enable this feature, set early_access to true under in the settings.
> 
> </details>

<!-- announcements_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=siluat/web-projects&utm_content=12):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE