# ikui > ikui is a curated collection of reusable React components, blocks, and templates for building captivating landing pages and user-focused marketing materials. Built with React, Tailwind CSS, and Motion, it draws heavy inspiration from shadcn/ui with a magical twist. Components are copy-ready and installable via the shadcn CLI. This file contains the full text of every ikui documentation page, concatenated for LLM consumption. For a lighter index with links, see `/llms.txt`. --- # Introduction ikui is a curated collection of reusable components, blocks, and templates designed to help you build captivating landing pages and user-focused marketing materials with ease. ### Philosophy Great design is more than just aesthetics. It's a cornerstone of building trust with your audience. In a digital world, your design is often the first impression a visitor has of your brand. It’s the key to answering critical questions in their minds before they commit to engaging with your business, whether that’s signing up, making a purchase, or sharing their data. Visitors might wonder: - Is this brand trustworthy? - Does this feel professional and reliable? - Will my experience with this product be as polished as its presentation? Poor design can signal carelessness or lack of attention to detail, leaving users hesitant. Thoughtful, high-quality design communicates confidence, reliability, and care. It suggests that if a team invests in the smallest details of their interface, they likely prioritize excellence in their product and customer experience as well. A shining example of this philosophy in action is the landing page for Vercel, which has inspired countless users since its debut. Its clean, intentional design instantly conveys quality, making you trust the product before even trying it. ikui draws heavy inspiration from the elegance and simplicity of shadcn/ui, reimagining it with a magical twist to help you craft delightful, trust-building user experiences. ## Community - [Github Repository](https://github.com/WuChenDi/ikui) - [Telegram](https://t.me/wuchendi) - [Blog](https://wcd.pages.dev/) - [Twitter/X](https://x.com/wuchendi96) --- # Get Started This guide walks you through the essential steps to seamlessly integrate ikui into your shadcn project. ## Prerequisites ikui is built on the latest **React 19** and [Tailwind CSS v4](https://tailwindcss.com). Ensure your project is set up with these technologies before proceeding. We recommend following the [Official shadcn/ui Installation Guide](https://ui.shadcn.com/docs/installation) and [shadcn/create](https://ui.shadcn.com/create) setup to prepare your environment. ## Set up ikui Registry Add the ikui registry namespace to your `components.json` and set your component library and style. Learn more about registry config from [shadcn registry docs](https://ui.shadcn.com/docs/registry). ```json { ... "style": "base-nova", ... "registries": { "@ikui": "https://ik-ui.pages.dev/r/{name}.json" } } ``` ## Components You can integrate ikui using the [**shadcn CLI**](https://ui.shadcn.com/docs/cli) for automation or by manual installation for full control. Both methods apply to everything in [Docs](/docs/introduction) and the [component catalog](/docs/components), including blocks you install with `@ikui/…`. ```bash npx shadcn@latest add @ikui/copy-button ``` ## AI-Enhanced Workflow ikui is designed from the ground up to be **AI-friendly**, making it easier for language models to understand and modify your UI. - **MCP**: Connect ikui through [MCP](/docs/mcp) so your IDE can find, install, and compose components for you. - **llms.txt**: Access our [llms.txt](/llms.txt) file for a structured map of our documentation and architecture optimized for AI agents. - **Copy Markdown**: Every page includes a **Copy Markdown** feature, allowing you to instantly feed documentation or code into your AI-driven development workflows. --- # Components Browse all available components in ikui. Click on a component to view its documentation. --- # Skills Skills give AI assistants like Claude Code project-aware knowledge of ikui. Once installed, your assistant knows how to find, install, compose, and customize ikui's media and timeline components using the correct registry, APIs, and conventions for your project. For example, you can ask your assistant to: - _"Add an ikui waveform player."_ - _"Build a video trimmer that exports the cut to MP4."_ - _"Drop in the audio trimmer block."_ - _"Add a before/after image compare slider."_ - _"Let users crop their avatar."_ The skill reads your project's `components.json` and pulls the **live component manifest** from the `@ikui` registry at load time, so the assistant always sees the current list of components — and resolves `@ikui/` against your real aliases and package runner — with no hardcoded inventory to fall out of date. ## Install 1. Install the skill with the [`skills`](https://github.com/WuChenDi/skills) CLI. ```bash npx skills add wuchendi/skills --skill ikui --global ``` 2. Register the `@ikui` registry in your project. Add the following to your `components.json` so the assistant can resolve `@ikui/`: ```json { "registries": { "@ikui": "https://ik-ui.pages.dev/r/{name}.json" } } ``` Once installed, the skill loads automatically when you ask your assistant to work with ikui, or when your project's `components.json` contains the `@ikui` registry. Learn more about skills at [skills.sh](https://skills.sh). ## What's Included The skill gives your assistant the following knowledge: ### Live Component Manifest On every interaction it fetches `registry.json` from the registry to get the current components and blocks — names, types, categories, and descriptions. The list is never hardcoded, so it stays accurate as ikui grows. ### Primitives vs Blocks The single most important routing decision. **Primitives** (`registry:component`) are small, single-purpose parts — a waveform, a thumbnail strip, a timeline ruler/element/playhead, a crop box — for building your own feature. **Blocks** (`registry:block`) are finished business compositions with all the wiring done — `audio-trimmer`, `video-trimmer`, `image-cropper`. The skill reaches for a block when you name an outcome, and drops to primitives when you're assembling something custom. ### Installation via the shadcn CLI ikui has no CLI of its own — everything installs through the shadcn CLI pointed at `@ikui`. The skill knows how to add by registry id or full URL, preview with `--dry-run`, and let `registryDependencies` (other ikui items and plain shadcn components) and npm `dependencies` resolve transitively. ### Composition Map How the media and timeline stack fits together: `audio-waveform` → `waveform-player`; `video-thumbnail-cache` → `thumbnail-strip` → `segmented-timeline-strip`; the `timeline-ruler` / `timeline-element` / `timeline-playhead` primitives that share one `time × pixelsPerSecond × zoom` basis; and the image set (`image-compare`, `image-crop`, `particle-image`). ### Conventions The rules ikui code follows: **Base UI** (`@base-ui/react`) — never Radix; RSC-default Next.js App Router with `"use client"` where needed; Tailwind v4 with semantic tokens and `cn()`; `lucide-react` icons and `motion` animations; imports resolved against your project's real aliases. ## How It Works 1. **Detection** — The skill activates when you ask your assistant to work with ikui, or when your project's `components.json` declares the `@ikui` registry. 2. **Live manifest** — It fetches `registry.json` and reads your `components.json`, injecting the current component list and your project config into the assistant's context. 3. **Grain routing** — It decides whether your request wants a finished **block** or the underlying **primitives**, instead of reimplementing parts that already exist. 4. **Convention enforcement** — Generated code follows ikui's rules: Base UI over Radix, `"use client"` where required, semantic Tailwind tokens, and your project's import aliases. ## Skill vs MCP They are complementary, not alternatives: - **[MCP](/docs/mcp)** gives your assistant a live connection to the registry — it can search and add items on demand. - **The Skill** gives your assistant ikui-specific judgment — how the pieces compose, which conventions to honor, and whether a request wants a primitive or a block. Use them together: MCP fetches, the Skill knows how to wire what it fetched. ## Learn More - [Components](/docs/components) — Browse every component and block in ikui - [MCP Server](/docs/mcp) — Connect the registry over MCP for on-demand search and install - [WuChenDi/skills](https://github.com/WuChenDi/skills) — The skill source - [skills.sh](https://skills.sh) — Learn more about AI skills --- # MCP ## Installation 1. Enable [MCP](https://modelcontextprotocol.io/docs/getting-started/intro) in your project environment. (Supports Claude Code, Cursor, etc.) 2. Add the registry to your project Add the following to your components.json file: ```json { "registries": { "@ikui": "https://ik-ui.pages.dev/r/{name}.json" } } ``` ### Usage You can now ask your IDE to use any ikui component. Here are some examples: - "Add a badge component" - "Add a blur reveal animation" - "Add a vertical marquee of logos" --- # LLMs ikui is proudly hand-crafted, but we want to encourage any and all usage of ikui — even when you are building with an AI coding agent like Claude Code, Cursor, or Copilot. Give one or more of these files to your LLM to teach it how to find, install, and compose ikui components: ## llms.txt An index of ikui's documentation. The agent reads it to discover pages and then dives into the ones it needs. [View llms.txt](/llms.txt). ## llms-full.txt The entire ikui documentation in a single file — every guide and component page concatenated for one-shot context. [View llms-full.txt](/llms-full.txt). ## Per-page Markdown Every documentation page is also available as clean Markdown: append `.md` to any docs URL (for example `/docs/copy-button.md`), or use the **Copy Markdown** button in the page header to feed a single page into your AI workflow. ## MCP Prefer a live connection? The [MCP server](/docs/mcp) lets your IDE find, install, and compose ikui components on demand. --- # Chart ```tsx 'use client' import { TrendingUp } from 'lucide-react' import { Bar, BarChart, CartesianGrid, XAxis } from 'recharts' import { Badge } from '@/components/ui/badge' import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card' import type { ChartConfig } from '@/components/ikui/chart' import { ChartContainer, ChartTooltip, ChartTooltipContent, } from '@/components/ikui/chart' const chartData = [ { month: 'Jan', desktop: 300 }, { month: 'Feb', desktop: 550 }, { month: 'Mar', desktop: 400 }, { month: 'Apr', desktop: 630 }, { month: 'May', desktop: 460 }, { month: 'Jun', desktop: 780 }, { month: 'Jul', desktop: 390 }, { month: 'Aug', desktop: 925 }, { month: 'Sep', desktop: 645 }, { month: 'Oct', desktop: 530 }, { month: 'Nov', desktop: 700 }, { month: 'Dec', desktop: 270 }, ] const chartConfig = { desktop: { label: 'Desktop', color: 'var(--chart-1)', }, } satisfies ChartConfig export function Demo() { return ( Revenue Growth Monthly revenue performance tracking value.slice(0, 3)} /> { return (
{value} 2024
) }} formatter={(value, name) => (
{chartConfig[name as keyof typeof chartConfig] ?.label || name}
${Number(value).toLocaleString()}
)} /> } /> ) } ``` Recharts-based chart primitives with theme-aware colors. `ChartContainer` provides the responsive wrapper and maps your `ChartConfig` to CSS color variables (`--color-`); `ChartTooltip`, `ChartTooltipContent`, `ChartLegend`, and `ChartLegendContent` cover the surrounding chrome. Compose the chart body itself from Recharts elements (Bar, Area, Line, Pie, Radar, …). ## Installation ```bash npx shadcn@latest add @ikui/chart ``` ## Usage ```tsx import { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig, } from "@/components/chart"; import { Bar, BarChart, XAxis } from "recharts"; ``` ```tsx const chartConfig = { desktop: { label: "Desktop", color: "var(--chart-1)" }, } satisfies ChartConfig; } /> ``` ## Examples ### Multiple bars Plot several series side by side — each `Bar` maps to a `chartConfig` key and pulls its color from the matching `--color-` variable. ```tsx 'use client' import { TrendingUp } from 'lucide-react' import type { CSSProperties } from 'react' import { Bar, BarChart, CartesianGrid, XAxis } from 'recharts' import { Badge } from '@/components/ui/badge' import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card' import type { ChartConfig } from '@/components/ikui/chart' import { ChartContainer, ChartTooltip, ChartTooltipContent, } from '@/components/ikui/chart' const chartData = [ { month: 'January', desktop: 120, mobile: 80 }, { month: 'February', desktop: 250, mobile: 200 }, { month: 'March', desktop: 230, mobile: 120 }, { month: 'April', desktop: 70, mobile: 190 }, { month: 'May', desktop: 209, mobile: 130 }, { month: 'June', desktop: 210, mobile: 140 }, ] const chartConfig = { desktop: { label: 'Desktop', color: 'var(--chart-1)', }, mobile: { label: 'Mobile', color: 'var(--chart-2)', }, } satisfies ChartConfig export function Demo() { return ( Market Share Departmental performance comparison value.slice(0, 3)} /> { return (
{value} 2024
) }} formatter={(value, name) => (
{chartConfig[name as keyof typeof chartConfig] ?.label || name}
{Number(value).toLocaleString()}
)} /> } /> ) } ``` ### Gradient area An `AreaChart` filled with an SVG `linearGradient` referencing the series color, for a soft trend surface. ```tsx 'use client' import { TrendingUp } from 'lucide-react' import type { CSSProperties } from 'react' import { Area, AreaChart, CartesianGrid, XAxis } from 'recharts' import { Badge } from '@/components/ui/badge' import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card' import type { ChartConfig } from '@/components/ikui/chart' import { ChartContainer, ChartTooltip, ChartTooltipContent, } from '@/components/ikui/chart' const chartData = [ { month: 'January', visitors: 2400 }, { month: 'February', visitors: 2850 }, { month: 'March', visitors: 2600 }, { month: 'April', visitors: 3100 }, { month: 'May', visitors: 2900 }, { month: 'June', visitors: 3400 }, ] const chartConfig = { visitors: { label: 'Visitors', color: 'var(--chart-1)', }, } satisfies ChartConfig export function Demo() { return ( Website Traffic Monthly unique visitor trends value.slice(0, 3)} /> (
{value} 2024
)} formatter={(value, name) => (
{chartConfig[name as keyof typeof chartConfig] ?.label || name}
{Number(value).toLocaleString()}
)} /> } /> ) } ``` ### Line with forecast A `ComposedChart` layering a `Line` over a patterned `Area` to highlight a projected trend, with a legend driven by `ChartLegendContent`. ```tsx 'use client' import type { CSSProperties } from 'react' import { Area, CartesianGrid, ComposedChart, Line, XAxis } from 'recharts' import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card' import type { ChartConfig } from '@/components/ikui/chart' import { ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent, } from '@/components/ikui/chart' const chartData = [ { month: 'January', forecast: 2600, forecastArea: 2600 }, { month: 'February', forecast: 4200, forecastArea: 4200 }, { month: 'March', forecast: 2400, forecastArea: 2400 }, { month: 'April', forecast: 5000, forecastArea: 5000 }, { month: 'May', forecast: 2800, forecastArea: 2800 }, { month: 'June', forecast: 5800, forecastArea: 5800 }, { month: 'July', forecast: 3200, forecastArea: 3200 }, { month: 'August', forecast: 6200, forecastArea: 6200 }, { month: 'September', forecast: 3800, forecastArea: 3800 }, ] const chartConfig = { forecast: { label: 'Forecast', color: 'var(--chart-4)' }, forecastArea: { label: 'Forecast', color: 'var(--chart-4)' }, } satisfies ChartConfig export function Demo() { return ( Sales Forecast Projected sales performance with trends value.slice(0, 3)} /> (
{value} 2024
)} formatter={(value, name) => (
{chartConfig[name as keyof typeof chartConfig] ?.label || name}
{value != null ? `$${Number(value).toLocaleString()}` : '—'}
)} /> } /> } /> ) } ``` ### Donut with center label A `PieChart` with `innerRadius` and an active `Sector`, using a `Label` to render a total in the middle. ```tsx 'use client' import type { CSSProperties } from 'react' import type { PieSectorShapeProps } from 'recharts' import { Label, Pie, PieChart, Sector } from 'recharts' import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card' import type { ChartConfig } from '@/components/ikui/chart' import { ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent, } from '@/components/ikui/chart' const chartData = [ { plan: 'free', users: 12800, fill: 'var(--color-free)' }, { plan: 'starter', users: 5400, fill: 'var(--color-starter)' }, { plan: 'pro', users: 3600, fill: 'var(--color-pro)' }, { plan: 'enterprise', users: 1200, fill: 'var(--color-enterprise)' }, ] const totalUsers = chartData.reduce((sum, d) => sum + d.users, 0) const paidUsers = totalUsers - chartData[0].users const conversionRate = ((paidUsers / totalUsers) * 100).toFixed(1) const chartConfig = { users: { label: 'Users' }, free: { label: 'Free', color: 'var(--chart-5)' }, starter: { label: 'Starter', color: 'var(--chart-3)' }, pro: { label: 'Pro', color: 'var(--chart-2)' }, enterprise: { label: 'Enterprise', color: 'var(--chart-1)' }, } satisfies ChartConfig export function Demo() { return ( Conversion Funnel {conversionRate}% of users are on paid plans (
{chartConfig[name as keyof typeof chartConfig] ?.label || name}
{Number(value).toLocaleString()}
)} /> } /> } className="-translate-y-2" /> ( )} > ) } ``` ### Radar A `RadarChart` over a `PolarGrid`, filled with a gradient and a glow filter to compare values across categories. ```tsx 'use client' import { PolarAngleAxis, PolarGrid, Radar, RadarChart } from 'recharts' import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card' import type { ChartConfig } from '@/components/ikui/chart' import { ChartContainer, ChartTooltip, ChartTooltipContent, } from '@/components/ikui/chart' const chartData = [ { skill: 'Frontend', score: 92 }, { skill: 'Backend', score: 78 }, { skill: 'DevOps', score: 68 }, { skill: 'Design', score: 74 }, { skill: 'Testing', score: 85 }, { skill: 'Security', score: 62 }, ] const chartConfig = { score: { label: 'Proficiency', color: 'var(--chart-1)' }, } satisfies ChartConfig export function Demo() { return ( Skill Assessment Team proficiency across key areas } /> ) } ``` ### Radial bars A `RadialBarChart` rendering each series as a concentric arc, with gradient fills and a legend. ```tsx 'use client' import type { CSSProperties } from 'react' import { PolarAngleAxis, RadialBar, RadialBarChart } from 'recharts' import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card' import type { ChartConfig } from '@/components/ikui/chart' import { ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent, } from '@/components/ikui/chart' const chartData = [ { name: 'mobile', score: 58, fill: 'url(#chart28-mobile)' }, { name: 'desktop', score: 76, fill: 'url(#chart28-desktop)' }, { name: 'api', score: 92, fill: 'url(#chart28-api)' }, ] const chartConfig = { score: { label: 'Score' }, mobile: { label: 'Mobile', color: 'var(--chart-4)' }, desktop: { label: 'Desktop', color: 'var(--chart-2)' }, api: { label: 'API', color: 'var(--chart-1)' }, } satisfies ChartConfig export function Demo() { return ( Lighthouse Scores Performance audit by platform {(['mobile', 'desktop', 'api'] as const).map((key) => ( ))} (
{chartConfig[name as keyof typeof chartConfig] ?.label || name}
{Number(value)}/100
)} /> } /> } className="-translate-y-2" /> ) } ``` --- # Spark Chart ```tsx 'use client' import { SparkChart } from '@/components/ikui/spark-chart' const data = [ 369, 438, 417, 448, 531, 673, 625, 575, 610, 701, 746, 964, 970, 1180, 1072, 1348, 2286, 2481, 2875, 4703, 4339, 6468, 10234, 14156, 10221, 10398, 6974, 9241, 7494, 6404, 7780, 7038, 6626, 6318, 4017, 3743, 3458, 3855, 4105, 5351, 8575, 10817, 10086, 9631, 8294, 9200, 7570, 7194, 9351, 8600, 6439, 8659, 9461, 9138, 11323, 11681, 10784, 13781, 19626, 29002, 33114, 45138, 58919, 57750, 37333, 35041, 41626, 47167, 43791, 61319, 57005, 46306, 38483, 43193, 45539, 37715, 31792, 19785, 23337, 20050, 19432, 20496, 17169, 16547, 23139, 23147, 28478, 29269, 27220, 30477, 29230, 25931, 26968, 34668, 37713, 42265, 42583, 61198, 71334, 60637, 67491, 62678, 64619, 58970, 63330, 70215, 96449, 93429, 102405, 84373, 82549, 94207, 104638, 107135, 115758, 108237, 114056, 109556, 90394, 87509, 78621, 66996, 68233, 75776, 76454, ] const start = new Date(2016, 0, 1).getTime() const end = new Date(2026, 3, 30).getTime() const labels = data.map((_, i) => { const d = new Date(start + (i / (data.length - 1)) * (end - start)) const month = d.toLocaleDateString('en-US', { month: 'short' }) return `${month} '${String(d.getFullYear()).slice(-2)}` }) export function Demo() { return ( v.toLocaleString('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0, }) } /> ) } ``` A dependency-free interactive line chart drawn with a single SVG path. The line and fill live in a fixed `640×220` viewBox and scale to the container; the cursor, dot, and tooltip are HTML overlays positioned by percentage, so they track the data while staying a constant on-screen size. Hovering (or dragging on touch) snaps to the nearest data point, and the Y axis auto-scales to the data range. ## Installation ```bash npx shadcn@latest add @ikui/spark-chart ``` ## Usage ```tsx import { SparkChart } from "@/components/spark-chart"; ``` ```tsx const data = [12, 18, 15, 24, 22, 30, 28, 35, 42, 38, 48, 56]; ``` ## Examples ### Reveal effect Color the line only up to the cursor — grey before it, the series color after — by setting `reveal`. ```tsx 'use client' import { SparkChart } from '@/components/ikui/spark-chart' const data = [ 96492, 96910, 95892, 94316, 94748, 96802, 97032, 103241, 102971, 104696, 104106, 102813, 104170, 103539, 103745, 103489, 103191, 106446, 105606, 106791, 109678, 111673, 107288, 107791, 109035, 109440, 108995, 107802, 105642, 103999, 104638, 105652, 105882, 105432, 104732, 101576, 104390, 105616, 105794, 110294, 110257, 108687, 105929, 106091, 105472, 105552, 106797, 104601, 104883, 104684, 103310, 102257, 100987, 105578, 106046, 107361, 106960, 107088, 107328, 108386, 107135, 105698, 108859, 109648, 108034, 108231, 109232, 108300, 108950, 111327, 115987, 117517, 117435, 119116, 119850, 117777, 118739, 119290, 118003, 117940, 117301, 117440, 119995, 118755, 118368, 117636, 117947, 119448, 117924, 117922, 117831, 115758, 113320, 112527, 114218, 115072, 114141, 115028, 117497, 116689, 116500, 119307, 118731, 120173, 123344, 118360, 117398, 117491, 117453, 116252, 112831, 114275, 112419, 116874, 115374, 113458, 110124, 111803, 111222, 112545, 108411, 108808, 108237, 109251, 111201, 111723, 110724, 110651, 110225, 111168, 112071, 111531, 113955, 115508, 116102, 115951, 115408, 115445, 116843, 116469, 117137, 115689, 115722, 115306, 112749, 112015, 113329, 109049, 109713, 109682, 112123, 114400, 114056, 118649, 120681, 122267, 122425, 123513, 124753, 121451, 123355, 121706, 113214, 110808, 115170, 115271, 113119, 110783, 108186, 106468, 107198, 108667, 110589, 108477, 107689, 110070, 111034, 111642, 114472, 114119, 112956, 110055, 108306, 109556, 110064, 110640, 106548, 101591, 103892, 101301, 103372, 102282, 104720, 105997, 102997, 101663, 99697, 94398, 95549, 94177, 92094, 92949, 91466, 86632, 85091, 84648, 86805, 88271, 87342, 90518, 91285, 90919, 90852, 90394, 86322, 91350, 93528, 92142, 89388, 89272, 90406, 90640, 92692, 92021, 92511, 90270, 90299, 88175, 86420, 87844, 86144, 85463, 88103, 88344, 88622, 88490, 87414, 87612, 87235, 87301, 87802, 87836, 87138, 88430, 87509, 88732, 89945, 90603, 91413, 93883, 93729, 91308, 91027, 90513, 90387, 90827, 91193, 95322, 96929, 95551, 95525, 95100, 93634, 92554, 88311, 89377, 89462, 89504, 89111, 86572, 88267, 89103, 89185, 84562, 84129, 78621, 76974, 78689, 75634, 73020, 62702, 70555, 69282, 70265, 70121, 68794, 66992, 66222, 68858, 69768, 68788, 68843, 67494, 66425, 66958, 68005, 68004, 67659, 64617, 64080, 67960, 67454, 65882, 66996, 65738, 68776, 68294, 72711, 70841, 68136, 67273, 65970, 68402, 69927, 70205, 70493, 70968, 71215, 72790, 74861, 73922, 71246, 69913, 70523, 68712, 67845, 70915, 70518, 71310, 68792, 66338, 66320, 65955, 66691, 68233, 68079, 66889, 66931, 67291, 68982, 68860, 71941, 71123, 71768, 72979, 73054, 70753, 74485, 74182, 74805, 75152, 77127, 75726, 73856, 75873, 76353, 78203, 78269, 77455, 77612, 78658, 77367, 76351, 75776, 76425, ] const start = new Date(2025, 4, 1).getTime() const end = new Date(2026, 3, 30).getTime() const labels = data.map((_, i) => { const d = new Date(start + (i / (data.length - 1)) * (end - start)) const month = d.toLocaleDateString('en-US', { month: 'short' }) return `${month} '${String(d.getFullYear()).slice(-2)}` }) export function Demo() { return ( v.toLocaleString('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0, }) } /> ) } ``` ### Custom tooltip format `formatValue` receives the value and its index, so the tooltip can render currency, units, or any custom node. ```tsx 'use client' import { SparkChart } from '@/components/ikui/spark-chart' const data = [ 2909.79, 3094.12, 3726.93, 3760.03, 3678.63, 3480.27, 3516.08, 3373.64, 2981.6, 3134.16, 3505.73, 3275.95, 2986.01, 2599.6, 2593.19, 2764.45, 2525.82, 2223.88, 2441.61, 2561.07, 2695.9, 2414.79, 2436.51, 2641.55, 2435.93, 2511.89, 2962.3, 3103.04, 3331.6, 3593.49, 4005.81, 3911.21, 3472.55, 3328.92, 3605.01, 3267.49, 3474.11, 3309.55, 3298.27, 2622.21, 2726.09, 2659.66, 2237.91, 2138.99, 1909.47, 1964.85, 1895.5, 1815.34, 1567.15, 1588.92, 1786.63, 1842.71, 2345.51, 2536.3, 2526.44, 2529.94, 2477.19, 2579.49, 2407.3, 2423.87, 2508.52, 2957.89, 3549.02, 3727.27, 3488.37, 4009.85, 4439.99, 4831.35, 4360.15, 4306.99, 4715.25, 4470.92, 4035.89, 4514.87, 3843.01, 3832.56, 3934.57, 3847.08, 3435.3, 3103.79, 2765.7, 3032.3, 3024.43, 3084.17, 2977.97, 2925.75, 3124.42, 3083.05, 3295.48, 2953.26, 2702.38, 2063.39, 2048.53, 1969.02, 1930.76, 1978.75, 2092.56, 2146.5, 1991.27, 2053.39, 2245.15, 2421.07, 2315.69, ] const start = new Date(2024, 4, 10).getTime() const end = new Date(2026, 3, 24).getTime() const labels = data.map((_, i) => { const d = new Date(start + (i / (data.length - 1)) * (end - start)) const month = d.toLocaleDateString('en-US', { month: 'short' }) return `${month} '${String(d.getFullYear()).slice(-2)}` }) export function Demo() { return ( v.toLocaleString('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0, }) } /> ) } ``` ### Custom color Pass any CSS color to `color`; the line, dot, and tooltip swatch follow it. ```tsx 'use client' import { SparkChart } from '@/components/ikui/spark-chart' const data = [ 183.05, 186.28, 187.43, 189.72, 189.84, 189.87, 191.04, 192.35, 190.9, 186.88, 189.98, 189.99, 190.29, 191.29, 192.25, 194.03, 194.35, 195.87, 194.48, 196.89, 193.12, 207.15, 213.07, 214.24, 212.49, 216.67, 214.29, 209.68, 207.49, 208.14, 209.07, 213.25, 214.1, 210.62, 216.75, 220.27, 221.55, 226.34, 227.82, 228.68, 232.98, 227.57, 230.54, 234.4, 234.82, 228.88, 224.18, 224.31, 223.96, 225.01, 218.54, 217.49, 217.96, 218.24, 218.8, 222.08, 218.36, 219.86, 209.27, 207.23, 209.82, 213.31, 216.24, 217.53, 221.27, 221.72, 224.72, 226.05, 225.89, 226.51, 226.4, 224.53, 226.84, 227.18, 228.03, 226.49, 229.79, 229, 222.77, 220.85, 222.38, 220.82, 220.91, 220.11, 222.66, 222.77, 222.5, 216.32, 216.79, 220.69, 228.87, 228.2, 226.47, 227.37, 226.37, 227.52, 227.79, 233, 226.21, 226.78, 225.67, 226.8, 221.69, 225.77, 229.54, 229.04, 227.55, 231.3, 233.85, 231.78, 232.15, 235, 236.48, 235.86, 230.76, 230.57, 231.41, 233.4, 233.67, 230.1, 225.91, 222.91, 222.01, 223.45, 222.72, 227.48, 226.96, 224.23, 224.23, 225.12, 228.22, 225, 228.02, 228.28, 229, 228.52, 229.87, 232.87, 235.06, 234.93, 237.33, 239.59, 242.65, 243.01, 243.04, 242.84, 246.75, 247.77, 246.49, 247.96, 248.13, 251.04, 253.48, 248.05, 249.79, 254.49, 255.27, 258.2, 259.02, 255.59, 252.2, 250.42, 243.85, 243.36, 245, 242.21, 242.7, 236.85, 234.4, 233.28, 237.87, 228.26, 229.98, 222.64, 223.83, 223.66, 222.78, 229.86, 238.26, 239.36, 237.59, 236, 228.01, 232.8, 232.47, 233.22, 227.63, 227.65, 232.62, 236.87, 241.53, 244.6, 244.47, 244.87, 245.83, 245.55, 247.1, 247.04, 240.36, 237.3, 241.84, 238.03, 235.93, 235.74, 235.33, 239.07, 227.48, 220.84, 216.98, 209.68, 213.49, 214, 212.69, 215.24, 214.1, 218.27, 220.73, 223.75, 221.53, 223.85, 217.9, 222.13, 223.19, 223.89, 203.19, 188.38, 181.46, 172.42, 198.85, 190.42, 198.15, 202.52, 202.14, 194.27, 196.98, 193.16, 199.74, 204.6, 208.37, 209.28, 210.14, 211.21, 212.5, 213.32, 205.35, 198.89, 198.51, 196.25, 197.49, 198.53, 210.79, 212.93, 212.33, 211.45, 211.26, 208.78, 206.86, 202.09, 201.36, 195.27, 200.21, 200.42, 199.95, 200.85, 201.7, 203.27, 202.82, 200.63, 203.92, 201.45, 202.67, 198.78, 199.2, 196.45, 198.42, 195.64, 196.58, 201, 201.5, 200.3, 201.56, 201, 201.08, 205.17, 207.82, 212.44, 213.55, 209.95, 210.01, 211.14, 212.41, 211.16, 208.62, 209.11, 210.16, 210.02, 211.18, 212.48, 214.4, 214.15, 213.76, 213.88, 214.05, 211.27, 209.05, 207.57, 202.38, 203.35, 202.92, 213.25, 220.03, 229.35, 227.18, 229.65, 233.33, 232.78, 231.59, 230.89, 230.56, 226.01, 224.9, 227.76, 227.16, 229.31, 230.49, 232.56, 232.14, 229.72, 238.47, 239.78, 239.69, 237.88, 234.35, 226.79, 230.03, 234.07, 236.7, 238.15, 238.99, 237.88, 245.5, 256.08, 254.43, 252.31, 256.87, 255.46, 254.43, 254.63, 255.45, 257.13, 258.02, 256.69, 256.48, 258.06, 254.04, 245.27, 247.66, 247.77, 249.34, 247.45, 252.29, 262.24, 262.77, 258.45, 259.58, 262.82, 268.81, 269, 269.7, 271.4, 270.37, 269.05, 270.04, 270.14, 269.77, 268.47, 269.43, 275.25, 273.47, 272.95, 272.41, 267.46, 267.44, 268.56, 266.25, 271.49, 275.92, 276.97, 277.55, 278.85, 283.1, 286.19, 284.15, 280.7, 278.78, 277.89, 277.18, 278.78, 278.03, 278.28, 274.11, 274.61, 271.84, 272.19, 273.67, 270.97, 272.36, 273.81, 273.4, 273.76, 273.08, 271.86, 271.01, 267.26, 262.36, 260.33, 259.04, 259.37, 260.25, 261.05, 259.96, 258.21, 255.53, 246.7, 247.65, 248.35, 248.04, 255.41, 258.27, 256.44, 258.28, 259.48, 270.01, 269.48, 276.49, 275.91, 278.12, 274.62, 273.68, 275.5, 261.73, 255.78, 263.88, 264.35, 260.58, 264.58, 266.18, 272.14, 274.23, 272.95, 264.18, 264.72, 263.75, 262.52, 260.29, 257.46, 259.88, 260.83, 260.81, 255.76, 250.12, 252.82, 254.23, 249.94, 248.96, 247.99, 251.49, 251.64, 252.62, 252.89, 248.8, 246.63, 253.79, 255.63, 255.92, 258.86, 253.5, 258.9, 260.49, 260.48, 259.2, 258.83, 266.43, 263.4, 270.23, 273.05, 266.17, 273.17, 273.43, 271.06, 267.61, 270.71, 270.17, 271.98, ] const start = new Date(2024, 4, 10).getTime() const end = new Date(2026, 3, 30).getTime() const labels = data.map((_, i) => { const d = new Date(start + (i / (data.length - 1)) * (end - start)) const month = d.toLocaleDateString('en-US', { month: 'short' }) return `${month} '${String(d.getFullYear()).slice(-2)}` }) export function Demo() { return ( v.toLocaleString('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 2, }) } /> ) } ``` ### Without animation Set `animated={false}` to snap the cursor, dot, and tooltip instantly instead of easing between points. ```tsx 'use client' import { SparkChart } from '@/components/ikui/spark-chart' const data = [ 89.88, 90.4, 91.36, 94.63, 94.36, 92.48, 94.78, 95.39, 94.95, 103.8, 106.47, 113.9, 114.82, 110.5, 109.63, 115, 116.44, 122.44, 121, 120.89, 121.79, 120.91, 125.2, 129.61, 131.88, 130.98, 135.58, 130.78, 126.57, 118.11, 126.09, 126.4, 123.99, 123.54, 124.3, 122.67, 128.28, 125.83, 128.2, 131.38, 134.91, 127.4, 129.24, 128.44, 126.36, 117.99, 121.09, 117.93, 123.54, 122.59, 114.25, 112.28, 113.06, 111.59, 103.73, 117.02, 109.21, 107.27, 100.45, 104.25, 98.91, 104.97, 104.75, 109.02, 116.14, 118.08, 122.86, 124.58, 130, 127.25, 128.5, 123.74, 129.37, 126.46, 128.3, 125.61, 117.59, 119.37, 108, 106.21, 107.21, 102.83, 106.47, 108.1, 116.91, 119.14, 119.1, 116.78, 115.59, 113.37, 117.87, 116, 116.26, 120.87, 123.51, 124.04, 121.4, 121.44, 117, 118.85, 122.85, 124.92, 127.72, 132.89, 132.65, 134.81, 134.8, 138.07, 131.6, 135.72, 136.93, 138, 143.71, 143.59, 139.56, 140.41, 141.54, 140.52, 141.25, 139.34, 132.76, 135.4, 136.05, 139.91, 145.61, 148.88, 147.63, 145.26, 148.29, 146.27, 146.76, 141.98, 140.15, 147.01, 145.89, 146.67, 141.95, 136.02, 136.92, 135.34, 138.25, 138.63, 140.26, 145.14, 145.06, 142.44, 138.81, 135.07, 139.31, 137.34, 134.25, 132, 130.39, 128.91, 130.68, 134.7, 139.67, 140.22, 139.93, 137.01, 137.49, 134.29, 138.31, 144.47, 149.43, 140.14, 140.11, 135.91, 133.23, 131.76, 136.24, 133.57, 137.71, 140.83, 147.07, 147.22, 142.62, 118.42, 128.99, 123.7, 124.65, 120.07, 116.66, 118.65, 124.83, 128.68, 129.84, 133.57, 132.8, 131.14, 135.29, 138.85, 139.4, 139.23, 140.11, 134.43, 130.28, 126.63, 131.28, 120.15, 124.92, 114.06, 115.99, 117.3, 110.57, 112.69, 106.98, 108.76, 115.74, 115.58, 121.67, 119.53, 115.43, 117.52, 118.53, 117.7, 121.41, 120.69, 113.76, 111.43, 109.67, 108.38, 110.15, 110.42, 101.8, 94.31, 97.64, 96.3, 114.33, 107.57, 110.93, 110.71, 112.2, 104.49, 101.49, 96.91, 98.89, 102.71, 106.43, 111.01, 108.73, 109.02, 108.92, 111.61, 114.5, 113.82, 113.54, 117.06, 117.37, 116.65, 123, 129.93, 135.34, 134.83, 135.4, 135.57, 134.38, 131.8, 132.83, 131.29, 135.5, 134.81, 139.19, 135.13, 137.38, 141.22, 141.92, 139.99, 141.72, 142.63, 143.96, 142.83, 145, 141.97, 144.69, 144.12, 145.48, 143.85, 144.17, 147.9, 154.31, 155.02, 157.75, 157.99, 153.3, 157.25, 159.34, 158.24, 160, 162.88, 164.1, 164.92, 164.07, 170.7, 171.37, 173, 172.41, 171.38, 167.03, 170.78, 173.74, 173.5, 176.75, 175.51, 179.27, 177.87, 173.72, 180, 178.26, 179.42, 180.77, 182.7, 182.06, 183.16, 181.59, 182.02, 180.45, 182.01, 175.64, 175.4, 174.98, 177.99, 179.81, 181.77, 181.6, 180.17, 174.18, 170.78, 170.62, 171.66, 167.02, 168.31, 170.76, 177.33, 177.17, 177.82, 177.75, 174.88, 170.29, 176.24, 176.67, 183.61, 178.43, 176.97, 177.69, 178.19, 181.85, 186.58, 187.24, 188.89, 187.62, 185.54, 185.04, 189.11, 192.57, 183.16, 188.32, 180.03, 179.83, 181.81, 183.22, 182.64, 181.16, 180.28, 182.16, 186.26, 191.49, 201.03, 207.04, 202.89, 202.49, 206.88, 198.69, 195.21, 188.08, 188.15, 199.05, 193.16, 193.8, 186.86, 190.17, 186.6, 181.36, 186.52, 180.64, 178.88, 182.55, 177.82, 180.26, 177, 179.92, 181.46, 179.59, 183.38, 182.41, 185.55, 184.97, 183.78, 180.93, 175.02, 176.29, 177.72, 170.94, 174.14, 180.99, 183.69, 189.21, 188.61, 190.53, 188.22, 187.54, 186.5, 188.85, 188.12, 187.24, 189.11, 185.04, 184.86, 184.94, 185.81, 183.14, 187.05, 186.23, 178.07, 183.32, 184.84, 187.67, 186.47, 188.52, 191.52, 192.51, 191.13, 185.61, 180.34, 174.19, 171.88, 185.41, 190.04, 188.54, 190.05, 186.94, 182.81, 184.97, 187.98, 187.9, 189.82, 191.55, 192.85, 195.56, 184.89, 177.19, 182.48, 180.05, 183.04, 183.34, 177.82, 182.65, 184.77, 186.03, 183.14, 180.25, 183.22, 181.93, 180.4, 178.56, 172.7, 175.64, 175.2, 178.68, 171.24, 167.52, 165.17, 174.4, 175.75, 177.39, 177.64, 178.1, 182.08, 183.91, 188.63, 189.31, 196.51, 198.87, 198.35, 201.68, 202.06, 199.88, 202.5, 199.64, 208.27, 216.61, 213.17, 209.25, 200.9, ] const start = new Date(2024, 4, 10).getTime() const end = new Date(2026, 3, 30).getTime() const labels = data.map((_, i) => { const d = new Date(start + (i / (data.length - 1)) * (end - start)) const month = d.toLocaleDateString('en-US', { month: 'short' }) return `${month} '${String(d.getFullYear()).slice(-2)}` }) export function Demo() { return ( v.toLocaleString('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 2, }) } /> ) } ``` ## Props | Prop | Type | Default | Description | | ---- | ---- | ------- | ----------- | | `data` | `number[]` | - | Numeric values to plot, left to right | | `labels` | `string[]` | - | Optional label per point (tooltip + X axis) | | `name` | `string` | - | Series name shown in the tooltip | | `color` | `string` | `\` | Line, dot, and swatch color (any CSS color) | | `width` | `number` | `640` | Maximum width in px (keeps a 640:220 ratio) | | `formatValue` | `(value: number, index: number) => React.ReactNode` | `v => v.toLocaleString()` | Format the tooltip value | | `showXAxis` | `boolean` | `true` | Show tick labels along the bottom | | `tickCount` | `number` | `6` | Target number of ticks on the X axis | | `reveal` | `boolean` | `false` | Grey the line until the cursor passes over it | | `showFill` | `boolean` | `true` | Show the gradient fill under the line | | `showDot` | `boolean` | `true` | Show the dot at the active point | | `animated` | `boolean` | `true` | Animate the cursor, dot, and tooltip on hover | | `className` | `string` | - | Extra classes on the root element | --- # QR Code ```tsx import { QRCode } from '@/components/ikui/qr-code' export function Demo() { return (
) } ``` A QR code generator with rounded finder patterns and dot-style data modules. Foreground and background default to theme tokens, so it adapts to light and dark mode out of the box. ## Installation ```bash npx shadcn@latest add @ikui/qr-code ``` ## Usage ```tsx import { QRCode } from "@/components/qr-code"; ``` ```tsx ``` ## Examples ### Colors ```tsx import { QRCode } from '@/components/ikui/qr-code' export function Demo() { return (
) } ``` ### Sizes Control the rendered pixel size with `size`. ```tsx import { QRCode } from '@/components/ikui/qr-code' export function Demo() { return (
) } ``` ### Quiet zone `marginSize` adds a quiet zone around the code, measured in modules. `0` (left) renders edge-to-edge; `4` (right) follows the spec's recommended margin. ```tsx import { QRCode } from '@/components/ikui/qr-code' export function Demo() { return (
) } ``` ### Border Toggle `bordered` to wrap the code in a themed border. ```tsx import { QRCode } from '@/components/ikui/qr-code' export function Demo() { return (
) } ``` ### Error correction Higher `errorLevel` adds redundancy (and modules), so the code survives more damage or overlay — L, M, Q, H from left to right. ```tsx import { QRCode } from '@/components/ikui/qr-code' const levels = ['L', 'M', 'Q', 'H'] as const export function Demo() { return (
{levels.map((level) => ( ))}
) } ``` ### Module shape `dotType` and `finderType` switch between the rounded dot style (left) and a classic square style (right) for higher scanner compatibility. ```tsx import { QRCode } from '@/components/ikui/qr-code' export function Demo() { return (
) } ``` ### Status overlay `status` overlays the code with `loading`, `expired`, or `scanned` feedback. `expired` blurs the code and exposes a refresh action via `onRefresh`. ```tsx 'use client' import * as React from 'react' import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs' import { QRCode } from '@/components/ikui/qr-code' const states = ['active', 'loading', 'expired', 'scanned'] as const type Status = (typeof states)[number] export function Demo() { const [status, setStatus] = React.useState('expired') return (
setStatus(value as Status)} > {states.map((s) => ( {s} ))} setStatus('active')} bordered />
) } ``` ### Center icon Drop an image in the center with `icon`. Bump `errorLevel` to `"H"` so the code stays scannable behind the logo. ```tsx import { QRCode } from '@/components/ikui/qr-code' export function Demo() { return ( ) } ``` ### Canvas render Set `type="canvas"` to rasterize to a ``. Canvas resolves colors once at draw time, so pass explicit `fgColor`/`bgColor` rather than theme tokens. ```tsx import { QRCode } from '@/components/ikui/qr-code' export function Demo() { return ( ) } ``` ### Download Pass a `ref` to get a `QRCodeHandle` with `download(filename?)` and `toDataURL(mimeType?)`. `canvas` exports a PNG, `svg` exports a vector `.svg`. Pass explicit `fgColor`/`bgColor` so the exported file keeps its colors outside the page. ```tsx 'use client' import { Download } from 'lucide-react' import * as React from 'react' import { Button } from '@/components/ui/button' import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs' import type { QRCodeHandle } from '@/components/ikui/qr-code' import { QRCode } from '@/components/ikui/qr-code' type Mode = 'svg' | 'canvas' export function Demo() { const qrRef = React.useRef(null) const [mode, setMode] = React.useState('svg') return (
setMode(value as Mode)}> SVG Canvas
) } ``` ## Props | Prop | Type | Default | Description | | ---- | ---- | ------- | ----------- | | `value` | `string` | - | The text or URL to encode in the QR code | | `type` | `` | `svg` | Render backend. SVG follows theme colors; canvas resolves them once at draw time. | | `size` | `number` | `268` | QR code size in pixels | | `fgColor` | `string` | `var(--foreground)` | Foreground (dark) color. Adapts to theme in dark mode. | | `bgColor` | `string` | `var(--background)` | Background (light) color. Adapts to theme in dark mode. | | `dotType` | `` | `dot` | Data module shape | | `finderType` | `` | `rounded` | Finder (corner) pattern shape | | `icon` | `string` | - | Image URL rendered in the center of the QR code (image URLs only) | | `` | `` | - | - | | `iconPadding` | `number` | `size * 0.03` | Padding of the background plate behind the center icon | | `marginSize` | `number` | `0` | Quiet-zone margin, measured in modules. 0 means no margin. | | `bordered` | `boolean` | `false` | Render a border around the QR code | | `status` | `` | `active` | Overlay state. 'expired' blurs the code and shows a refresh action. | | `onRefresh` | `() => void` | - | Called when the refresh action of the 'expired' overlay is clicked | | `errorLevel` | `` | `M` | Error correction level. L: 7%, M: 15%, Q: 25%, H: 30% | | `className` | `string` | - | Additional CSS classes | --- # Copy Button ```tsx import { CopyButton } from '@/components/ikui/copy-button' export function Demo() { return } ``` ## Installation ```bash npx shadcn@latest add @ikui/copy-button ``` ## Usage ```tsx import { CopyButton } from "@/components/copy-button"; ``` ```tsx ``` ## Examples ### Size ```tsx import { CopyButton } from '@/components/ikui/copy-button' export function Demo() { return (
) } ``` ### With label ```tsx import { CopyButton } from '@/components/ikui/copy-button' export function Demo() { return (
Copy Copy
) } ``` ## Props | Prop | Type | Default | Description | | ---- | ---- | ------- | ----------- | | `value` | `string | (() => string | Promise)` | - | Text to copy, or a (possibly async) function resolving to it on click | | `size` | `SizeVariant` | `default` | Button size. Options: sm, default, lg | | `timeout` | `number` | `1500` | How long the copied state stays before resetting, in ms | | `copyIcon` | `ReactNode` | - | Icon shown in the idle state | | `copiedIcon` | `ReactNode` | - | Icon shown after a successful copy | | `onCopy` | `(value: string) => void` | - | Called with the copied text after it is written to the clipboard | | `onCopyError` | `(error: unknown) => void` | - | Called when reading the value or writing to the clipboard fails | | `children` | `ReactNode` | - | Optional label rendered next to the icon, turning this into a text button | | `copiedChildren` | `ReactNode` | - | Label shown after a successful copy. Falls back to children when omitted | | `className` | `string` | - | Additional CSS classes | --- # Cascader ```tsx import type { CascaderOption } from '@/components/ikui/cascader' import { Cascader } from '@/components/ikui/cascader' const options: CascaderOption[] = [ { value: 'zhejiang', label: 'Zhejiang', children: [ { value: 'hangzhou', label: 'Hangzhou', children: [ { value: 'xihu', label: 'West Lake' }, { value: 'yuhang', label: 'Yuhang' }, ], }, { value: 'ningbo', label: 'Ningbo', children: [{ value: 'haishu', label: 'Haishu' }], }, ], }, { value: 'jiangsu', label: 'Jiangsu', children: [ { value: 'nanjing', label: 'Nanjing', children: [ { value: 'xuanwu', label: 'Xuanwu' }, { value: 'qinhuai', label: 'Qinhuai' }, ], }, { value: 'suzhou', label: 'Suzhou', children: [{ value: 'gusu', label: 'Gusu' }], }, ], }, ] export function Demo() { return } ``` A column-based cascading dropdown for picking a path through hierarchical data — regions, category trees, org charts. Each level opens the next column to its right; choosing a leaf commits the full path. The `value` is the array of option values from root to leaf. It is uncontrolled by default (`defaultValue`) or fully controlled via `value` + `onChange`. On small screens it switches from a popover to a bottom drawer automatically. ## Installation ```bash npx shadcn@latest add @ikui/cascader ``` ## Usage ```tsx import { Cascader, type CascaderOption } from "@/components/cascader"; ``` ```tsx const options: CascaderOption[] = [ { value: "zhejiang", label: "Zhejiang", children: [{ value: "hangzhou", label: "Hangzhou" }], }, ]; console.log(value, selected)} />; ``` ## Examples ### Controlled value Pass `value` and `onChange` to control the selection. `onChange` reports both the value path and the matching `CascaderOption[]`. ```tsx 'use client' import { useState } from 'react' import type { CascaderOption } from '@/components/ikui/cascader' import { Cascader } from '@/components/ikui/cascader' const options: CascaderOption[] = [ { value: 'zhejiang', label: 'Zhejiang', children: [ { value: 'hangzhou', label: 'Hangzhou', children: [ { value: 'xihu', label: 'West Lake' }, { value: 'yuhang', label: 'Yuhang' }, ], }, ], }, { value: 'jiangsu', label: 'Jiangsu', children: [ { value: 'nanjing', label: 'Nanjing', children: [ { value: 'xuanwu', label: 'Xuanwu' }, { value: 'qinhuai', label: 'Qinhuai' }, ], }, ], }, ] export function Demo() { const [value, setValue] = useState(['zhejiang', 'hangzhou', 'xihu']) return (

Value: {value.length ? value.join(' / ') : '(none)'}

) } ``` ### Expand on hover Set `expandTrigger="hover"` to open child columns on mouse-over instead of click. A leaf still requires a click to commit. ```tsx import type { CascaderOption } from '@/components/ikui/cascader' import { Cascader } from '@/components/ikui/cascader' const options: CascaderOption[] = [ { value: 'frontend', label: 'Frontend', children: [ { value: 'react', label: 'React', children: [ { value: 'next', label: 'Next.js' }, { value: 'remix', label: 'Remix' }, ], }, { value: 'vue', label: 'Vue', children: [{ value: 'nuxt', label: 'Nuxt' }], }, ], }, { value: 'backend', label: 'Backend', children: [ { value: 'node', label: 'Node.js', children: [ { value: 'hono', label: 'Hono' }, { value: 'express', label: 'Express' }, ], }, ], }, ] export function Demo() { return ( ) } ``` ### Disabled options Mark any option `disabled` to make it (and its branch) unselectable. Here `allowClear` is also turned off, so there is no clear button. ```tsx import type { CascaderOption } from '@/components/ikui/cascader' import { Cascader } from '@/components/ikui/cascader' const options: CascaderOption[] = [ { value: 'zhejiang', label: 'Zhejiang', children: [ { value: 'hangzhou', label: 'Hangzhou', children: [ { value: 'xihu', label: 'West Lake' }, { value: 'yuhang', label: 'Yuhang', disabled: true }, ], }, ], }, { value: 'jiangsu', label: 'Jiangsu', disabled: true, children: [{ value: 'nanjing', label: 'Nanjing' }], }, ] export function Demo() { return } ``` ### Custom display Use `displayRender` to control how the selected path renders in the trigger — here it shows only the leaf label instead of the full `a / b / c` path. ```tsx 'use client' import type { CascaderOption } from '@/components/ikui/cascader' import { Cascader } from '@/components/ikui/cascader' const options: CascaderOption[] = [ { value: 'zhejiang', label: 'Zhejiang', children: [ { value: 'hangzhou', label: 'Hangzhou', children: [ { value: 'xihu', label: 'West Lake' }, { value: 'yuhang', label: 'Yuhang' }, ], }, ], }, ] export function Demo() { return ( labels.at(-1)} /> ) } ``` ## Props | Prop | Type | Default | Description | | ---- | ---- | ------- | ----------- | | `options` | `CascaderOption[]` | - | The hierarchical option tree | | `value` | `string[]` | - | Controlled selected path (option values root → leaf) | | `defaultValue` | `string[]` | - | Initial path when uncontrolled | | `onChange` | `(value: string[], selectedOptions: CascaderOption[]) => void` | - | Fired when a leaf is selected or the value is cleared | | `placeholder` | `string` | - | Trigger text when nothing is selected | | `disabled` | `boolean` | `false` | Disable the whole control | | `allowClear` | `boolean` | `true` | Show a clear (×) button when a value is selected | | `expandTrigger` | `` | - | How child columns open | | `displayRender` | `(labels: string[], selectedOptions: CascaderOption[]) => React.ReactNode` | - | Custom render of the selected path in the trigger | | `className` | `string` | - | Class applied to the trigger | | `popupClassName` | `string` | - | Class applied to the popover / drawer panel | ### CascaderOption | Prop | Type | Default | Description | | ---- | ---- | ------- | ----------- | | `value` | `string` | - | Unique value within its level | | `label` | `React.ReactNode` | - | Rendered label in the column | | `textLabel` | `string` | - | Plain-text fallback used in the trigger when label is not a string | | `disabled` | `boolean` | - | Make this option unselectable | | `children` | `CascaderOption[]` | - | Child options — presence makes this a non-leaf node | --- # Audio Waveform ```tsx 'use client' import { useEffect, useState } from 'react' import { AudioWaveform } from '@/components/ikui/audio-waveform' import { createSampleBlob } from './sample-audio' export function Demo() { const [blob, setBlob] = useState(null) useEffect(() => { setBlob(createSampleBlob()) }, []) if (!blob) return null return (
) } ``` A static waveform built for timelines. It decodes an audio source (URL, `Blob`, or an already-decoded `AudioBuffer`) into normalized peaks — or takes a precomputed `peaks` array — and renders mirrored bars onto a canvas at an **explicit pixel width**. Decoding happens once; changing the `width` (e.g. on zoom) only re-draws, resampling the cached peaks. An optional `progress` fraction colors the played portion. Decode once and draw at any pixel width — it's built for a zoomable, horizontally-scrollable editor timeline track, not a fixed container. It also backs [`waveform-player`](./waveform-player), where the played portion is colored via `progress` and the bar doubles as a seek scrubber. ## Installation ```bash npx shadcn@latest add @ikui/audio-waveform ``` ## Usage ```tsx import { AudioWaveform } from "@/components/audio-waveform"; ``` ```tsx ``` Decode once and reuse the peaks across re-renders (or share them with another view) via `onDecoded`: ```tsx const [peaks, setPeaks] = useState() // first mount decodes; later renders pass `peaks` to skip decoding {peaks && } ``` ## Examples ### Zoomable timeline track The component is built for timelines: it decodes once, then draws at an explicit pixel `width`. Zoom widens the canvas and scrolls instead of re-fitting, reusing the cached peaks; click to move the `progress` split. ```tsx 'use client' import { Minus, Plus } from 'lucide-react' import { useEffect, useRef, useState } from 'react' import { Button } from '@/components/ui/button' import { AudioWaveform } from '@/components/ikui/audio-waveform' import { createSampleBlob } from './sample-audio' const DURATION = 3 const BASE_PPS = 160 const MIN_ZOOM = 1 const MAX_ZOOM = 6 const HEIGHT = 56 export function Demo() { const [blob, setBlob] = useState(null) const [zoom, setZoom] = useState(2) const [progress, setProgress] = useState(0.4) const scrollRef = useRef(null) useEffect(() => { setBlob(createSampleBlob()) }, []) if (!blob) return null const width = DURATION * BASE_PPS * zoom const setFromEvent = (e: React.MouseEvent) => { const rect = e.currentTarget.getBoundingClientRect() setProgress(Math.min(Math.max((e.clientX - rect.left) / width, 0), 1)) } return (
{(progress * 100).toFixed(0)}% decoded once · drawn at {width}px
{zoom.toFixed(1)}x

A timeline waveform: it decodes the audio once , then draws at an explicit pixel width — zoom in and it widens and scrolls instead of re-fitting, reusing the same cached peaks. Click to move the progress split.

) } ``` ### Custom bars Tune the bar geometry and colors: `barWidth` and `gap` set the bar shape, `rounded` toggles the caps, and `barColor` / `barPlayedColor` color the unplayed and played sides of the `progress` split. ```tsx 'use client' import { useEffect, useState } from 'react' import { AudioWaveform } from '@/components/ikui/audio-waveform' import { createSampleBlob } from './sample-audio' export function Demo() { const [blob, setBlob] = useState(null) useEffect(() => { setBlob(createSampleBlob()) }, []) if (!blob) return null return (
) } ``` ## Props | Prop | Type | Default | Description | | ---- | ---- | ------- | ----------- | | `audioUrl` | `string` | - | Audio URL to fetch and decode. Provide one of url / blob / audioBuffer / peaks | | `blob` | `Blob` | - | Audio blob to decode | | `audioBuffer` | `AudioBuffer` | - | Already-decoded buffer — skips fetching and decoding | | `peaks` | `number[]` | - | Precomputed normalized peaks (each 0..1). Skips decoding entirely | | `width` | `number` | - | Explicit width in CSS px (drives zoom). Defaults to the container width | | `height` | `number` | - | Height in CSS px | | `barWidth` | `number` | `2` | Bar width in px | | `gap` | `number` | `1` | Gap between bars in px | | `minBarHeight` | `number` | `2` | Minimum bar height in px, so near-silence stays visible | | `backgroundColor` | `string` | - | Background fill | | `barColor` | `string` | - | Bar color | | `barPlayedColor` | `string` | - | Color for bars left of progress. Falls back to barColor when unset | | `progress` | `number` | `0` | Played fraction 0..1. Bars before it use barPlayedColor | | `rounded` | `boolean` | `true` | Rounded bar caps | | `onDecoded` | `(peaks: number[]) => void` | - | Called once with the normalized peaks after decoding | | `className` | `string` | - | Class applied to the wrapper | | `style` | `React.CSSProperties` | - | Inline styles applied to the wrapper | --- # Waveform Player ```tsx 'use client' import { useEffect, useState } from 'react' import { createSampleBlob } from '@/docs/audio-waveform/sample-audio' import { WaveformPlayer } from '@/components/ikui/waveform-player' export function Demo() { const [blob, setBlob] = useState(null) useEffect(() => { setBlob(createSampleBlob()) }, []) if (!blob) return null return (
) } ``` ## Installation ```bash npx shadcn@latest add @ikui/waveform-player ``` ## Usage ```tsx import { WaveformPlayer } from "@/components/waveform-player"; ``` ```tsx ``` ```tsx ``` Click or drag anywhere across the waveform to seek to that position. ## Examples ### Custom colors `barColor` sets the unplayed bars and `barPlayedColor` the played portion (left of the playhead). Both accept any CSS color string. ```tsx 'use client' import { useEffect, useState } from 'react' import { createSampleBlob } from '@/docs/audio-waveform/sample-audio' import { WaveformPlayer } from '@/components/ikui/waveform-player' export function Demo() { const [blob, setBlob] = useState(null) useEffect(() => { setBlob(createSampleBlob()) }, []) if (!blob) return null return (
) } ``` ## Props | Prop | Type | Default | Description | | ---- | ---- | ------- | ----------- | | `blob` | `Blob` | - | Audio blob to play and visualize. Takes precedence over url | | `url` | `string` | - | Audio URL to play and visualize | | `width` | `number` | - | Width of the visualizer. Defaults to the container width | | `height` | `number` | `84` | Height of the visualizer | | `barWidth` | `number` | `2` | Width of each individual bar | | `gap` | `number` | `1` | Gap between each bar | | `barColor` | `string` | - | Color of the unplayed bars | | `barPlayedColor` | `string` | `rgb(34, 197, 94)` | Color of the played bars | | `className` | `string` | - | Additional CSS classes for the wrapper | | `onPlayStateChange` | `(playing: boolean) => void` | - | Called whenever playback starts or stops | --- # Thumbnail Strip ```tsx 'use client' import { useEffect, useState } from 'react' import { Skeleton } from '@/components/ui/skeleton' import { ThumbnailStrip } from '@/components/ikui/thumbnail-strip' import { VideoThumbnailCache } from '@/components/ikui/video-thumbnail-cache' const VIDEO_URL = 'https://hj-video.zeroaigen.cn/prod/AI/VIDEO/f4e7fdc9807348eedc1e64a963c7433e.mp4' const PIXELS_PER_SECOND = 96 const TILE_WIDTH = 96 const TILE_HEIGHT = 60 type LoadState = | { kind: 'loading' } | { kind: 'error'; message: string } | { kind: 'ready'; cache: VideoThumbnailCache; duration: number } export function Demo() { const [state, setState] = useState({ kind: 'loading' }) useEffect(() => { let cancelled = false let cache: VideoThumbnailCache | null = null void VideoThumbnailCache.fromUrl(VIDEO_URL) .then((c) => { if (cancelled) { c.dispose() return } cache = c const meta = c.getMetadata() if (!meta) throw new Error('metadata missing after init') setState({ kind: 'ready', cache: c, duration: meta.duration }) }) .catch((err: unknown) => { if (cancelled) return setState({ kind: 'error', message: err instanceof Error ? err.message : String(err), }) }) return () => { cancelled = true cache?.dispose() } }, []) if (state.kind === 'loading') { return ( ) } if (state.kind === 'error') { return (

Failed to load video: {state.message}

) } const totalWidth = Math.ceil(state.duration * PIXELS_PER_SECOND) return (
) } ``` A horizontal canvas strip that lays a video's frames out along a timeline, one frame per tile. Designed for video editor track rows and scrubbable overviews — viewport-virtualized so only the visible range is decoded and painted. Pairs with the [`video-thumbnail-cache`](./video-thumbnail-cache) lib, which wraps mediabunny and handles frame decoding, caching, and eviction. The strip itself only consumes a `cache` reference plus the visual parameters. ## Installation The strip pulls `video-thumbnail-cache` (and transitively `mediabunny`) in automatically — one install gives you the full pair. ```bash npx shadcn@latest add @ikui/thumbnail-strip ``` ## Usage ```tsx import { ThumbnailStrip } from "@/components/thumbnail-strip"; import { VideoThumbnailCache } from "@/lib/video-thumbnail-cache"; ``` ```tsx const cache = await VideoThumbnailCache.fromUrl(videoUrl) const { duration } = cache.getMetadata()! ``` The strip computes `secondsPerTile = (tileWidth / totalWidth) * duration` internally. To "zoom", change `totalWidth`. To window into a sub-range of the video (e.g. trimmed clip), pass `startOffset` and a shorter `duration`. The component auto-detects the nearest scrollable ancestor and only paints the visible range plus a 200 px overscan. Frames missing from the cache are queued onto a 150 ms debounced batch; results stream back through `cache.loadBitmaps` and paint incrementally. A nearest-cached frame is painted as a placeholder while the exact one resolves. By default (`autoFallback`) the strip also decodes the first frame and tiles it beneath the canvas, so the row is filled instantly and never flashes blank before tiles load. Pass an explicit `fallbackUrl` to override the poster, or `autoFallback={false}` to disable it. ## Examples ### Windowed sub-range Pass `startOffset` plus a shorter `duration` to window into part of the video — here the middle half, skipping the first quarter. One cache can back any number of such sub-ranges (e.g. a trimmed clip), each strip painting only its own window. ```tsx 'use client' import { useEffect, useState } from 'react' import { Skeleton } from '@/components/ui/skeleton' import { ThumbnailStrip } from '@/components/ikui/thumbnail-strip' import { VideoThumbnailCache } from '@/components/ikui/video-thumbnail-cache' const VIDEO_URL = 'https://hj-video.zeroaigen.cn/prod/AI/VIDEO/f4e7fdc9807348eedc1e64a963c7433e.mp4' const PIXELS_PER_SECOND = 96 const TILE_WIDTH = 96 const TILE_HEIGHT = 60 type LoadState = | { kind: 'loading' } | { kind: 'error'; message: string } | { kind: 'ready'; cache: VideoThumbnailCache; duration: number } export function Demo() { const [state, setState] = useState({ kind: 'loading' }) useEffect(() => { let cancelled = false let cache: VideoThumbnailCache | null = null void VideoThumbnailCache.fromUrl(VIDEO_URL) .then((c) => { if (cancelled) { c.dispose() return } cache = c const meta = c.getMetadata() if (!meta) throw new Error('metadata missing after init') setState({ kind: 'ready', cache: c, duration: meta.duration }) }) .catch((err: unknown) => { if (cancelled) return setState({ kind: 'error', message: err instanceof Error ? err.message : String(err), }) }) return () => { cancelled = true cache?.dispose() } }, []) if (state.kind === 'loading') { return (
) } if (state.kind === 'error') { return (

Failed to load video: {state.message}

) } // Window into the middle of the clip: skip the first quarter, show the // middle half. The same cache could back any number of such sub-ranges. const startOffset = state.duration * 0.25 const windowDuration = state.duration * 0.5 const totalWidth = Math.ceil(windowDuration * PIXELS_PER_SECOND) return (
skips {startOffset.toFixed(1)}s · shows {windowDuration.toFixed(1)}s of{' '} {state.duration.toFixed(1)}s
) } ``` ## Props | Prop | Type | Default | Description | | ---- | ---- | ------- | ----------- | | `cache` | `VideoThumbnailCache` | - | Frame source — see the video-thumbnail-cache docs | | `duration` | `number` | - | Duration of the visible range in seconds | | `startOffset` | `number` | `0` | Seconds skipped at the start of the cache's video | | `totalWidth` | `number` | - | Total strip width in CSS pixels | | `tileWidth` | `number` | - | Width of each tile in CSS pixels | | `tileHeight` | `number` | - | Strip and tile height in CSS pixels | | `fallbackUrl` | `string` | - | Single image tiled with repeat-x beneath the canvas as an instant fallback. Takes precedence over the auto-generated poster | | `autoFallback` | `boolean` | `true` | When no fallbackUrl is set, decode the first frame (at startOffset) and tile it as the fallback poster so the strip is never blank before tiles load | | `objectFit` | `` | - | How decoded frames fit into each tile | | `scrollContainer` | `HTMLElement | null` | - | Scroll container override. Auto-detected when undefined; null uses the viewport | | `className` | `string` | - | Class applied to the outer wrapper | | `style` | `React.CSSProperties` | - | Inline styles applied to the outer wrapper | --- # Segmented Timeline Strip ```tsx 'use client' import { useEffect, useMemo, useState } from 'react' import { Skeleton } from '@/components/ui/skeleton' import type { TimelineSegment } from '@/components/ikui/segmented-timeline-strip' import { SegmentedTimelineStrip } from '@/components/ikui/segmented-timeline-strip' import { VideoThumbnailCache } from '@/components/ikui/video-thumbnail-cache' const VIDEO_URL = 'https://hj-video.zeroaigen.cn/prod/AI/VIDEO/53e46f7949f0d57b77b0cfe47ecf0301.mp4' const SEGMENT_COUNT = 5 type LoadState = | { kind: 'loading' } | { kind: 'error'; message: string } | { kind: 'ready'; cache: VideoThumbnailCache; duration: number } export function Demo() { const [state, setState] = useState({ kind: 'loading' }) const [currentIndex, setCurrentIndex] = useState(0) const [currentTime, setCurrentTime] = useState(0) useEffect(() => { let cancelled = false let cache: VideoThumbnailCache | null = null void VideoThumbnailCache.fromUrl(VIDEO_URL) .then((c) => { if (cancelled) { c.dispose() return } cache = c const meta = c.getMetadata() if (!meta) throw new Error('metadata missing after init') setState({ kind: 'ready', cache: c, duration: meta.duration }) }) .catch((err: unknown) => { if (cancelled) return setState({ kind: 'error', message: err instanceof Error ? err.message : String(err), }) }) return () => { cancelled = true cache?.dispose() } }, []) const segments = useMemo(() => { if (state.kind !== 'ready') return [] const segDur = state.duration / SEGMENT_COUNT return Array.from({ length: SEGMENT_COUNT }, (_, i) => ({ id: i, cache: state.cache, duration: segDur, startOffset: i * segDur, label: String(i + 1), })) }, [state]) if (state.kind === 'loading') { return } if (state.kind === 'error') { return (

Failed to load video: {state.message}

) } return (
{ setCurrentIndex(segmentIndex) setCurrentTime(timeWithinSegment) }} className="rounded-md" />
) } ``` A horizontal timeline overview that concatenates multiple video segments end-to-end. Each segment is rendered by a [`thumbnail-strip`](./thumbnail-strip); the wrapper layers DOM chrome on top — active-segment border, dim mask, segment label, playhead, total duration badge — and translates click-to-seek into per-segment coordinates. Segment widths are proportional to `duration`. The whole strip fills its container; no horizontal scrolling. Multiple segments may share one `VideoThumbnailCache` (different `startOffset`s into the same video) or each carry its own cache (multi-clip projects). ## Installation Pulls `thumbnail-strip` and `video-thumbnail-cache` (and transitively `mediabunny`) automatically. ```bash npx shadcn@latest add @ikui/segmented-timeline-strip ``` ## Usage ```tsx import { SegmentedTimelineStrip, type TimelineSegment, } from "@/components/segmented-timeline-strip"; import { VideoThumbnailCache } from "@/lib/video-thumbnail-cache"; ``` ```tsx const cache = await VideoThumbnailCache.fromUrl(videoUrl) const { duration } = cache.getMetadata()! const segDur = duration / 5 const segments: TimelineSegment[] = Array.from({ length: 5 }, (_, i) => ({ id: i, cache, duration: segDur, startOffset: i * segDur, label: String(i + 1), })) { video.currentTime = segmentIndex * segDur + timeWithinSegment }} /> ``` To wire a video element's `timeupdate` to the playhead, lift `currentTime` into React state and feed the strip the *segment-local* time (subtract the current segment's `startOffset`). ## Examples ### Wired to a video player Each segment can instead carry its own `VideoThumbnailCache` (a multi-clip project). Lift `currentTime` from the `